From db126c1776c8f7306d9ef28d0b399c8ca5efea13 Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:44:30 +0900 Subject: [PATCH 1/2] Fix issue that happens around Unittest and CI: ResourceWarning for open functions and entrance.sav error --- Main.py | 13 +++++++++++++ Messages.py | 3 ++- Patches.py | 9 +++++++-- Unittest.py | 10 ++++++++-- World.py | 9 ++++++++- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Main.py b/Main.py index 9709f55fc..8d3cdefaf 100644 --- a/Main.py +++ b/Main.py @@ -32,6 +32,11 @@ from version import __version__ +def _attempt_rng_seed(settings: Settings, attempt: int) -> int: + """Return a deterministic retry seed independent of failed-attempt RNG use.""" + seed_material = f"{settings.numeric_seed}:{attempt}".encode("utf-8") + return int.from_bytes(hashlib.sha256(seed_material).digest(), byteorder="big") + def main(settings: Settings, max_attempts: int = 10) -> Spoiler: clear_hint_exclusion_cache() logger = logging.getLogger('') @@ -40,8 +45,16 @@ def main(settings: Settings, max_attempts: int = 10) -> Spoiler: rom = resolve_settings(settings) max_attempts = max(max_attempts, 1) + generation_rng_state = random.getstate() spoiler = None for attempt in range(1, max_attempts + 1): + if attempt == 1: + random.setstate(generation_rng_state) + else: + # Do not let the amount of random work performed by a failed attempt + # influence the next attempt. This keeps ER/fill retries stable even + # when unrelated setup code changes. + random.seed(_attempt_rng_seed(settings, attempt)) try: spoiler = generate(settings) break diff --git a/Messages.py b/Messages.py index c1035bd0c..3afd0c22e 100644 --- a/Messages.py +++ b/Messages.py @@ -226,7 +226,8 @@ def normalize_jp_controller_tokens(text: str) -> str: for code, token in SCJP.items(): if code < 0x10000: REVERSE_MAP_JP[code] = token - json.dump([CHARACTER_MAP_JP, REVERSE_MAP_JP], open(data_path('generated/jp_char_map.otrx'), mode="w")) + with open(data_path('generated/jp_char_map.otrx'), mode="w", encoding="utf-8") as stream: + json.dump([CHARACTER_MAP_JP, REVERSE_MAP_JP], stream) for code, token in SCJP.items(): if code < len(REVERSE_MAP_JP): diff --git a/Patches.py b/Patches.py index 00d0b3bf7..ab1be1af6 100644 --- a/Patches.py +++ b/Patches.py @@ -59,9 +59,14 @@ def patch_rom(spoiler: Spoiler, world: World, rom: Rom) -> Rom: lang = world.language # Binary patches of certain assets. + with open(data_path("bin_patch.json"), mode="r", encoding="utf-8") as stream: + bin_patch_data = json.load(stream) + bin_patches = [ - (os.path.join(lang.path, x), int(y[0], base=16)) for x, y in json.load(open(data_path("bin_patch.json"))).items() if x in lang.data.keys() - ] + (os.path.join(lang.path, x), int(y[0], base=16)) + for x, y in bin_patch_data.items() + if x in lang.data.keys() + ] for (bin_path, write_address) in bin_patches: with open(bin_path, 'rb') as stream: diff --git a/Unittest.py b/Unittest.py index 11e187365..dc5ae7680 100644 --- a/Unittest.py +++ b/Unittest.py @@ -1081,13 +1081,19 @@ def extract_first_second_level_keys(data: dict) -> list: class TestLanguageFile(unittest.TestCase): def test_langfiles(self): base_keys = extract_first_second_level_keys(data.lang.property_build.lang_info) - bin_patch = list(json.load(open(data_path("bin_patch.json"), encoding='utf-8')).keys()) + ["blue_fire_arrow_item_name_jap.ia4", "blue_fire_arrow_item_name_eng.ia4"] + with open(data_path("bin_patch.json"), mode="r", encoding="utf-8") as stream: + bin_patch = list(json.load(stream).keys()) + [ + "blue_fire_arrow_item_name_jap.ia4", + "blue_fire_arrow_item_name_eng.ia4", + ] for lang in os.listdir(lang_path()): if os.path.isdir(os.path.join(lang_path(), lang)) and lang != "__pycache__": lang_files = os.listdir(os.path.join(lang_path(), lang)) self.assertIn("property.json", lang_files, msg = "\n{}: property.json is not included".format(lang)) lang_files.remove("property.json") - property_keys = extract_first_second_level_keys(json.load(open(os.path.join(lang_path(), lang, "property.json"), encoding='utf-8'))) + property_path = os.path.join(lang_path(), lang, "property.json") + with open(property_path, mode="r", encoding="utf-8") as stream: + property_keys = extract_first_second_level_keys(json.load(stream)) only_in_base = list(sorted(set(base_keys) - set(property_keys))) only_in_lang = list(sorted(set(property_keys) - set(base_keys))) diff_keys = [] diff --git a/World.py b/World.py index 85a9553b6..9c2d5e501 100644 --- a/World.py +++ b/World.py @@ -66,7 +66,14 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting self.distribution: WorldDistribution = settings.distribution.world_dists[world_id] # language property... - self.language: Language = Language(settings.language) + # Language loading/fallback must not perturb generation RNG. Entrance + # and item placement are fixed-seed tests, so even unrelated random + # consumption during language setup can change ER/fill outcomes. + language_rng_state = random.getstate() + try: + self.language: Language = Language(settings.language) + finally: + random.setstate(language_rng_state) # rename a few attributes... self.keysanity: bool = settings.shuffle_smallkeys in ('keysanity', 'remove', 'any_dungeon', 'overworld', 'regional') From 82b42e0012f458c7136b4e051ed327b1c23744a8 Mon Sep 17 00:00:00 2001 From: JackTriton <35764777+JackTriton@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:21:05 +0900 Subject: [PATCH 2/2] Revert Main and World --- Main.py | 13 ------------- World.py | 9 +-------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Main.py b/Main.py index 8d3cdefaf..9709f55fc 100644 --- a/Main.py +++ b/Main.py @@ -32,11 +32,6 @@ from version import __version__ -def _attempt_rng_seed(settings: Settings, attempt: int) -> int: - """Return a deterministic retry seed independent of failed-attempt RNG use.""" - seed_material = f"{settings.numeric_seed}:{attempt}".encode("utf-8") - return int.from_bytes(hashlib.sha256(seed_material).digest(), byteorder="big") - def main(settings: Settings, max_attempts: int = 10) -> Spoiler: clear_hint_exclusion_cache() logger = logging.getLogger('') @@ -45,16 +40,8 @@ def main(settings: Settings, max_attempts: int = 10) -> Spoiler: rom = resolve_settings(settings) max_attempts = max(max_attempts, 1) - generation_rng_state = random.getstate() spoiler = None for attempt in range(1, max_attempts + 1): - if attempt == 1: - random.setstate(generation_rng_state) - else: - # Do not let the amount of random work performed by a failed attempt - # influence the next attempt. This keeps ER/fill retries stable even - # when unrelated setup code changes. - random.seed(_attempt_rng_seed(settings, attempt)) try: spoiler = generate(settings) break diff --git a/World.py b/World.py index 9c2d5e501..85a9553b6 100644 --- a/World.py +++ b/World.py @@ -66,14 +66,7 @@ def __init__(self, world_id: int, settings: Settings, resolve_randomized_setting self.distribution: WorldDistribution = settings.distribution.world_dists[world_id] # language property... - # Language loading/fallback must not perturb generation RNG. Entrance - # and item placement are fixed-seed tests, so even unrelated random - # consumption during language setup can change ER/fill outcomes. - language_rng_state = random.getstate() - try: - self.language: Language = Language(settings.language) - finally: - random.setstate(language_rng_state) + self.language: Language = Language(settings.language) # rename a few attributes... self.keysanity: bool = settings.shuffle_smallkeys in ('keysanity', 'remove', 'any_dungeon', 'overworld', 'regional')