diff --git a/port/port_imgui_menu.cpp b/port/port_imgui_menu.cpp index a83ccdc6d7..b71cc2f72a 100644 --- a/port/port_imgui_menu.cpp +++ b/port/port_imgui_menu.cpp @@ -1664,163 +1664,10 @@ static const char* const kRandoTrickPjs = "Portal Jump Storage - early Cloud Top static const char* const kRandoTrickTooltip = "Glitch-logic tier: progression may be placed behind these documented " "speedrun glitches. Requires Glitchless logic OFF."; -/* ---- Cosmetics (.logic !color settings) ---------------------------------- - * A RANDO_SETTING_COLOR setting carries option_count default color sets - * (RGB555 hex strings in opt_value[]). The override value consumed by - * ParseColorDirective is comma-separated RGB555 hex, one per set - * (e.g. "7C1F,03E0"). Per the `.logic` spec, defaults never set defines: - * the override only exists once the player actually edits a color, so an - * enabled-but-untouched setting still rolls vanilla. */ extern "C" void Rando_Cosmetic_Apply(void); /* rando_cosmetic.cpp — live palette re-apply */ extern "C" void Rando_Keymap_Apply(void); /* rando_keymap.c — rebind ground-item keys */ extern "C" void Rando_SetCosmetics(int tunic_color, int heart_color); /* rando.cpp — live cosmetic settings */ -typedef struct RandoColorUiState { - char define[48]; - bool enabled; /* checkbox; the engine override only exists once dirty */ - bool dirty; /* an edit was committed at least once this session */ - bool pending; /* floats edited; commit once the picker goes idle */ - float col[RANDO_LOGIC_MAX_COLOR_SETS][3]; -} RandoColorUiState; -static RandoColorUiState sRandoColorUi[32]; -static int sRandoColorUiCount = 0; - -static bool RandoUi_FindOverrideValue(const char* define, const char** out_value) { - const uint32_t n = RandoLogic_GetOverrideCount(); - for (uint32_t i = 0; i < n; ++i) { - const char* name = NULL; - const char* value = NULL; - if (RandoLogic_GetOverride(i, &name, &value) && name != NULL && std::strcmp(name, define) == 0) { - if (out_value != NULL) - *out_value = value; - return true; - } - } - return false; -} - -/* GBA RGB555 layout: R in the low 5 bits (matches ParseColorDirective's - * packing and the `0x..._0 & 0x1F` eventdefine extraction in .logic). */ -static void RandoUi_Rgb555ToFloat(unsigned v, float out[3]) { - out[0] = (float)(v & 0x1F) / 31.0f; - out[1] = (float)((v >> 5) & 0x1F) / 31.0f; - out[2] = (float)((v >> 10) & 0x1F) / 31.0f; -} - -static unsigned RandoUi_FloatToRgb555(const float in[3]) { - unsigned c[3]; - for (int i = 0; i < 3; ++i) { - float f = in[i]; - if (f < 0.0f) - f = 0.0f; - if (f > 1.0f) - f = 1.0f; - c[i] = (unsigned)(f * 31.0f + 0.5f); - } - return (c[2] << 10) | (c[1] << 5) | c[0]; -} - -/* Per-define UI cache. Needed because the engine never echoes overrides back - * into opt_value[] (those always hold the file defaults after a reparse). */ -static RandoColorUiState* RandoUi_ColorState(const RandoLogicSetting* s) { - for (int i = 0; i < sRandoColorUiCount; ++i) { - if (std::strcmp(sRandoColorUi[i].define, s->define) == 0) - return &sRandoColorUi[i]; - } - if (sRandoColorUiCount >= (int)(sizeof(sRandoColorUi) / sizeof(sRandoColorUi[0]))) { - return NULL; - } - RandoColorUiState* st = &sRandoColorUi[sRandoColorUiCount++]; - std::snprintf(st->define, sizeof(st->define), "%s", s->define); - for (int j = 0; j < RANDO_LOGIC_MAX_COLOR_SETS; ++j) { - unsigned v = 0x7FFF; /* spec: white when no default given */ - if (j < s->option_count) - v = (unsigned)std::strtoul(s->opt_value[j], NULL, 16); - RandoUi_Rgb555ToFloat(v, st->col[j]); - } - /* Pre-existing override (sidecar restore / earlier session): adopt it. */ - const char* ov = NULL; - if (RandoUi_FindOverrideValue(s->define, &ov) && ov != NULL && ov[0] != '\0') { - st->enabled = true; - st->dirty = true; - const char* p = ov; - int j = 0; - while (*p != '\0' && j < RANDO_LOGIC_MAX_COLOR_SETS) { - char* end = NULL; - unsigned v = (unsigned)std::strtoul(p, &end, 16); - if (end == p) - break; - RandoUi_Rgb555ToFloat(v, st->col[j++]); - p = end; - while (*p == ',' || *p == ' ') - ++p; - } - } - return st; -} - -/* Shared override-mutation tail: reparse the .logic, persist the sidecar, and - * - while a seed is live - rebind the location keymap + re-evaluate cosmetics - * so nothing silently desyncs. */ -static void RandoUi_ReparseAndRebind(void) { - RandoLogic_Reparse(); - Port_RandoFileMenu_PersistLogicOverrides(); - if (Rando_IsActive()) { - Rando_Keymap_Apply(); - Rando_Cosmetic_Apply(); - } -} - -static void RandoUi_CommitColorOverride(RandoColorUiState* st, int set_count) { - char value[48]; /* 8 sets x "XXXX," fits; engine caps stored values at 31 */ - size_t len = 0; - for (int j = 0; j < set_count && j < RANDO_LOGIC_MAX_COLOR_SETS; ++j) { - len += (size_t)std::snprintf(value + len, sizeof(value) - len, "%s%04X", j ? "," : "", - RandoUi_FloatToRgb555(st->col[j])); - if (len >= sizeof(value) - 1) - break; - } - if (len > 31) { - std::fprintf(stderr, "[RANDO] color override %s exceeds engine value cap (%u chars) - truncated\n", st->define, - (unsigned)len); - } - RandoLogic_SetOverride(st->define, value); - st->dirty = true; - /* A reparse clears the bound ground-item/scripted location keys that only - * seed activation rebinds, so RandoUi_ReparseAndRebind re-binds the keymap - * + re-evaluates cosmetics while a seed is active - making the edit live. */ - RandoUi_ReparseAndRebind(); - std::fprintf(stderr, "[RANDO] color override %s = %s\n", st->define, value); -} - -/* The engine only exposes SetOverride + ClearOverrides-all; an empty-value - * override is NOT vanilla (ParseColorDirective would still define the bare - * flag and flip !ifdef blocks). So clearing one define = snapshot the other - * overrides, ClearOverrides, re-set the survivors, reparse — the selective - * version of rando_file_menu.c's ClearOverrides+Reparse reset. */ -static void RandoUi_RemoveOverride(const char* define) { - static char names[RANDO_LOGIC_MAX_SETTINGS][48]; - static char values[RANDO_LOGIC_MAX_SETTINGS][32]; /* engine value cap */ - const uint32_t n = RandoLogic_GetOverrideCount(); - uint32_t kept = 0; - for (uint32_t i = 0; i < n && kept < RANDO_LOGIC_MAX_SETTINGS; ++i) { - const char* name = NULL; - const char* value = NULL; - if (!RandoLogic_GetOverride(i, &name, &value) || name == NULL) - continue; - if (std::strcmp(name, define) == 0) - continue; - std::snprintf(names[kept], sizeof(names[0]), "%s", name); - std::snprintf(values[kept], sizeof(values[0]), "%s", value ? value : ""); - kept++; - } - RandoLogic_ClearOverrides(); - for (uint32_t i = 0; i < kept; ++i) - RandoLogic_SetOverride(names[i], values[i]); - RandoUi_ReparseAndRebind(); - std::fprintf(stderr, "[RANDO] color override %s cleared (vanilla)\n", define); -} - static void DrawRandoCosmeticsSection(void) { ImGui::Spacing(); if (!ImGui::CollapsingHeader("Cosmetics", ImGuiTreeNodeFlags_DefaultOpen)) @@ -1861,53 +1708,6 @@ static void DrawRandoCosmeticsSection(void) { } } -/* ---- Logic settings browser (shared by the F8 tab + file-select modal) -- - * The `.logic` file declares per-setting window tab, group, and tooltip - * text; the browser turns the former flat list into OoTR-style progressive - * disclosure: collapsing tab sections, group separators, a search filter, - * per-setting upstream tooltips, modified-from-default markers, and - * right-click reset. Edits route through the same override+reparse path the - * engine already uses; while a seed is active the location keymap and - * cosmetics are rebound so nothing silently desyncs (settings affect the - * NEXT roll, the active item table is untouched). */ - -static void RandoUi_ApplyOverride(const char* define, const char* value) { - RandoLogic_SetOverride(define, value); - RandoUi_ReparseAndRebind(); -} - -static bool RandoUi_SettingModified(const RandoLogicSetting* s) { - switch (s->type) { - case RANDO_SETTING_FLAG: - return s->flag_on != s->default_flag; - case RANDO_SETTING_DROPDOWN: - return s->option_index != s->default_option; - case RANDO_SETTING_NUMBER: - return s->number != s->default_number; - default: - return false; - } -} - -static void RandoUi_SettingDefaultValue(const RandoLogicSetting* s, char* out, size_t out_len) { - switch (s->type) { - case RANDO_SETTING_FLAG: - std::snprintf(out, out_len, "%s", s->default_flag ? "true" : "false"); - break; - case RANDO_SETTING_DROPDOWN: - std::snprintf( - out, out_len, "%s", - (s->default_option >= 0 && s->default_option < s->option_count) ? s->opt_value[s->default_option] : ""); - break; - case RANDO_SETTING_NUMBER: - std::snprintf(out, out_len, "%d", s->default_number); - break; - default: - out[0] = '\0'; - break; - } -} - static void RandoUi_HelpTooltip(const char* text) { ImGui::SameLine(); ImGui::TextDisabled("(?)"); @@ -1920,295 +1720,6 @@ static void RandoUi_HelpTooltip(const char* text) { } } -static int RandoUi_ModifiedSettingCount(void) { - int n = 0; - const uint32_t count = RandoLogic_GetSettingCount(); - for (uint32_t i = 0; i < count; ++i) { - const RandoLogicSetting* s = RandoLogic_GetSetting(i); - if (s != NULL && s->type != RANDO_SETTING_COLOR && RandoUi_SettingModified(s)) - ++n; - } - return n; -} - -/* Reset every non-color setting to its file default. Color overrides are - * preserved (they live in the Cosmetics section and are orthogonal). */ -static void RandoUi_ResetSettingsToDefaults(void) { - const uint32_t count = RandoLogic_GetSettingCount(); - for (uint32_t i = 0; i < count; ++i) { - const RandoLogicSetting* s = RandoLogic_GetSetting(i); - if (s == NULL || s->type == RANDO_SETTING_COLOR || !RandoUi_SettingModified(s)) - continue; - char value[40]; - RandoUi_SettingDefaultValue(s, value, sizeof(value)); - RandoLogic_SetOverride(s->define, value); - } - RandoUi_ReparseAndRebind(); -} - -/* ---- Presets (OoTR convention: load changes everything except cosmetics). - * Each preset starts from file defaults, then applies its pairs. */ -typedef struct RandoUiPresetPair { - const char* define; - const char* value; -} RandoUiPresetPair; -typedef struct RandoUiPreset { - const char* name; - const char* desc; - const RandoUiPresetPair* pairs; - int count; -} RandoUiPreset; - -static const RandoUiPresetPair kRandoPresetStandard[] = { - { "RUPEEMANIA", "true" }, { "SPECIALPOTS", "true" }, - { "DIGGING", "true" }, { "UNDERWATER", "true" }, - { "GOLDEN_ENEMY", "true" }, { "OPEN_TINGLE", "true" }, - { "OPEN_LIBRARY", "true" }, { "CUCCO_SETTING", "CUCCO_5" }, - { "GORON_SETTING", "GORON_5" }, { "BIGGORON_SETTING", "BIGGORON_NORMAL" }, -}; -static const RandoUiPresetPair kRandoPresetKeysanity[] = { - { "RUPEEMANIA", "true" }, - { "SPECIALPOTS", "true" }, - { "DIGGING", "true" }, - { "UNDERWATER", "true" }, - { "GOLDEN_ENEMY", "true" }, - { "OPEN_TINGLE", "true" }, - { "OPEN_LIBRARY", "true" }, - { "CUCCO_SETTING", "CUCCO_5" }, - { "GORON_SETTING", "GORON_5" }, - { "BIGGORON_SETTING", "BIGGORON_NORMAL" }, - { "SMALL_KEYS_SETTING", "SMALL_KEYSANITY" }, - { "BIG_KEYS_SETTING", "BIG_KEYSANITY" }, - { "MAP_SETTING", "MAP_KEYSANITY" }, - { "COMPASS_SETTING", "COMPASS_KEYSANITY" }, -}; -static const RandoUiPresetPair kRandoPresetOpen[] = { - { "OPENWORLD", "OPENWORLD_ON" }, { "OPEN_WIND_TRIBE", "true" }, { "OPEN_TINGLE", "true" }, - { "OPEN_LIBRARY", "true" }, { "CRENEL_CREST", "true" }, { "FALLS_CREST", "true" }, - { "CLOUD_CREST", "true" }, { "SWAMP_CREST", "true" }, { "SHF_CREST", "true" }, - { "MINISH_CREST", "true" }, -}; - -static const RandoUiPreset kRandoPresets[] = { - { "File defaults (Beginner)", - "Every setting at the .logic file's defaults - chests and hearts " - "shuffled, progression close to vanilla. Best first seed.", - NULL, 0 }, - { "Standard shuffle", - "Adds the common location shuffles on top of the defaults: rupees, " - "special pots, dig spots, underwater spots, golden enemies, all " - "cucco rounds, Goron merchant sets, and Biggoron. Library and " - "Tingle siblings start open.", - kRandoPresetStandard, (int)(sizeof(kRandoPresetStandard) / sizeof(kRandoPresetStandard[0])) }, - { "Keysanity", - "Standard shuffle plus dungeon small keys, big keys, maps, and " - "compasses shuffled anywhere in the world.", - kRandoPresetKeysanity, (int)(sizeof(kRandoPresetKeysanity) / sizeof(kRandoPresetKeysanity[0])) }, - { "Open world (fast)", - "World obstacles start open, all wind crests are active, and the " - "Wind Tribe tower, library, and Tingle siblings are unlocked from " - "the start. Shorter seeds with less walking.", - kRandoPresetOpen, (int)(sizeof(kRandoPresetOpen) / sizeof(kRandoPresetOpen[0])) }, -}; - -static void RandoUi_ApplyPreset(int preset_index) { - if (preset_index < 0 || preset_index >= (int)(sizeof(kRandoPresets) / sizeof(kRandoPresets[0]))) - return; - const RandoUiPreset* p = &kRandoPresets[preset_index]; - /* Start from file defaults so presets are absolute, not additive. */ - const uint32_t count = RandoLogic_GetSettingCount(); - for (uint32_t i = 0; i < count; ++i) { - const RandoLogicSetting* s = RandoLogic_GetSetting(i); - if (s == NULL || s->type == RANDO_SETTING_COLOR || !RandoUi_SettingModified(s)) - continue; - char value[40]; - RandoUi_SettingDefaultValue(s, value, sizeof(value)); - RandoLogic_SetOverride(s->define, value); - } - for (int i = 0; i < p->count; ++i) - RandoLogic_SetOverride(p->pairs[i].define, p->pairs[i].value); - RandoUi_ReparseAndRebind(); - std::fprintf(stderr, "[RANDO] preset applied: %s\n", p->name); -} - -static void DrawRandoPresetsRow(void) { - static int sPresetIdx = 0; - const int preset_count = (int)(sizeof(kRandoPresets) / sizeof(kRandoPresets[0])); - ImGui::SetNextItemWidth(220); - if (ImGui::BeginCombo("##rando_preset", kRandoPresets[sPresetIdx].name)) { - for (int i = 0; i < preset_count; ++i) { - if (ImGui::Selectable(kRandoPresets[i].name, i == sPresetIdx)) - sPresetIdx = i; - if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { - ImGui::BeginTooltip(); - ImGui::PushTextWrapPos(360.0f); - ImGui::TextUnformatted(kRandoPresets[i].desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - if (ImGui::Button("Load preset")) - RandoUi_ApplyPreset(sPresetIdx); - RandoUi_HelpTooltip(kRandoPresets[sPresetIdx].desc); -} - -static void DrawRandoSettingRow(const RandoLogicSetting* s, int idx) { - ImGui::PushID(idx); - const bool modified = RandoUi_SettingModified(s); - if (modified) { - /* Modified-from-default marker: color cue plus a non-color glyph so - * the state never relies on color alone. */ - ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), "*"); - ImGui::SameLine(0.0f, 4.0f); - } - switch (s->type) { - case RANDO_SETTING_FLAG: { - bool v = s->flag_on; - if (ImGui::Checkbox(s->label, &v)) - RandoUi_ApplyOverride(s->define, v ? "true" : "false"); - break; - } - case RANDO_SETTING_DROPDOWN: { - const int oi = s->option_index; - const char* preview = (oi >= 0 && oi < s->option_count) ? s->opt_label[oi] : "?"; - ImGui::SetNextItemWidth(200); - if (ImGui::BeginCombo(s->label, preview)) { - for (int o = 0; o < s->option_count; ++o) { - const bool sel = (o == oi); - if (ImGui::Selectable(s->opt_label[o], sel)) - RandoUi_ApplyOverride(s->define, s->opt_value[o]); - if (sel) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - break; - } - case RANDO_SETTING_NUMBER: { - /* Commit on release - every commit reparses the whole .logic file, - * far too heavy per drag pixel. */ - static int sNumEditIdx = -1; - static int sNumEditVal = 0; - int v = (sNumEditIdx == idx) ? sNumEditVal : s->number; - ImGui::SetNextItemWidth(200); - if (ImGui::SliderInt(s->label, &v, s->num_min, s->num_max)) { - sNumEditIdx = idx; - sNumEditVal = v; - } - if (ImGui::IsItemDeactivatedAfterEdit() && sNumEditIdx == idx) { - char text[32]; - std::snprintf(text, sizeof(text), "%d", sNumEditVal); - RandoUi_ApplyOverride(s->define, text); - sNumEditIdx = -1; - } - break; - } - default: - break; - } - if (ImGui::BeginPopupContextItem("##setting_ctx")) { - ImGui::TextDisabled("%s", s->define); - if (ImGui::MenuItem("Reset to default", NULL, false, modified)) { - char value[40]; - RandoUi_SettingDefaultValue(s, value, sizeof(value)); - RandoUi_ApplyOverride(s->define, value); - } - ImGui::EndPopup(); - } - if (s->tooltip[0]) - RandoUi_HelpTooltip(s->tooltip); - ImGui::PopID(); -} - -static void DrawRandoLogicSettingsBrowser(float height) { - static ImGuiTextFilter sFilter; - const uint32_t count = RandoLogic_GetSettingCount(); - - sFilter.Draw("##rando_settings_filter", 200); - ImGui::SameLine(); - ImGui::TextDisabled("Search"); - const int modified = RandoUi_ModifiedSettingCount(); - if (modified > 0) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.95f, 0.75f, 0.25f, 1.0f), "* %d changed", modified); - ImGui::SameLine(); - if (ImGui::SmallButton("Reset all")) - ImGui::OpenPopup("##rando_reset_all"); - if (ImGui::BeginPopup("##rando_reset_all")) { - ImGui::TextUnformatted("Reset every setting to the file defaults?"); - if (ImGui::Button("Reset")) { - RandoUi_ResetSettingsToDefaults(); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Keep")) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - } - - ImGui::BeginChild("##rando_logic_settings", ImVec2(0, height), ImGuiChildFlags_Borders, 0); - const bool filtering = sFilter.IsActive(); - char cur_tab[24] = ""; - char cur_group[32] = ""; - bool tab_open = true; - for (uint32_t i = 0; i < count; ++i) { - const RandoLogicSetting* s = RandoLogic_GetSetting(i); - if (s == NULL || s->type == RANDO_SETTING_COLOR) - continue; - if (filtering) { - if (!sFilter.PassFilter(s->label) && !sFilter.PassFilter(s->define) && !sFilter.PassFilter(s->group) && - !sFilter.PassFilter(s->tab)) { - continue; - } - /* Flat results with tab > group breadcrumbs between sections. */ - if (std::strcmp(cur_tab, s->tab) != 0 || std::strcmp(cur_group, s->group) != 0) { - std::snprintf(cur_tab, sizeof(cur_tab), "%s", s->tab); - std::snprintf(cur_group, sizeof(cur_group), "%s", s->group); - char crumb[64]; - std::snprintf(crumb, sizeof(crumb), "%s > %s", s->tab, s->group); - ImGui::SeparatorText(crumb); - } - } else { - if (std::strcmp(cur_tab, s->tab) != 0) { - std::snprintf(cur_tab, sizeof(cur_tab), "%s", s->tab); - cur_group[0] = '\0'; - /* Per-section changed badge keeps edits findable when the - * section is collapsed. */ - int tab_changed = 0; - for (uint32_t j = i; j < count; ++j) { - const RandoLogicSetting* t = RandoLogic_GetSetting(j); - if (t == NULL) - continue; - if (std::strcmp(t->tab, s->tab) != 0) - break; /* tabs are contiguous in file order */ - if (t->type != RANDO_SETTING_COLOR && RandoUi_SettingModified(t)) - ++tab_changed; - } - char header[64]; - if (tab_changed > 0) { - std::snprintf(header, sizeof(header), "%s (* %d changed)###tab_%s", s->tab, tab_changed, s->tab); - } else { - std::snprintf(header, sizeof(header), "%s###tab_%s", s->tab, s->tab); - } - tab_open = ImGui::CollapsingHeader( - header, (std::strcmp(s->tab, "Main Settings") == 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0); - } - if (!tab_open) - continue; - if (std::strcmp(cur_group, s->group) != 0) { - std::snprintf(cur_group, sizeof(cur_group), "%s", s->group); - ImGui::SeparatorText(s->group); - } - } - DrawRandoSettingRow(s, (int)i); - } - ImGui::EndChild(); -} - /* ---- Built-in logic-aware Tracker overlay ------------------------------ * Reads the player's live inventory and progress flags, runs the logic * propagation solver in the background, and displays owned items/elements, @@ -2320,41 +1831,6 @@ static bool RandoUi_CheckItemOwned(const char* name) { return false; } -static bool RandoUi_LocationChecked(uint32_t loc_idx) { - uint32_t key = RandoLogic_GetLocationKeyAt(loc_idx); - if (key == UINT32_MAX) - return false; - - if (key & 0x80000000u) { - uint32_t group = (key >> 16) & 0x7FFF; - uint32_t subkey = key & 0xFFFF; - if (group == RANDO_SCRIPTED_KEY_SPECIAL) { - switch (subkey) { - case RANDO_SPECIAL_KEY_BELL_HP: - return CheckLocalFlagByBank(GetFlagBankOffset(2), 0xd0); /* Hyrule Town local flag 0xd0 */ - case RANDO_SPECIAL_KEY_TINGLE_TROPHY: - return GetInventoryValue(ITEM_QST_TINGLE_TROPHY) != 0; - case RANDO_SPECIAL_KEY_FORTRESS_PRIZE: - return GetInventoryValue(ITEM_OCARINA) != 0; - } - } - return false; - } - - uint32_t area = (key >> 16) & 0xFF; - uint32_t room = (key >> 8) & 0xFF; - uint32_t flag_or_chest = key & 0xFF; - - unsigned flag = Rando_GetChestLocalFlag(area, room, flag_or_chest); - if (flag != 0xFF) { - unsigned offset = GetFlagBankOffset(area); - return CheckLocalFlagByBank(offset, flag) != 0; - } else { - unsigned offset = GetFlagBankOffset(area); - return CheckLocalFlagByBank(offset, flag_or_chest) != 0; - } -} - static bool sShowRandoTracker = false; /* One tracker grid/element cell: bracketed label, accent-colored when owned, @@ -2370,20 +1846,6 @@ static void DrawRandoTrackerOverlay(void) { if (!sShowRandoTracker) return; - static bool sReached[RANDO_LOGIC_MAX_LOCATIONS] = {}; - static bool sChecked[RANDO_LOGIC_MAX_LOCATIONS] = {}; - static int sFrameThrottle = 15; - const uint32_t count = RandoLogic_GetLocationCountRaw(); - - if (++sFrameThrottle >= 15) { - sFrameThrottle = 0; - const uint16_t* active_table = Rando_GetRandomizedItemTable(); - RandoLogic_EvaluateReachability(active_table, RandoUi_CheckItemOwned, sReached, count); - for (uint32_t i = 0; i < count; ++i) { - sChecked[i] = RandoUi_LocationChecked(i); - } - } - ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); if (ImGui::Begin("Randomizer HUD Tracker", &sShowRandoTracker, ImGuiWindowFlags_NoCollapse)) { if (ImGui::BeginTabBar("##tracker_tabs")) { @@ -2568,82 +2030,6 @@ static void DrawRandoTrackerOverlay(void) { ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Locations")) { - static ImGuiTextFilter sLocFilter; - sLocFilter.Draw("##loc_filter", 180); - ImGui::SameLine(); - ImGui::TextDisabled("Filter by area/check name"); - - ImGui::BeginChild("##tracker_loc_list", ImVec2(0, 0), ImGuiChildFlags_Borders, 0); - char cur_area[48] = ""; - bool area_open = false; - - for (uint32_t i = 0; i < count; ++i) { - RandoLogicLocationType t = RandoLogic_GetLocationType(i); - if (t == RANDO_LOGIC_LOCATION_HELPER) - continue; - - const char* name = RandoLogic_GetLocationName(i); - if (name == nullptr || name[0] == '\0') - continue; - - bool checked = sChecked[i]; - if (checked) - continue; - - bool reached = sReached[i]; - if (!reached) - continue; - - if (sLocFilter.IsActive() && !sLocFilter.PassFilter(name)) - continue; - - char area_name[48] = "Overworld"; - const char* under = std::strchr(name, '_'); - if (under != nullptr && (size_t)(under - name) < sizeof(area_name)) { - std::memcpy(area_name, name, under - name); - area_name[under - name] = '\0'; - } - - if (std::strcmp(cur_area, area_name) != 0) { - std::snprintf(cur_area, sizeof(cur_area), "%s", area_name); - int avail = 0; - for (uint32_t j = i; j < count; ++j) { - const char* n = RandoLogic_GetLocationName(j); - if (n == nullptr || RandoLogic_GetLocationType(j) == RANDO_LOGIC_LOCATION_HELPER) - continue; - if (sChecked[j] || !sReached[j]) - continue; - if (sLocFilter.IsActive() && !sLocFilter.PassFilter(n)) - continue; - if (std::strncmp(n, cur_area, std::strlen(cur_area)) == 0 && - n[std::strlen(cur_area)] == '_') { - ++avail; - } - } - char header[64]; - std::snprintf(header, sizeof(header), "%s (%d available)###area_%s", cur_area, avail, cur_area); - area_open = ImGui::CollapsingHeader(header, ImGuiTreeNodeFlags_DefaultOpen); - } - - if (area_open) { - const char* label = name; - if (std::strncmp(label, cur_area, std::strlen(cur_area)) == 0 && - label[std::strlen(cur_area)] == '_') { - label += std::strlen(cur_area) + 1; - } - ImGui::Bullet(); - ImGui::SameLine(); - ImGui::TextUnformatted(label); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Logical check: %s", name); - } - } - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); } } @@ -4010,7 +3396,8 @@ static void DrawRandoFileMenuModal(void) { if (ImGui::InputText("Seed (empty = random)", Port_RandoFileMenu_SeedBuffer(), RANDO_FILE_MENU_SEED_MAX + 1, ImGuiInputTextFlags_EnterReturnsTrue)) { Port_RandoFileMenu_SeedEdited(); - Port_RandoFileMenu_CommitAndStart(); + if (forceOpen) /* commit only when the new-file flow armed a slot */ + Port_RandoFileMenu_CommitAndStart(); } if (ImGui::IsItemEdited()) Port_RandoFileMenu_SeedEdited(); diff --git a/port/port_repro_rando.c b/port/port_repro_rando.c index 4c410947c2..4fcc97f863 100644 --- a/port/port_repro_rando.c +++ b/port/port_repro_rando.c @@ -13,6 +13,7 @@ #include "port_debug_actions.h" #include "room.h" #include "rando/rando_save.h" +#include "rando/rando_keymap.h" #include "rando/rando_runtime.h" #include "save.h" #include "flags.h" @@ -220,6 +221,77 @@ static int run_real_logic_chest_probe(void) { return 1; } +/* audit C6: directly verify the per-seed location-collected set — the anti-dupe / + * anti-missable signal that drives dojo and grip-ring-scrub gates. Prior coverage + * only inferred it from a sidecar file-size check; this exercises the real + * mark-on-grant, per-location isolation, reset, and save/load round-trip. */ +static int run_collected_persistence(void) { + RandomizerSettings s = Rando_DefaultSettings(); + if (!GenerateSeed(0xC061u, s) || !Rando_IsActive()) { + fprintf(stderr, "[rando-repro] FAIL: collected-set generation failed\n"); + return 0; + } + + uint32_t collected_key = Rando_BuildScriptedKey(RANDO_SCRIPTED_KEY_SCRUB, RANDO_SCRUB_KEY_GRIP, 0, 0); + uint32_t witness_key = 0x002211E0u; /* a real chest location, never granted here */ + + /* (1) fresh seed => nothing collected */ + if (Rando_IsCollectedByKey(collected_key)) { + fprintf(stderr, "[rando-repro] FAIL: scrub key 0x%08X collected on a fresh seed\n", collected_key); + return 0; + } + if (Rando_IsCollectedByKey(witness_key)) { + fprintf(stderr, "[rando-repro] FAIL: witness chest 0x%08X collected on a fresh seed\n", witness_key); + return 0; + } + + /* (2) Rando_OverrideLocationKey marks the location collected as a side effect */ + unsigned char type = 0, sub = 0; + if (!Rando_OverrideLocationKey(collected_key, &type, &sub)) { + fprintf(stderr, "[rando-repro] FAIL: override did not fire for scrub key 0x%08X (location not in pool?)\n", + collected_key); + return 0; + } + if (!Rando_IsCollectedByKey(collected_key)) { + fprintf(stderr, "[rando-repro] FAIL: scrub key 0x%08X not marked collected after award\n", collected_key); + return 0; + } + + /* (3) marking is per-location, not global — an ungranted key stays uncollected */ + if (Rando_IsCollectedByKey(witness_key)) { + fprintf(stderr, "[rando-repro] FAIL: witness chest 0x%08X wrongly marked after scrub award\n", witness_key); + return 0; + } + + /* (4) persistence round-trip: save -> reset (clears) -> load (restores) */ + if (!Port_RandoSave_SaveActiveSlot(0)) { + fprintf(stderr, "[rando-repro] FAIL: collected-set sidecar save failed\n"); + return 0; + } + Rando_Reset(); + if (Rando_IsCollectedByKey(collected_key)) { + fprintf(stderr, "[rando-repro] FAIL: scrub key 0x%08X still collected after Rando_Reset\n", collected_key); + return 0; + } + if (!Port_RandoSave_LoadSlot(0) || !Rando_IsActive()) { + fprintf(stderr, "[rando-repro] FAIL: collected-set sidecar reload failed\n"); + return 0; + } + if (!Rando_IsCollectedByKey(collected_key)) { + fprintf(stderr, "[rando-repro] FAIL: scrub key 0x%08X not restored as collected after reload\n", collected_key); + return 0; + } + if (Rando_IsCollectedByKey(witness_key)) { + fprintf(stderr, "[rando-repro] FAIL: witness chest 0x%08X wrongly collected after reload\n", witness_key); + return 0; + } + + fprintf(stderr, "[rando-repro] collected-persistence OK: scrub key 0x%08X marks/resets/round-trips, " + "witness chest 0x%08X stays uncollected\n", + collected_key, witness_key); + return 1; +} + extern SDL_Window* Port_PPU_ActiveWindow(void); static void commit_random_seed_from_menu(void) { @@ -374,6 +446,7 @@ void Port_ReproRando_Tick(unsigned int frame) { if (!run_logic_key_path()) { sDone = 1; exit(1); } if (!run_world_open_test()) { sDone = 1; exit(1); } if (!run_real_logic_chest_probe()) { sDone = 1; exit(1); } + if (!run_collected_persistence()) { sDone = 1; exit(1); } } if (frame > 200) { diff --git a/port/port_tts.cpp b/port/port_tts.cpp index 751dacfd38..9b00590ff0 100644 --- a/port/port_tts.cpp +++ b/port/port_tts.cpp @@ -296,6 +296,20 @@ struct State { #else std::atomic current_proc{ nullptr }; #endif + + /* exit() paths (e.g. the repro harnesses) skip Port_TTS_Shutdown; a + * joinable worker at static destruction is std::terminate (SIGABRT). + * Idempotent: after a normal Shutdown the thread is already joined. */ + ~State() { + quitting.store(true); + { + std::lock_guard lk(queue_mu); + queue.clear(); + } + queue_cv.notify_all(); + if (worker.joinable()) + worker.join(); + } }; State g_state; diff --git a/port/rando/rando.cpp b/port/rando/rando.cpp index 793c35f0b1..e55fb09133 100644 --- a/port/rando/rando.cpp +++ b/port/rando/rando.cpp @@ -1985,6 +1985,10 @@ static bool sActive = false; static bool sInitialized = false; static char sSpoiler[8192]; static uint64_t sAutoSeedCounter = 0x9e3779b97f4a7c15ull; +/* Per-seed location-collected bitset (audit C6). Indexed by kLocations[] + * position; cleared on activate/reset, set at each location-keyed award, + * persisted via the save sidecar. */ +static uint8_t sCollected[RANDO_COLLECTED_BYTES]; static uint64_t SplitMix64_Next(SplitMix64* rng) { uint64_t z = (rng->state += 0x9e3779b97f4a7c15ull); @@ -2239,6 +2243,14 @@ static void EvaluateHelpers(const RandomizerSettings* settings, const bool* item return false; if (door_idx == 3) return items[ITEM_FLIPPERS] || og; + /* Door 6 is the DHC castle-garden entrance: it only exists once + * the castle transforms (four elements placed). Model it on the + * elements so entrance shuffle can't rate a dungeon behind it + * reachable from the start (audit R5). Door 7 (cellar) stays + * open. */ + if (door_idx == 6) + return items[ITEM_EARTH_ELEMENT] && items[ITEM_FIRE_ELEMENT] && items[ITEM_WATER_ELEMENT] && + items[ITEM_WIND_ELEMENT]; return true; }; @@ -2295,10 +2307,32 @@ static bool IsObscureLocation(const RandoLocationDef* loc) { return true; return false; } +/* Interim logic-modeling pins (audit C3/C5): these scripted locations are + * reachable in the solver's model but gated in-engine by state the solver + * does not yet track, so progression placed there can verify beatable yet + * deadlock. Kept vanilla (out of the pool) until the gates are modeled: + * - Goron Merchant tiers 2-5 (a>=1): each unlocks one tier per room load + * AND only after LV1..LV4_CLEAR (goronMerchantShopManager.c). + * - Cucco rounds 1-9 (a<=8): the new-file baseline pins the game at round + * 10, so only CUCCO(9) ("Level 10 Reward") ever keys (cuccoMinigame.c). */ +static bool RandoInterimGated(const RandoLocationDef* loc) { + if ((loc->key & 0x80000000u) == 0) + return false; + uint32_t group = (loc->key >> 24) & 0x7f; + uint32_t a = (loc->key >> 16) & 0xff; + if (group == RANDO_SCRIPTED_KEY_GORON_MERCHANT && a >= 1) + return true; + if (group == RANDO_SCRIPTED_KEY_CUCCO && a <= 8) + return true; + return false; +} static bool LocationEnabled(const RandomizerSettings* settings, const RandoLocationDef* loc) { if (!settings->shuffle_dojos && loc->category == RANDO_LOC_CATEGORY_DOJO) { return false; } + if (RandoInterimGated(loc)) { + return false; + } if (!settings->obscure_locations && IsObscureLocation(loc)) { return false; } @@ -2931,6 +2965,7 @@ static RandoStatus ActivateSeed(uint64_t seed, const RandomizerSettings* setting sSettings = *settings; sSeed = seed; sActive = true; + memset(sCollected, 0, sizeof(sCollected)); BuildCompatibilityRemap(); BuildSpoiler(seed, settings); fprintf(stderr, "[RANDO] seed %llu generated (native logic, %s pool, %zu locations)\n", (unsigned long long)seed, @@ -3040,6 +3075,7 @@ extern "C" void Rando_Reset(void) { extern void Rando_Music_ClearAssignments(void); Rando_Entrance_ClearAssignments(); Rando_Music_ClearAssignments(); + memset(sCollected, 0, sizeof(sCollected)); sLocationAwardPending = false; sSpoiler[0] = '\0'; fprintf(stderr, "[RANDO] reset to vanilla\n"); @@ -3135,12 +3171,50 @@ extern "C" bool Rando_OverrideLocationKey(uint32_t location_key, uint8_t* type, sLocationAwardPending = true; sLocationAwardType = (uint8_t)item; sLocationAwardSubtype = item_subtype; + sCollected[i >> 3] |= (uint8_t)(1u << (i & 7)); return true; } } return false; } +/* A location-keyed award arms the one-shot latch so the generic junk + * bijection (Rando_OverrideItem) does not re-randomize it. Give paths that + * do NOT route through Rando_OverrideItem (the silent GiveItem branch in + * itemOnGround.c) must disarm it, or a later incidental item with the same + * {type,subtype} would wrongly skip its remap once (audit R4). */ +extern "C" void Rando_ClearAwardLatch(void) { + sLocationAwardPending = false; +} + +extern "C" bool Rando_IsCollectedByKey(uint32_t location_key) { + EnsureInitialized(); + if (!sActive) + return false; + for (size_t i = 0; i < RANDO_LOCATION_COUNT; ++i) { + if (kLocations[i].key == location_key) + return (sCollected[i >> 3] & (1u << (i & 7))) != 0; + } + return false; +} + +extern "C" void Rando_GetCollectedSet(uint8_t* out, size_t out_len) { + if (out == NULL) + return; + size_t n = out_len < sizeof(sCollected) ? out_len : sizeof(sCollected); + memcpy(out, sCollected, n); + if (out_len > n) + memset(out + n, 0, out_len - n); +} + +extern "C" void Rando_SetCollectedSet(const uint8_t* in, size_t in_len) { + memset(sCollected, 0, sizeof(sCollected)); + if (in == NULL) + return; + size_t n = in_len < sizeof(sCollected) ? in_len : sizeof(sCollected); + memcpy(sCollected, in, n); +} + extern "C" bool Rando_ActivateTable(uint64_t seed, RandomizerSettings settings, const uint16_t* table, const uint8_t* subtype_table, size_t count) { EnsureInitialized(); diff --git a/port/rando/rando.h b/port/rando/rando.h index 5a5710455a..a152e16dd6 100644 --- a/port/rando/rando.h +++ b/port/rando/rando.h @@ -399,6 +399,17 @@ bool Rando_OverrideItem(uint8_t* type, uint8_t* subtype); size_t Rando_GetSpoiler(char* buf, size_t buflen); +/* Per-seed "location collected" set (audit C6). Marks any location-keyed + * award as taken, so gates that vanilla-check skill/item inventory (town + * dojos, grip-ring scrub) can instead ask whether THIS shuffled location + * was collected. Without it a shuffled reward that is not the vanilla item + * re-offers forever (dupe), and an out-of-order vanilla pickup skips a + * tier permanently (missable). Persisted in the save sidecar (v7). */ +#define RANDO_COLLECTED_BYTES ((RANDO_LOCATION_COUNT + 7) / 8) +bool Rando_IsCollectedByKey(uint32_t location_key); +void Rando_GetCollectedSet(uint8_t* out, size_t out_len); +void Rando_SetCollectedSet(const uint8_t* in, size_t in_len); + #ifdef __cplusplus } #endif diff --git a/port/rando/rando_file_menu.c b/port/rando/rando_file_menu.c index a6e6dd544f..266a11ee86 100644 --- a/port/rando/rando_file_menu.c +++ b/port/rando/rando_file_menu.c @@ -213,6 +213,8 @@ uint32_t Port_RandoFileMenu_Fingerprint(void) { void Port_RandoFileMenu_CommitAndStart(void) { RandomizerSettings settings = BuildMenuSettings(); uint64_t seed; + if (!sMenu.open) + return; /* only an armed slot (Port_RandoFileMenu_Open) may commit */ PersistMenuSettings(); seed = CurrentSeedValue(); diff --git a/port/rando/rando_save.c b/port/rando/rando_save.c index 9ec357ceff..3becec6e42 100644 --- a/port/rando/rando_save.c +++ b/port/rando/rando_save.c @@ -10,6 +10,7 @@ #include "rando/rando.h" #include "rando/rando_entrance.h" #include "rando/rando_music.h" +#include "item_ids.h" /* ITEM_SKILL_LONG_SPIN: last real engine item id */ #include #include @@ -34,8 +35,14 @@ extern int fileno(FILE*); * v4: per-location reward subtypes (shell counts, kinstone piece ids, dungeon * item ids) so same-item placements restore exactly across reloads. * v5: shuffle_entrances flag (decoupled from shuffle_kinstones) + tricks - * bitmask (glitch-logic tier) so a seed's logic tier restores exactly. */ -#define RANDO_SIDECAR_VERSION 6u + * bitmask (glitch-logic tier) so a seed's logic tier restores exactly. + * v6: obscure/homewarp/start_sword/early_crests/instant_text/tunic/heart + + * shuffle_dungeon_items (in the former reserved3 byte). + * v7: per-slot location-collected bitset appended AFTER the slots array (a + * RandoSidecarFile appendix, NOT a slot field), so the slot layout is + * byte-identical to v6 and older files migrate with the collected set + * zeroed (dojos/scrubs re-derive from empty, harmless). */ +#define RANDO_SIDECAR_VERSION 7u #define RANDO_SIDECAR_MAX_OVERRIDES 64 #define RANDO_SIDECAR_MAX_ENTRANCES 16 #define RANDO_SIDECAR_MUSIC_AREAS 256 @@ -81,11 +88,15 @@ typedef struct RandoSidecarSlot { uint8_t instant_text; uint8_t tunic_color; uint8_t heart_color; - uint8_t reserved3; + uint8_t shuffle_dungeon_items; /* v6 byte, was reserved3 (always 0 == off) */ } RandoSidecarSlot; typedef struct RandoSidecarFile { RandoSidecarSlot slots[RANDO_SIDECAR_SLOTS]; + /* v7 appendix: per-slot location-collected bitset. Kept OUT of + * RandoSidecarSlot so the slot layout stays byte-identical to v6 and the + * per-slot read size is unchanged; read only when version >= 7. */ + uint8_t collected[RANDO_SIDECAR_SLOTS][RANDO_COLLECTED_BYTES]; } RandoSidecarFile; static RandoSidecarFile sSidecar; @@ -137,6 +148,15 @@ static bool LoadAll(void) { break; } } + /* v7 appendix: per-slot collected bitset, right after the slots. A + * v6 file ends here (guard skips the read, collected stays zeroed). + * A truncated v7 appendix is non-fatal: keep the valid slots and + * re-derive collected from empty. */ + if (ok && version >= 7) { + if (fread(sSidecar.collected, sizeof(sSidecar.collected), 1, f) != 1) { + memset(sSidecar.collected, 0, sizeof(sSidecar.collected)); + } + } } fclose(f); if (!ok) { @@ -163,6 +183,21 @@ static bool LoadAll(void) { memset(rec, 0, sizeof(*rec)); continue; } + /* Placed rewards are engine item ids (virtual big-key ids live only + * in subtypes); anything past the metadata table's last entry would + * index gItemMetaData[] out of bounds on award. */ + bool table_ok = true; + for (uint32_t t = 0; t < rec->count; ++t) { + if (rec->table[t] > ITEM_SKILL_LONG_SPIN) { + table_ok = false; + break; + } + } + if (!table_ok) { + fprintf(stderr, "[rando] warning: sidecar slot %d has out-of-range item id; cleared\n", i); + memset(rec, 0, sizeof(*rec)); + continue; + } /* Force-terminate strings; disarm out-of-range entrance indices. */ for (uint32_t o = 0; o < rec->override_count; ++o) { rec->overrides[o].name[sizeof(rec->overrides[o].name) - 1] = '\0'; @@ -287,6 +322,7 @@ bool Port_RandoSave_SaveActiveSlot(int slot) { rec->instant_text = settings.instant_text; rec->tunic_color = (uint8_t)settings.tunic_color; rec->heart_color = (uint8_t)settings.heart_color; + rec->shuffle_dungeon_items = settings.shuffle_dungeon_items ? 1 : 0; /* Save entrance assignments */ rec->entrance_count = 0; for (int i = 0; i < 8; ++i) { @@ -303,6 +339,10 @@ bool Port_RandoSave_SaveActiveSlot(int slot) { rec->music[a] = (int16_t)Rando_Music_GetAssignment(a); } + /* Capture the live collected set for this slot (v7 appendix). LoadAll + * above preserved the other slots' sets; this overwrites only ours. */ + Rando_GetCollectedSet(sSidecar.collected[slot], RANDO_COLLECTED_BYTES); + if (!SaveAll()) return false; fprintf(stderr, "[RANDO] saved sidecar slot %d (%u locations)\n", slot, rec->count); @@ -340,6 +380,7 @@ bool Port_RandoSave_LoadSlot(int slot) { settings.instant_text = rec->instant_text; settings.tunic_color = rec->tunic_color; settings.heart_color = rec->heart_color; + settings.shuffle_dungeon_items = rec->shuffle_dungeon_items != 0; } // No logic define overrides to restore anymore @@ -347,6 +388,10 @@ bool Port_RandoSave_LoadSlot(int slot) { if (!Rando_ActivateTable(rec->seed, settings, rec->table, rec->subtype_table, rec->count)) return false; + /* Restore the collected set (Rando_ActivateTable cleared it). v6-and-older + * loads leave it zeroed, so dojos/scrubs re-derive from empty. */ + Rando_SetCollectedSet(sSidecar.collected[slot], RANDO_COLLECTED_BYTES); + /* Restore entrance and music assignments */ Rando_Entrance_ClearAssignments(); for (uint32_t e = 0; e < rec->entrance_count; ++e) { diff --git a/src/beanstalkSubtask.c b/src/beanstalkSubtask.c index 84f4a423a3..c28206f536 100644 --- a/src/beanstalkSubtask.c +++ b/src/beanstalkSubtask.c @@ -1182,6 +1182,21 @@ bool32 sub_0801AA58(Entity* this, u32 param_2, u32 param_3) { return FALSE; } +static u32 GetSafeTileSetIndex(u16 tileIndex, u16 tileIndexOrig, u32 tilePosAndLayer) { + u32 tileSetIndex; + if (tileIndex < 2048) { + tileSetIndex = tileIndex * 4; + } else if (tileIndex >= 0x4000) { + tileSetIndex = GetTileSetIndexForSpecialTile(tilePosAndLayer, tileIndexOrig); + if (tileSetIndex >= 8192) { + tileSetIndex = 0; + } + } else { + tileSetIndex = 0; + } + return tileSetIndex; +} + void RenderMapLayerToSubTileMap(u16* subTileMap, MapLayer* mapLayer) { u16* subTiles; u16* mapData; @@ -1206,11 +1221,7 @@ void RenderMapLayerToSubTileMap(u16* subTileMap, MapLayer* mapLayer) { for (tileX = 0; tileX < 0x10; tileX++) { // inner loop seems to be unrolled four times for some reason? - if (mapData[0] < 0x4000) { - tileSetIndex = mapData[0] * 4; - } else { - tileSetIndex = GetTileSetIndexForSpecialTile(tilePosAndLayer, mapDataOriginal[0]); - } + tileSetIndex = GetSafeTileSetIndex(mapData[0], mapDataOriginal[0], tilePosAndLayer); subTiles = mapLayer->subTiles + tileSetIndex; subTileMap[0] = subTiles[0]; subTileMap[1] = subTiles[1]; @@ -1218,11 +1229,7 @@ void RenderMapLayerToSubTileMap(u16* subTileMap, MapLayer* mapLayer) { subTileMap[0x80 + 1] = subTiles[3]; subTileMap += 2; - if (mapData[1] < 0x4000) { - tileSetIndex = mapData[1] * 4; - } else { - tileSetIndex = GetTileSetIndexForSpecialTile(tilePosAndLayer + 1, mapDataOriginal[1]); - } + tileSetIndex = GetSafeTileSetIndex(mapData[1], mapDataOriginal[1], tilePosAndLayer + 1); subTiles = mapLayer->subTiles + tileSetIndex; subTileMap[0] = subTiles[0]; subTileMap[1] = subTiles[1]; @@ -1230,11 +1237,7 @@ void RenderMapLayerToSubTileMap(u16* subTileMap, MapLayer* mapLayer) { subTileMap[0x80 + 1] = subTiles[3]; subTileMap += 2; - if (mapData[2] < 0x4000) { - tileSetIndex = mapData[2] * 4; - } else { - tileSetIndex = GetTileSetIndexForSpecialTile(tilePosAndLayer + 2, mapDataOriginal[2]); - } + tileSetIndex = GetSafeTileSetIndex(mapData[2], mapDataOriginal[2], tilePosAndLayer + 2); subTiles = mapLayer->subTiles + tileSetIndex; subTileMap[0] = subTiles[0]; subTileMap[1] = subTiles[1]; @@ -1242,11 +1245,7 @@ void RenderMapLayerToSubTileMap(u16* subTileMap, MapLayer* mapLayer) { subTileMap[0x80 + 1] = subTiles[3]; subTileMap += 2; - if (mapData[3] < 0x4000) { - tileSetIndex = mapData[3] * 4; - } else { - tileSetIndex = GetTileSetIndexForSpecialTile(tilePosAndLayer + 3, mapDataOriginal[3]); - } + tileSetIndex = GetSafeTileSetIndex(mapData[3], mapDataOriginal[3], tilePosAndLayer + 3); subTiles = mapLayer->subTiles + tileSetIndex; subTileMap[0] = subTiles[0]; subTileMap[1] = subTiles[1]; diff --git a/src/enemy/businessScrub.c b/src/enemy/businessScrub.c index 73bfa603b8..8d48d349b8 100644 --- a/src/enemy/businessScrub.c +++ b/src/enemy/businessScrub.c @@ -21,6 +21,8 @@ #ifdef PC_PORT #include "rando/rando_keymap.h" extern bool Rando_OverrideLocationKey(u32 location_key, u8* type, u8* subtype); +extern bool Rando_IsActive(void); +extern bool Rando_IsCollectedByKey(u32 location_key); #endif struct SalesOffering { u8 field_0x0; @@ -36,7 +38,7 @@ struct SalesOffering { typedef struct { /*0x00*/ Entity base; #ifdef PC_PORT - u8 unused1[12 + 4]; /* #98/#99 pattern: +4 for Enemy::child PC growth */ + u8 unused1[12 + 4]; /* #98/#99 pattern: +4 for Enemy::child PC growth */ #else /*0x68*/ u8 unused1[12]; #endif @@ -558,6 +560,16 @@ bool32 sub_0802915C(BusinessScrubEntity* this) { switch (offer->offeredItem) { case ITEM_GRIP_RING: +#ifdef PC_PORT + if (Rando_IsActive()) { + /* audit C6: offer until THIS scrub location is collected, not + * until the vanilla grip ring is owned — its shuffled reward + * may not be the grip ring, which would re-sell forever. */ + if (!Rando_IsCollectedByKey(BusinessScrub_RandoKey(offer))) + return TRUE; + break; + } +#endif if (GetInventoryValue(ITEM_GRIP_RING) == 0) return TRUE; break; @@ -582,7 +594,15 @@ bool32 sub_08029198(const struct SalesOffering* offer) { tmp = CheckGlobalFlag(AKINDO_BOTTLE_SELL); break; case ITEM_BOW: + tmp = GetInventoryValue(offer->offeredItem); + break; case ITEM_GRIP_RING: +#ifdef PC_PORT + if (Rando_IsActive()) { + tmp = Rando_IsCollectedByKey(BusinessScrub_RandoKey(offer)) ? 1 : 0; + break; + } +#endif tmp = GetInventoryValue(offer->offeredItem); break; default: diff --git a/src/fileselect.c b/src/fileselect.c index fe98dd4753..22b237b0a7 100644 --- a/src/fileselect.c +++ b/src/fileselect.c @@ -669,6 +669,7 @@ static void DrawPortSettingsMenu(void); extern bool Port_RandoFileMenu_ShouldOpenForNewFile(void); extern void Port_RandoFileMenu_Open(int save_slot); extern bool Port_RandoFileMenu_IsOpen(void); +extern bool Port_RandoFileMenu_IsModalOpen(void); #endif void sub_08051358(void); @@ -2078,6 +2079,16 @@ void sub_080513C0(void) { case 1: gMapDataBottomSpecial.saveStatus[gMapDataBottomSpecial.unk6] = 1; #ifdef PC_PORT + { + /* New file in this slot: drop any stale rando sidecar left by + * a previous occupant, so a vanilla file cannot be silently + * rando-ized by the crash-window heal (audit R2). A rando + * new-file commit rewrites the sidecar afterwards via + * Port_RandoSave_SaveActiveSlot (later frame), so this clear + * never races the legitimate seed. */ + extern void Port_RandoSave_ClearSlot(int slot); + Port_RandoSave_ClearSlot((int)gMapDataBottomSpecial.unk6); + } if (Port_RandoFileMenu_ShouldOpenForNewFile()) { SetFileSelectState(STATE_RANDOMIZER_CONFIG); break; @@ -2425,7 +2436,10 @@ void sub_080518E4(void) { #ifdef PC_PORT static void HandleFileRandoConfig(void) { - if (!Port_RandoFileMenu_IsOpen()) { + /* Gate on the armed modal, not IsOpen(): IsOpen() ORs in the manually + * L-toggled sidebar, and a pre-opened sidebar must not suppress arming + * the modal for the slot the new-file flow just created. */ + if (!Port_RandoFileMenu_IsModalOpen()) { Port_RandoFileMenu_Open((int)gMapDataBottomSpecial.unk6); } } diff --git a/src/gameUtils.c b/src/gameUtils.c index bcc4a8b916..0907f23539 100644 --- a/src/gameUtils.c +++ b/src/gameUtils.c @@ -33,6 +33,9 @@ #include /* rando: MUSIC_RANDO area-BGM remap (port/rando/rando_music.c) */ extern int Rando_Music_Remap(int area, int song); +#include +/* rando: LV*_CLEAR sanitizer bypass (see ResetTmpFlags) */ +extern bool Rando_IsActive(void); #endif u32 StairsAreValid(void); @@ -1150,6 +1153,14 @@ void ResetTmpFlags(void) { if (!CheckGlobalFlag(WATERBEAN_PUT)) ClearGlobalFlag(WATERBEAN_OUT); +#ifdef PC_PORT + /* Rando shuffles element prizes: "element not held" no longer implies + * "dungeon not cleared". Wiping LV*_CLEAR on every load re-arms bosses + * (prize re-awards, item dupes) and stalls the goron-restock/kinstone + * chains gated on these flags. Vanilla saves keep the sanitizer. */ + if (Rando_IsActive()) + return; +#endif if (!GetInventoryValue(ITEM_EARTH_ELEMENT)) ClearGlobalFlag(LV1_CLEAR); if (!GetInventoryValue(ITEM_FIRE_ELEMENT)) diff --git a/src/npc/bladeBrothers.c b/src/npc/bladeBrothers.c index 886d9365b4..7381008cb1 100644 --- a/src/npc/bladeBrothers.c +++ b/src/npc/bladeBrothers.c @@ -21,6 +21,8 @@ #ifdef PC_PORT #include "rando/rando_keymap.h" extern bool Rando_OverrideLocationKey(u32 location_key, u8* type, u8* subtype); +extern bool Rando_IsActive(void); +extern bool Rando_IsCollectedByKey(u32 location_key); #endif typedef struct { /*0x00*/ Entity base; @@ -145,6 +147,14 @@ const u16 gUnk_08111664[] = { TEXT_INDEX(TEXT_BLADE_MASTERS, 0x34), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x3d), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x46), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x50), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x5a), }; +#ifdef PC_PORT +/* audit C6: in rando a dojo grants a SHUFFLED item, so "owns skill X" no + * longer means "completed this dojo". Ask the rando collected-set whether + * the dojo lesson at this timer was taken instead. */ +static bool BladeBrothers_DojoCollected(u8 timer) { + return Rando_IsCollectedByKey(Rando_BuildScriptedKey(RANDO_SCRIPTED_KEY_DOJO, timer, 0, 0)); +} +#endif const u16 gUnk_0811167A[] = { TEXT_INDEX(TEXT_BLADE_MASTERS, 0x02), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x0a), TEXT_INDEX(TEXT_BLADE_MASTERS, 0x12), @@ -379,6 +389,19 @@ static void sub_08068BEC(Entity* this, u32 unused) { void sub_08068C28(Entity* this) { this->timer = gUnk_08111623[this->type]; if (this->type == 1) { +#ifdef PC_PORT + if (Rando_IsActive()) { + /* Town-dojo tier = count of chain lessons already collected + * (prefix 0..3), not vanilla skill ownership (audit C6): an + * out-of-order vanilla skill pickup must not skip a tier, and a + * shuffled non-skill reward must still advance it. */ + u8 n = 0; + while (n < 3 && BladeBrothers_DojoCollected(n)) + ++n; + this->timer = n; + return; + } +#endif if (GetInventoryValue(ITEM_SKILL_SPIN_ATTACK)) { if (!GetInventoryValue(ITEM_SKILL_ROCK_BREAKER)) { this->timer = 1; @@ -417,6 +440,22 @@ void sub_08068CA0(Entity* this, ScriptExecutionContext* context) { u8 bVar1; u32 uVar2; +#ifdef PC_PORT + if (Rando_IsActive()) { + /* "Dojo exhausted?" keyed on collected lessons, not vanilla skills + * (audit C6): a dojo whose shuffled reward is not its vanilla skill + * would otherwise re-offer (and re-grant) forever. */ + if (this->type == 1) { + context->condition = (BladeBrothers_DojoCollected(0) && BladeBrothers_DojoCollected(1) && + BladeBrothers_DojoCollected(2) && BladeBrothers_DojoCollected(3)) + ? 1 + : 0; + } else { + context->condition = BladeBrothers_DojoCollected(this->timer) ? 1 : 0; + } + return; + } +#endif bVar1 = this->type; if (bVar1 == 1) { context->condition = bVar1; diff --git a/src/object/fourElements.c b/src/object/fourElements.c index be371006b1..3f7968aab2 100644 --- a/src/object/fourElements.c +++ b/src/object/fourElements.c @@ -155,6 +155,13 @@ void FourElements_Action2(FourElementsEntity* this) { } #endif InitItemGetSequence(item, subtype, 1); + /* Set the dungeon-clear + prize-collected flags at GRANT time, not + * after the ~450-frame ceremony (Action6). A Save&Quit mid-ceremony + * otherwise persists the awarded item while the dungeon reads + * uncleared, so the next visit re-runs the prize and re-awards it + * (audit R3). Idempotent, so Action6's calls remain harmless. */ + FourElements_SetDungeonClearFlag(super->type); + SetRoomFlag(0); sub_0808C650(super, 1); SetFade(FADE_BLACK_WHITE | FADE_INSTANT, 2); SoundReq(SFX_F8); diff --git a/src/object/itemOnGround.c b/src/object/itemOnGround.c index 9857bacdb8..99f00595f0 100644 --- a/src/object/itemOnGround.c +++ b/src/object/itemOnGround.c @@ -23,6 +23,7 @@ #ifdef PC_PORT #include extern bool Rando_OverrideLocationKey(u32 location_key, u8* type, u8* subtype); +extern void Rando_ClearAwardLatch(void); #endif void sub_08081150(ItemOnGroundEntity* this); @@ -406,6 +407,12 @@ bool32 sub_08081420(ItemOnGroundEntity* this) { return TRUE; } else { GiveItem(super->type, super->type2); +#ifdef PC_PORT + /* GiveItem does not route through Rando_OverrideItem, so disarm the + * location-award latch a preceding Rando_OverrideLocationKey may have + * set (audit R4). */ + Rando_ClearAwardLatch(); +#endif return FALSE; } } diff --git a/src/playerUtils.c b/src/playerUtils.c index 038719e3da..dba8e7e316 100644 --- a/src/playerUtils.c +++ b/src/playerUtils.c @@ -3839,17 +3839,17 @@ void sub_0807BBE4(void) { tileIndex = *bottomMap; bottomMap++; if (tileIndex < 0x4000) { - *bottomCollision = gMapTileTypeToCollisionData[bottomTiles[tileIndex]]; + *bottomCollision = (tileIndex < 2048) ? gMapTileTypeToCollisionData[bottomTiles[tileIndex]] : 0; } else { - *bottomCollision = gMapSpecialTileToCollisionData[tileIndex - 0x4000]; + *bottomCollision = (tileIndex - 0x4000 < 151) ? gMapSpecialTileToCollisionData[tileIndex - 0x4000] : 0; } bottomCollision++; tileIndex = (u32)*topMap; topMap++; if (tileIndex < 0x4000) { - *topCollision = gMapTileTypeToCollisionData[topTiles[tileIndex]]; + *topCollision = (tileIndex < 2048) ? gMapTileTypeToCollisionData[topTiles[tileIndex]] : 0; } else { - *topCollision = gMapSpecialTileToCollisionData[tileIndex - 0x4000]; + *topCollision = (tileIndex - 0x4000 < 151) ? gMapSpecialTileToCollisionData[tileIndex - 0x4000] : 0; } topCollision++; } diff --git a/src/scroll.c b/src/scroll.c index 489013fd72..4357a9e628 100644 --- a/src/scroll.c +++ b/src/scroll.c @@ -957,9 +957,9 @@ void FillActTileForLayer(MapLayer* mapLayer) { for (tilePos = 0; tilePos < 0x40 * 0x40; tilePos++) { u16 tileIndex = mapData[tilePos]; if (tileIndex < 0x4000) { - mapLayer->actTiles[tilePos] = GetMapTileTypeToActTile(tileTypes[tileIndex]); + mapLayer->actTiles[tilePos] = (tileIndex < 2048) ? GetMapTileTypeToActTile(tileTypes[tileIndex]) : 0; } else { - mapLayer->actTiles[tilePos] = gMapSpecialTileToActTile[tileIndex - 0x4000]; + mapLayer->actTiles[tilePos] = (tileIndex - 0x4000 < 151) ? gMapSpecialTileToActTile[tileIndex - 0x4000] : 0; } } } diff --git a/tools/src/asset_processor/assets/dungeonmap.cpp b/tools/src/asset_processor/assets/dungeonmap.cpp index e7b1c4a902..f558c86576 100644 --- a/tools/src/asset_processor/assets/dungeonmap.cpp +++ b/tools/src/asset_processor/assets/dungeonmap.cpp @@ -64,6 +64,8 @@ void DungeonMapAsset::buildToBinary() { switch (data[i]) { case '\n': continue; + case '\r': + continue; case ' ': byte <<= 2; pixels++;