diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index f546c9c8e9..c8a21c5fda 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -108,37 +108,11 @@ static const std::map cosmeticsRandomizerModes = { { RANDOMIZE_ON_FILE_LOAD_SEEDED, "On File Load (Seeded)" }, }; -typedef struct { - const char* cvar; - const char* valuesCvar; - const char* rainbowCvar; - const char* lockedCvar; - const char* changedCvar; - std::string label; - CosmeticGroup group; - ImVec4 currentColor; - Color_RGBA8 defaultColor; - bool supportsAlpha; - bool supportsRainbow; - bool advancedOption; -} CosmeticOption; - Color_RGBA8 ColorRGBA8(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { Color_RGBA8 color = { r, g, b, a }; return color; } -#define COSMETIC_OPTION(id, label, group, defaultColor, supportsAlpha, supportsRainbow, advancedOption) \ - { \ - id, { \ - CVAR_COSMETIC(id), CVAR_COSMETIC(id ".Value"), CVAR_COSMETIC(id ".Rainbow"), CVAR_COSMETIC(id ".Locked"), \ - CVAR_COSMETIC(id ".Changed"), label, group, \ - ImVec4(defaultColor.r / 255.0f, defaultColor.g / 255.0f, defaultColor.b / 255.0f, \ - defaultColor.a / 255.0f), \ - defaultColor, supportsAlpha, supportsRainbow, advancedOption \ - } \ - } - // clang-format off /* So, you would like to add a new cosmetic option? BUCKLE UP @@ -213,7 +187,7 @@ Color_RGBA8 ColorRGBA8(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { in the moon cosmetic, where for the gDPSetEnvColor color we are halving the RGB values, to make them a bit darker similar to how the original colors were darker than the gDPSetPrimColor. You will see many more examples of this below in the `ApplyOrResetCustomGfxPatches` method */ -static std::map cosmeticOptions = { +std::map cosmeticOptions = { COSMETIC_OPTION("Link.KokiriTunic", "Kokiri Tunic", COSMETICS_GROUP_LINK, ColorRGBA8( 30, 105, 27, 255), false, true, false), COSMETIC_OPTION("Link.GoronTunic", "Goron Tunic", COSMETICS_GROUP_LINK, ColorRGBA8(100, 20, 0, 255), false, true, false), COSMETIC_OPTION("Link.ZoraTunic", "Zora Tunic", COSMETICS_GROUP_LINK, ColorRGBA8( 0, 60, 100, 255), false, true, false), @@ -562,7 +536,10 @@ void CosmeticsUpdateTick() { index += static_cast(60 * rainbowSpeed); } } + UpdateCustomCosmeticsRainbow(hue, rainbowSpeed, index); + ApplyOrResetCustomGfxPatches(false); + ApplyCustomCosmetics(); hue++; if (hue >= (360 * rainbowSpeed)) { hue = 0; @@ -2148,68 +2125,6 @@ void RandomizeColor(CosmeticOption& cosmeticOption, bool manual = true) { ApplySideEffects(cosmeticOption); } -void ResetColor(CosmeticOption& cosmeticOption) { - Color_RGBA8 defaultColor = { cosmeticOption.defaultColor.r, cosmeticOption.defaultColor.g, - cosmeticOption.defaultColor.b, cosmeticOption.defaultColor.a }; - cosmeticOption.currentColor.x = defaultColor.r / 255.0f; - cosmeticOption.currentColor.y = defaultColor.g / 255.0f; - cosmeticOption.currentColor.z = defaultColor.b / 255.0f; - cosmeticOption.currentColor.w = defaultColor.a / 255.0f; - - CVarClear(cosmeticOption.changedCvar); - CVarClear(cosmeticOption.rainbowCvar); - CVarClear(cosmeticOption.lockedCvar); - CVarClear(cosmeticOption.valuesCvar); - CVarClear((std::string(cosmeticOption.valuesCvar) + ".R").c_str()); - CVarClear((std::string(cosmeticOption.valuesCvar) + ".G").c_str()); - CVarClear((std::string(cosmeticOption.valuesCvar) + ".B").c_str()); - CVarClear((std::string(cosmeticOption.valuesCvar) + ".A").c_str()); - CVarClear((std::string(cosmeticOption.valuesCvar) + ".Type").c_str()); - - // This portion should match 1:1 the multiplied colors in `ApplySideEffect()` - if (cosmeticOption.label == "Bow Body") { - ResetColor(cosmeticOptions.at("Equipment.BowTips")); - ResetColor(cosmeticOptions.at("Equipment.BowHandle")); - } else if (cosmeticOption.label == "Idle Primary") { - ResetColor(cosmeticOptions.at("Navi.IdleSecondary")); - } else if (cosmeticOption.label == "Enemy Primary") { - ResetColor(cosmeticOptions.at("Navi.EnemySecondary")); - } else if (cosmeticOption.label == "NPC Primary") { - ResetColor(cosmeticOptions.at("Navi.NPCSecondary")); - } else if (cosmeticOption.label == "Props Primary") { - ResetColor(cosmeticOptions.at("Navi.PropsSecondary")); - } else if (cosmeticOption.label == "Level 1 Secondary") { - ResetColor(cosmeticOptions.at("SpinAttack.Level1Primary")); - } else if (cosmeticOption.label == "Level 2 Secondary") { - ResetColor(cosmeticOptions.at("SpinAttack.Level2Primary")); - } else if (cosmeticOption.label == "Item Select Color") { - ResetColor(cosmeticOptions.at("Kaleido.ItemSelB")); - ResetColor(cosmeticOptions.at("Kaleido.ItemSelC")); - ResetColor(cosmeticOptions.at("Kaleido.ItemSelD")); - } else if (cosmeticOption.label == "Equip Select Color") { - ResetColor(cosmeticOptions.at("Kaleido.EquipSelB")); - ResetColor(cosmeticOptions.at("Kaleido.EquipSelC")); - ResetColor(cosmeticOptions.at("Kaleido.EquipSelD")); - } else if (cosmeticOption.label == "Map Dungeon Color") { - ResetColor(cosmeticOptions.at("Kaleido.MapSelDunB")); - ResetColor(cosmeticOptions.at("Kaleido.MapSelDunC")); - ResetColor(cosmeticOptions.at("Kaleido.MapSelDunD")); - } else if (cosmeticOption.label == "Quest Status Color") { - ResetColor(cosmeticOptions.at("Kaleido.QuestStatusB")); - ResetColor(cosmeticOptions.at("Kaleido.QuestStatusC")); - ResetColor(cosmeticOptions.at("Kaleido.QuestStatusD")); - } else if (cosmeticOption.label == "Map Color") { - ResetColor(cosmeticOptions.at("Kaleido.MapSelectB")); - ResetColor(cosmeticOptions.at("Kaleido.MapSelectC")); - ResetColor(cosmeticOptions.at("Kaleido.MapSelectD")); - } else if (cosmeticOption.label == "Save Color") { - ResetColor(cosmeticOptions.at("Kaleido.SaveB")); - ResetColor(cosmeticOptions.at("Kaleido.SaveC")); - ResetColor(cosmeticOptions.at("Kaleido.SaveD")); - } - ShipInit::Init(cosmeticOption.valuesCvar); -} - void DrawCosmeticRow(CosmeticOption& cosmeticOption) { if (UIWidgets::CVarColorPicker(cosmeticOption.label.c_str(), cosmeticOption.cvar, cosmeticOption.defaultColor, cosmeticOption.supportsAlpha, 0, THEME_COLOR)) { @@ -2509,6 +2424,14 @@ void CosmeticsEditorWindow::DrawElement() { ImGui::EndTabItem(); } + if (HasCustomCosmetics() && ImGui::BeginTabItem("Mods")) { + + UIWidgets::Separator(true, true, 2.0f, 2.0f); + + DrawCustomCosmetics(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Keys")) { ImGui::BeginDisabled(CVarGetInteger(CVAR_SETTING("DisableChanges"), 0)); @@ -2623,8 +2546,10 @@ void CosmeticsEditorWindow::InitElement() { cosmeticOption.currentColor.w = cvarColor.a / 255.0f; } Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ScanCustomCosmetics(); ApplyOrResetCustomGfxPatches(); ApplyAuthenticGfxPatches(); + ApplyCustomCosmetics(); } void CosmeticsEditor_RandomizeAll() { @@ -2649,6 +2574,7 @@ void CosmeticsEditor_AutoRandomizeAll() { Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); ApplyOrResetCustomGfxPatches(); + ApplyCustomCosmetics(); } void CosmeticsEditor_RandomizeGroup(CosmeticGroup group) { @@ -2692,7 +2618,10 @@ void RegisterCosmeticHooks() { []() { CosmeticsEditor_AutoRandomizeAll(); }); COND_HOOK(OnLoadGame, CVarGetInteger(CVAR_COSMETIC("RandomizeCosmeticsGenModes"), RANDOMIZE_OFF) == RANDOMIZE_OFF, - [](s32 fileNum) { ApplyOrResetCustomGfxPatches(); }); + [](s32 fileNum) { + ApplyOrResetCustomGfxPatches(); + ApplyCustomCosmetics(); + }); COND_HOOK(OnLoadGame, CVarGetInteger(CVAR_COSMETIC("RandomizeCosmeticsGenModes"), RANDOMIZE_OFF) == RANDOMIZE_ON_FILE_LOAD, diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.h b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.h index 7da88bdbc6..c432d49d10 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.h +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.h @@ -30,6 +30,9 @@ typedef enum { } CosmeticGroup; #ifdef __cplusplus +#include +#include +#include "soh/SohGui/UIWidgets.hpp" extern "C" { #endif //__cplusplus @@ -38,6 +41,114 @@ Color_RGBA8 CosmeticsEditor_GetDefaultValue(const char* id); #ifdef __cplusplus } +#define COSMETIC_OPTION(id, label, group, defaultColor, supportsAlpha, supportsRainbow, advancedOption) \ + { \ + id, { \ + CVAR_COSMETIC(id), CVAR_COSMETIC(id ".Value"), CVAR_COSMETIC(id ".Rainbow"), CVAR_COSMETIC(id ".Locked"), \ + CVAR_COSMETIC(id ".Changed"), label, group, \ + ImVec4(defaultColor.r / 255.0f, defaultColor.g / 255.0f, defaultColor.b / 255.0f, \ + defaultColor.a / 255.0f), \ + defaultColor, supportsAlpha, supportsRainbow, advancedOption \ + } \ + } + +typedef struct { + const char* cvar; + const char* valuesCvar; + const char* rainbowCvar; + const char* lockedCvar; + const char* changedCvar; + std::string label; + CosmeticGroup group; + ImVec4 currentColor; + Color_RGBA8 defaultColor; + bool supportsAlpha; + bool supportsRainbow; + bool advancedOption; +} CosmeticOption; + +extern std::map cosmeticOptions; + +inline void ResetColor(CosmeticOption& cosmeticOption) { + Color_RGBA8 defaultColor = { cosmeticOption.defaultColor.r, cosmeticOption.defaultColor.g, + cosmeticOption.defaultColor.b, cosmeticOption.defaultColor.a }; + cosmeticOption.currentColor.x = defaultColor.r / 255.0f; + cosmeticOption.currentColor.y = defaultColor.g / 255.0f; + cosmeticOption.currentColor.z = defaultColor.b / 255.0f; + cosmeticOption.currentColor.w = defaultColor.a / 255.0f; + + CVarClear(cosmeticOption.changedCvar); + CVarClear(cosmeticOption.rainbowCvar); + CVarClear(cosmeticOption.lockedCvar); + CVarClear(cosmeticOption.valuesCvar); + CVarClear((std::string(cosmeticOption.valuesCvar) + ".R").c_str()); + CVarClear((std::string(cosmeticOption.valuesCvar) + ".G").c_str()); + CVarClear((std::string(cosmeticOption.valuesCvar) + ".B").c_str()); + CVarClear((std::string(cosmeticOption.valuesCvar) + ".A").c_str()); + CVarClear((std::string(cosmeticOption.valuesCvar) + ".Type").c_str()); + + if (cosmeticOption.label == "Bow Body") { + ResetColor(cosmeticOptions.at("Equipment.BowTips")); + ResetColor(cosmeticOptions.at("Equipment.BowHandle")); + } else if (cosmeticOption.label == "Idle Primary") { + ResetColor(cosmeticOptions.at("Navi.IdleSecondary")); + } else if (cosmeticOption.label == "Enemy Primary") { + ResetColor(cosmeticOptions.at("Navi.EnemySecondary")); + } else if (cosmeticOption.label == "NPC Primary") { + ResetColor(cosmeticOptions.at("Navi.NPCSecondary")); + } else if (cosmeticOption.label == "Props Primary") { + ResetColor(cosmeticOptions.at("Navi.PropsSecondary")); + } else if (cosmeticOption.label == "Level 1 Secondary") { + ResetColor(cosmeticOptions.at("SpinAttack.Level1Primary")); + } else if (cosmeticOption.label == "Level 2 Secondary") { + ResetColor(cosmeticOptions.at("SpinAttack.Level2Primary")); + } else if (cosmeticOption.label == "Item Select Color") { + ResetColor(cosmeticOptions.at("Kaleido.ItemSelB")); + ResetColor(cosmeticOptions.at("Kaleido.ItemSelC")); + ResetColor(cosmeticOptions.at("Kaleido.ItemSelD")); + } else if (cosmeticOption.label == "Equip Select Color") { + ResetColor(cosmeticOptions.at("Kaleido.EquipSelB")); + ResetColor(cosmeticOptions.at("Kaleido.EquipSelC")); + ResetColor(cosmeticOptions.at("Kaleido.EquipSelD")); + } else if (cosmeticOption.label == "Map Dungeon Color") { + ResetColor(cosmeticOptions.at("Kaleido.MapSelDunB")); + ResetColor(cosmeticOptions.at("Kaleido.MapSelDunC")); + ResetColor(cosmeticOptions.at("Kaleido.MapSelDunD")); + } else if (cosmeticOption.label == "Quest Status Color") { + ResetColor(cosmeticOptions.at("Kaleido.QuestStatusB")); + ResetColor(cosmeticOptions.at("Kaleido.QuestStatusC")); + ResetColor(cosmeticOptions.at("Kaleido.QuestStatusD")); + } else if (cosmeticOption.label == "Map Color") { + ResetColor(cosmeticOptions.at("Kaleido.MapSelectB")); + ResetColor(cosmeticOptions.at("Kaleido.MapSelectC")); + ResetColor(cosmeticOptions.at("Kaleido.MapSelectD")); + } else if (cosmeticOption.label == "Save Color") { + ResetColor(cosmeticOptions.at("Kaleido.SaveB")); + ResetColor(cosmeticOptions.at("Kaleido.SaveC")); + ResetColor(cosmeticOptions.at("Kaleido.SaveD")); + } + ShipInit::Init(cosmeticOption.valuesCvar); +} + +inline CosmeticOption MakeCosmeticOption(const char* cvar, const char* valuesCvar, const char* rainbowCvar, + const char* lockedCvar, const char* changedCvar, const char* label, + CosmeticGroup group, Color_RGBA8 defaultColor, bool supportsAlpha, + bool supportsRainbow, bool advancedOption) { + return CosmeticOption{ cvar, + valuesCvar, + rainbowCvar, + lockedCvar, + changedCvar, + label, + group, + ImVec4(defaultColor.r / 255.0f, defaultColor.g / 255.0f, defaultColor.b / 255.0f, + defaultColor.a / 255.0f), + defaultColor, + supportsAlpha, + supportsRainbow, + advancedOption }; +} + typedef struct { const std::string Name; const std::string ToolTip; @@ -60,6 +171,11 @@ void CosmeticsEditor_RandomizeGroup(CosmeticGroup group); void CosmeticsEditor_ResetAll(); void CosmeticsEditor_ResetGroup(CosmeticGroup group); void ApplyOrResetCustomGfxPatches(bool manualChange = true); +void ScanCustomCosmetics(); +bool HasCustomCosmetics(); +void DrawCustomCosmetics(); +void ApplyCustomCosmetics(); +void UpdateCustomCosmeticsRainbow(int hue, float rainbowSpeed, int& index); class CosmeticsEditorWindow final : public Ship::GuiWindow { public: diff --git a/soh/soh/Enhancements/cosmetics/DynamicCosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/DynamicCosmeticsEditor.cpp new file mode 100644 index 0000000000..7df9aaefda --- /dev/null +++ b/soh/soh/Enhancements/cosmetics/DynamicCosmeticsEditor.cpp @@ -0,0 +1,430 @@ +#include "CosmeticsEditor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "soh/SohGui/UIWidgets.hpp" +#include "soh/SohGui/SohGui.hpp" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "macros.h" +#include "soh/cvar_prefixes.h" +} + +static constexpr const char* CUSTOM_COSMETIC_GROUP = "Custom"; +static constexpr const char* CUSTOM_CVAR_PREFIX = "gCosmetics.Custom."; + +struct CustomCosmeticBinding { + std::string materialPath; + size_t commandIndex = 0; + bool isPrimColor = true; + uint8_t defaultA = 255; + uint8_t primM = 0; + uint8_t primL = 0; +}; + +struct CustomCosmeticEntry { + CosmeticOption option; + std::string baseCvar; + std::string valuesCvar; + std::string rainbowCvar; + std::string lockedCvar; + std::string changedCvar; + std::string category; + std::vector bindings; +}; + +static std::vector customCosmeticEntries; + +static bool IsCustomArchive(const std::shared_ptr& archive) { + if (archive == nullptr) { + return false; + } + + const auto& archivePath = archive->GetPath(); + return archivePath.find("\\mods\\") != std::string::npos || archivePath.find("/mods/") != std::string::npos; +} + +static int GetCustomMaterialSortOrder(const std::string& materialPath) { + if (materialPath.starts_with("objects/object_link_child/") || + materialPath.starts_with("__OTR__objects/object_link_child/")) { + return 0; + } + if (materialPath.starts_with("objects/object_link_boy/") || + materialPath.starts_with("__OTR__objects/object_link_boy/")) { + return 1; + } + + return 2; +} + +static void SanitizeCustomKey(std::string& value) { + for (auto it = value.begin(); it != value.end();) { + if (!std::isalnum(static_cast(*it))) { + it = value.erase(it); + } else { + ++it; + } + } +} + +static bool TryLoadCustomDisplayListXml(Ship::ArchiveManager* archiveManager, Ship::ResourceManager* resourceManager, + const std::string& materialPath, tinyxml2::XMLDocument& document, + std::shared_ptr& material, tinyxml2::XMLElement*& root) { + auto file = archiveManager->LoadFile(materialPath); + if (file == nullptr || !file->IsLoaded || file->Buffer == nullptr) { + return false; + } + + document.Parse(file->Buffer->data(), file->Buffer->size()); + if (document.Error()) { + return false; + } + + root = document.FirstChildElement(); + if (root == nullptr || std::string(root->Name()) != "DisplayList") { + return false; + } + + material = std::dynamic_pointer_cast(resourceManager->LoadResource(materialPath)); + return material != nullptr; +} + +static size_t FindDisplayListInstructionIndex(const Fast::DisplayList& displayList, const Gfx& expected, + size_t searchStart) { + for (size_t i = searchStart; i < displayList.Instructions.size(); i++) { + const Gfx& current = displayList.Instructions[i]; + if (current.words.w0 == expected.words.w0 && current.words.w1 == expected.words.w1) { + return i; + } + } + + return SIZE_MAX; +} + +static Color_RGBA8 GetCustomCosmeticColor(const CustomCosmeticEntry& entry) { + if (CVarGetInteger(entry.option.changedCvar, 0)) { + return CVarGetColor(entry.option.valuesCvar, entry.option.defaultColor); + } + + return entry.option.defaultColor; +} + +void ApplyCustomCosmetics() { + auto resourceManager = Ship::Context::GetRawInstance()->GetResourceManager(); + auto archiveManager = resourceManager->GetArchiveManager(); + + for (auto& entry : customCosmeticEntries) { + Color_RGBA8 color = GetCustomCosmeticColor(entry); + + for (const auto& binding : entry.bindings) { + if (!IsCustomArchive(archiveManager->GetArchiveFromFile(binding.materialPath))) { + continue; + } + + auto material = + std::dynamic_pointer_cast(resourceManager->LoadResource(binding.materialPath)); + if (material == nullptr || binding.commandIndex >= material->Instructions.size()) { + continue; + } + + if (binding.isPrimColor) { + material->Instructions[binding.commandIndex] = + gsDPSetPrimColor(binding.primM, binding.primL, color.r, color.g, color.b, binding.defaultA); + } else { + material->Instructions[binding.commandIndex] = + gsDPSetEnvColor(color.r, color.g, color.b, binding.defaultA); + } + } + } +} + +static void SetCustomCosmeticColor(const CustomCosmeticEntry& entry, Color_RGBA8 color) { + CVarSetColor(entry.option.valuesCvar, color); + CVarSetInteger(entry.option.rainbowCvar, 0); + CVarSetInteger(entry.option.changedCvar, 1); + ShipInit::Init(entry.option.valuesCvar); + ShipInit::Init(entry.option.rainbowCvar); + ShipInit::Init(entry.option.changedCvar); + ApplyCustomCosmetics(); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} + +static void ResetCustomCosmeticColor(const CustomCosmeticEntry& entry) { + ResetColor(const_cast(entry.option)); + ApplyCustomCosmetics(); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); +} + +static void RandomizeCustomCosmeticColor(const CustomCosmeticEntry& entry) { + Color_RGBA8 color = { static_cast(rand() % 256), static_cast(rand() % 256), + static_cast(rand() % 256), 255 }; + SetCustomCosmeticColor(entry, color); +} + +static void DrawCustomCosmeticColorRow(const char* label, const char* cvar, Color_RGBA8 defaultColor, + const char* rainbowCvar, const char* lockedCvar, const char* changedCvar, + const std::function& onColorChanged, + const std::function& onRandomize, + const std::function& onRainbowToggle, + const std::function& onReset) { + if (UIWidgets::CVarColorPicker(label, cvar, defaultColor, false, 0, THEME_COLOR)) { + onColorChanged(); + } + + ImGui::SameLine((ImGui::CalcTextSize("Message Light Blue (None No Shadow)").x * 1.0f) + 60.0f); + if (UIWidgets::Button( + ("Random##" + std::string(label)).c_str(), + UIWidgets::ButtonOptions().Size(ImVec2(80, 31)).Padding(ImVec2(2.0f, 0.0f)).Color(THEME_COLOR))) { + onRandomize(); + } + + ImGui::SameLine(); + if (UIWidgets::CVarCheckbox(("Rainbow##" + std::string(label)).c_str(), rainbowCvar, + UIWidgets::CheckboxOptions().Color(THEME_COLOR))) { + onRainbowToggle(); + } + + ImGui::SameLine(); + UIWidgets::CVarCheckbox(("Locked##" + std::string(label)).c_str(), lockedCvar, + UIWidgets::CheckboxOptions().Color(THEME_COLOR)); + + if (CVarGetInteger(changedCvar, 0)) { + ImGui::SameLine(); + if (UIWidgets::Button(("Reset##" + std::string(label)).c_str(), + UIWidgets::ButtonOptions().Size(ImVec2(80, 31)).Padding(ImVec2(2.0f, 0.0f)))) { + onReset(); + } + } +} + +void ScanCustomCosmetics() { + customCosmeticEntries.clear(); + + auto resourceManager = Ship::Context::GetRawInstance()->GetResourceManager(); + auto archiveManager = resourceManager->GetArchiveManager(); + auto materialPaths = archiveManager->ListFiles("*"); + std::unordered_map entryIndicesByKey; + + for (const auto& materialPath : *materialPaths) { + if (!IsCustomArchive(archiveManager->GetArchiveFromFile(materialPath))) { + continue; + } + + tinyxml2::XMLDocument document; + std::shared_ptr material; + tinyxml2::XMLElement* root = nullptr; + if (!TryLoadCustomDisplayListXml(archiveManager.get(), resourceManager.get(), materialPath, document, material, + root)) { + continue; + } + + size_t searchStart = 0; + for (auto* child = root->FirstChildElement(); child != nullptr; child = child->NextSiblingElement()) { + std::string childName = child->Name(); + bool isPrimColor = childName == "SetPrimColor"; + if (!isPrimColor && childName != "SetEnvColor") { + continue; + } + + const char* cosmeticEntry = child->Attribute("CosmeticEntry"); + const char* cosmeticCategory = child->Attribute("CosmeticCategory"); + if (cosmeticEntry == nullptr || cosmeticEntry[0] == '\0') { + continue; + } + + std::string key = cosmeticEntry; + SanitizeCustomKey(key); + if (key.empty()) { + continue; + } + Gfx expectedInstruction; + if (isPrimColor) { + expectedInstruction = + gsDPSetPrimColor(child->IntAttribute("M"), child->IntAttribute("L"), child->IntAttribute("R"), + child->IntAttribute("G"), child->IntAttribute("B"), child->IntAttribute("A")); + } else { + expectedInstruction = gsDPSetEnvColor(child->IntAttribute("R"), child->IntAttribute("G"), + child->IntAttribute("B"), child->IntAttribute("A")); + } + + size_t commandIndex = FindDisplayListInstructionIndex(*material, expectedInstruction, searchStart); + if (commandIndex == SIZE_MAX) { + continue; + } + searchStart = commandIndex + 1; + + size_t entryIndex = 0; + if (auto it = entryIndicesByKey.find(key); it != entryIndicesByKey.end()) { + entryIndex = it->second; + } else { + entryIndex = customCosmeticEntries.size(); + entryIndicesByKey[key] = entryIndex; + + CustomCosmeticEntry entry; + entry.category = (cosmeticCategory != nullptr) ? cosmeticCategory : ""; + entry.baseCvar = std::string(CUSTOM_CVAR_PREFIX) + key; + entry.valuesCvar = entry.baseCvar + ".Value"; + entry.rainbowCvar = entry.baseCvar + ".Rainbow"; + entry.lockedCvar = entry.baseCvar + ".Locked"; + entry.changedCvar = entry.baseCvar + ".Changed"; + const Color_RGBA8 defaultColor = { static_cast(child->IntAttribute("R")), + static_cast(child->IntAttribute("G")), + static_cast(child->IntAttribute("B")), + static_cast(child->IntAttribute("A")) }; + entry.option = + MakeCosmeticOption(entry.baseCvar.c_str(), entry.valuesCvar.c_str(), entry.rainbowCvar.c_str(), + entry.lockedCvar.c_str(), entry.changedCvar.c_str(), cosmeticEntry, + COSMETICS_GROUP_MAX, defaultColor, false, true, false); + customCosmeticEntries.push_back(std::move(entry)); + } + + CustomCosmeticBinding binding; + binding.materialPath = materialPath; + binding.commandIndex = commandIndex; + binding.isPrimColor = isPrimColor; + binding.defaultA = static_cast(child->IntAttribute("A")); + binding.primM = static_cast(child->IntAttribute("M")); + binding.primL = static_cast(child->IntAttribute("L")); + customCosmeticEntries[entryIndex].bindings.push_back(std::move(binding)); + } + } + + std::stable_sort(customCosmeticEntries.begin(), customCosmeticEntries.end(), + [](const CustomCosmeticEntry& lhs, const CustomCosmeticEntry& rhs) { + int lhsOrder = 2; + int rhsOrder = 2; + + for (const auto& binding : lhs.bindings) { + lhsOrder = std::min(lhsOrder, GetCustomMaterialSortOrder(binding.materialPath)); + } + for (const auto& binding : rhs.bindings) { + rhsOrder = std::min(rhsOrder, GetCustomMaterialSortOrder(binding.materialPath)); + } + + if (lhsOrder != rhsOrder) { + return lhsOrder < rhsOrder; + } + + if (lhs.category.empty() != rhs.category.empty()) { + return !lhs.category.empty(); + } + + if (lhs.category != rhs.category) { + return lhs.category < rhs.category; + } + + return lhs.option.label < rhs.option.label; + }); + + ApplyCustomCosmetics(); +} + +static void DrawCustomCosmeticRow(const CustomCosmeticEntry& entry) { + const char* cvar = entry.option.cvar; + + DrawCustomCosmeticColorRow( + entry.option.label.c_str(), cvar, entry.option.defaultColor, entry.option.rainbowCvar, entry.option.lockedCvar, + entry.option.changedCvar, + [&entry]() { + CVarSetInteger(entry.option.changedCvar, 1); + ShipInit::Init(entry.option.changedCvar); + ApplyCustomCosmetics(); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + }, + [&entry]() { RandomizeCustomCosmeticColor(entry); }, + [&entry]() { + CVarSetInteger(entry.option.changedCvar, 1); + ShipInit::Init(entry.option.changedCvar); + ApplyCustomCosmetics(); + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + }, + [&entry]() { ResetCustomCosmeticColor(entry); }); +} + +static void DrawCustomCosmeticCategory(const char* label, const std::vector& entries) { + ImGui::Text("%s", label); + ImGui::SameLine((ImGui::CalcTextSize("Message Light Blue (None No Shadow)").x * 1.0f) + 60.0f); + if (UIWidgets::Button( + ("Random##" + std::string(label)).c_str(), + UIWidgets::ButtonOptions().Size(ImVec2(80, 31)).Padding(ImVec2(2.0f, 0.0f)).Color(THEME_COLOR))) { + for (const auto* entry : entries) { + RandomizeCustomCosmeticColor(*entry); + } + Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ApplyCustomCosmetics(); + } + ImGui::SameLine(); + if (UIWidgets::Button(("Reset##" + std::string(label)).c_str(), + UIWidgets::ButtonOptions().Size(ImVec2(80, 31)).Padding(ImVec2(2.0f, 0.0f)))) { + for (const auto* entry : entries) { + ResetCustomCosmeticColor(*entry); + } + ApplyCustomCosmetics(); + } + UIWidgets::Spacer(); + for (const auto* entry : entries) { + DrawCustomCosmeticRow(*entry); + } + UIWidgets::Separator(true, true, 2.0f, 2.0f); +} + +bool HasCustomCosmetics() { + return !customCosmeticEntries.empty(); +} + +void DrawCustomCosmetics() { + if (customCosmeticEntries.empty()) { + return; + } + + std::vector currentEntries; + std::string currentCategory; + + auto flushCategory = [&]() { + if (currentEntries.empty()) { + return; + } + + const char* label = currentCategory.empty() ? CUSTOM_COSMETIC_GROUP : currentCategory.c_str(); + DrawCustomCosmeticCategory(label, currentEntries); + currentEntries.clear(); + }; + + for (const auto& entry : customCosmeticEntries) { + if (entry.category != currentCategory) { + flushCategory(); + currentCategory = entry.category; + } + currentEntries.push_back(&entry); + } + + flushCategory(); +} + +void UpdateCustomCosmeticsRainbow(int hue, float rainbowSpeed, int& index) { + for (const auto& entry : customCosmeticEntries) { + if (CVarGetInteger(entry.option.rainbowCvar, 0)) { + double frequency = 2 * M_PI / (360 * rainbowSpeed); + Color_RGBA8 newColor; + newColor.r = static_cast(sin(frequency * (hue + index) + 0) * 127) + 128; + newColor.g = static_cast(sin(frequency * (hue + index) + (2 * M_PI / 3)) * 127) + 128; + newColor.b = static_cast(sin(frequency * (hue + index) + (4 * M_PI / 3)) * 127) + 128; + newColor.a = 255; + CVarSetColor(entry.option.valuesCvar, newColor); + } + if (!CVarGetInteger(CVAR_COSMETIC("RainbowSync"), 0)) { + index += static_cast(60 * rainbowSpeed); + } + } +}