Skip to content
Open
617 changes: 2 additions & 615 deletions port/port_imgui_menu.cpp

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions port/port_repro_rando.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions port/port_tts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,20 @@ struct State {
#else
std::atomic<HANDLE> 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<std::mutex> lk(queue_mu);
queue.clear();
}
queue_cv.notify_all();
if (worker.joinable())
worker.join();
}
};

State g_state;
Expand Down
74 changes: 74 additions & 0 deletions port/rando/rando.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions port/rando/rando.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions port/rando/rando_file_menu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
51 changes: 48 additions & 3 deletions port/rando/rando_save.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <stdint.h>
#include <stdio.h>
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -340,13 +380,18 @@ 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

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) {
Expand Down
Loading
Loading