From 995c9786284ddc6e5c4eec743bac82fa930dd3c9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 2 Feb 2023 01:14:23 +0100 Subject: [PATCH] Core: replace global random state with descriptive error (#1424) * Core: replace global random state with descriptive error * Core: make random a proxy object and rename slot_seeds --- BaseClasses.py | 28 +++++++++++++++++++++++----- LttPAdjuster.py | 2 +- Main.py | 4 ++++ OoTAdjuster.py | 2 +- worlds/alttp/Rom.py | 16 ++++++++-------- worlds/checksfinder/__init__.py | 2 +- worlds/dkc3/Rom.py | 2 +- worlds/factorio/Mod.py | 2 +- worlds/factorio/__init__.py | 2 +- worlds/hk/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- worlds/oot/Cosmetics.py | 2 +- worlds/oot/Patches.py | 2 +- worlds/oot/__init__.py | 4 ++-- worlds/pokemon_rb/rom.py | 2 +- worlds/ror2/__init__.py | 2 +- worlds/smw/Rom.py | 2 +- worlds/spire/__init__.py | 2 +- 18 files changed, 51 insertions(+), 29 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index bf89a0e6..219ff5ee 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -29,6 +29,20 @@ class Group(TypedDict, total=False): link_replacement: bool +class ThreadBarrierProxy(): + """Passes through getattr while passthrough is True""" + def __init__(self, obj: Any): + self.passthrough = True + self.obj = obj + + def __getattr__(self, item): + if self.passthrough: + return getattr(self.obj, item) + else: + raise RuntimeError("You are in a threaded context and global random state was removed for your safety. " + "Please use multiworld.per_slot_randoms[player] or randomize ahead of output.") + + class MultiWorld(): debug_types = False player_name: Dict[int, str] @@ -61,6 +75,9 @@ class MultiWorld(): game: Dict[int, str] + random: random.Random + per_slot_randoms: Dict[int, random.Random] + class AttributeProxy(): def __init__(self, rule): self.rule = rule @@ -69,7 +86,8 @@ class MultiWorld(): return self.rule(player) def __init__(self, players: int): - self.random = random.Random() # world-local random state is saved for multiple generations running concurrently + # world-local random state is saved for multiple generations running concurrently + self.random = ThreadBarrierProxy(random.Random()) self.players = players self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids} self.glitch_triforce = False @@ -160,7 +178,7 @@ class MultiWorld(): set_player_attr('completion_condition', lambda state: True) self.custom_data = {} self.worlds = {} - self.slot_seeds = {} + self.per_slot_randoms = {} self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: @@ -206,8 +224,8 @@ class MultiWorld(): else: self.random.seed(self.seed) self.seed_name = name if name else str(self.seed) - self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in - range(1, self.players + 1)} + self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in + range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: for option_key in Options.common_options: @@ -291,7 +309,7 @@ class MultiWorld(): self.state = CollectionState(self) def secure(self): - self.random = secrets.SystemRandom() + self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True @functools.cached_property diff --git a/LttPAdjuster.py b/LttPAdjuster.py index a2cc2eeb..205a7681 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -35,7 +35,7 @@ class AdjusterWorld(object): def __init__(self, sprite_pool): import random self.sprite_pool = {1: sprite_pool} - self.slot_seeds = {1: random} + self.per_slot_randoms = {1: random} class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter): diff --git a/Main.py b/Main.py index 983874f5..879ca720 100644 --- a/Main.py +++ b/Main.py @@ -251,6 +251,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No balance_multiworld_progression(world) logger.info(f'Beginning output...') + + # we're about to output using multithreading, so we're removing the global random state to prevent accidental use + world.random.passthrough = False + outfilebase = 'AP_' + world.seed_name output = tempfile.TemporaryDirectory() diff --git a/OoTAdjuster.py b/OoTAdjuster.py index c2df9b07..f449113d 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -197,7 +197,7 @@ def set_icon(window): def adjust(args): # Create a fake world and OOTWorld to use as a base world = MultiWorld(1) - world.slot_seeds = {1: random} + world.per_slot_randoms = {1: random} ootworld = OOTWorld(world, 1) # Set options in the fake OOTWorld for name, option in chain(cosmetic_options.items(), sfx_options.items()): diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index a374658e..a55946ba 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -92,7 +92,7 @@ class LocalRom(object): # cause crash to provide traceback import xxtea - local_random = world.slot_seeds[player] + local_random = world.per_slot_randoms[player] key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big')) self.write_bytes(0x1800B0, bytearray(key)) self.write_int16(0x180087, 1) @@ -384,7 +384,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct max_enemizer_tries = 5 for i in range(max_enemizer_tries): - enemizer_seed = str(world.slot_seeds[player].randint(0, 999999999)) + enemizer_seed = str(world.per_slot_randoms[player].randint(0, 999999999)) enemizer_command = [os.path.abspath(enemizercli), '--rom', randopatch_path, '--seed', enemizer_seed, @@ -414,7 +414,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct continue for j in range(i + 1, max_enemizer_tries): - world.slot_seeds[player].randint(0, 999999999) + world.per_slot_randoms[player].randint(0, 999999999) # Sacrifice all remaining random numbers that would have been used for unused enemizer tries. # This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness break @@ -766,7 +766,7 @@ def get_nonnative_item_sprite(item: str) -> int: def patch_rom(world, rom, player, enemized): - local_random = world.slot_seeds[player] + local_random = world.per_slot_randoms[player] # patch items @@ -1646,7 +1646,7 @@ def patch_rom(world, rom, player, enemized): rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit if world.tile_shuffle[player]: - tile_set = TileSet.get_random_tile_set(world.slot_seeds[player]) + tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player]) rom.write_byte(0x4BA21, tile_set.get_speed()) rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_bytes(0x4BA2A, tile_set.get_bytes()) @@ -1779,7 +1779,7 @@ def hud_format_text(text): def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options, world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False): - local_random = random if not world else world.slot_seeds[player] + local_random = random if not world else world.per_slot_randoms[player] disable_music: bool = not music # enable instant item menu if menuspeed == 'instant': @@ -2105,7 +2105,7 @@ def write_string_to_rom(rom, target, string): def write_strings(rom, world, player): from . import ALTTPWorld - local_random = world.slot_seeds[player] + local_random = world.per_slot_randoms[player] w: ALTTPWorld = world.worlds[player] tt = TextTable() @@ -2330,7 +2330,7 @@ def write_strings(rom, world, player): if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or ( world.swordless[player] or world.logic[player] == 'noglitches')): prog_bow_locs = world.find_item_locations('Progressive Bow', player, True) - world.slot_seeds[player].shuffle(prog_bow_locs) + world.per_slot_randoms[player].shuffle(prog_bow_locs) found_bow = False found_bow_alt = False while prog_bow_locs and not (found_bow and found_bow_alt): diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index 5dce1b8e..9be2350f 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -38,7 +38,7 @@ class ChecksFinderWorld(World): def _get_checksfinder_data(self): return { - 'world_seed': self.multiworld.slot_seeds[self.player].getrandbits(32), + 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), 'seed_name': self.multiworld.seed_name, 'player_name': self.multiworld.get_player_name(self.player), 'player_id': self.player, diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 6308e958..37b8ecf0 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -476,7 +476,7 @@ class LocalRom(object): def patch_rom(world, rom, player, active_level_list): - local_random = world.slot_seeds[player] + local_random = world.per_slot_randoms[player] # Boomer Costs bonus_coin_cost = world.krematoa_bonus_coin_cost[player] diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 73f4c4b7..cda1ca1f 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -99,7 +99,7 @@ def generate_mod(world: "Factorio", output_directory: str): for location in world.locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" - random = multiworld.slot_seeds[player] + random = multiworld.per_slot_randoms[player] def flop_random(low, high, base=None): """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 4b6d6b9e..6b4e5c86 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -222,7 +222,7 @@ class Factorio(World): map_basic_settings = self.multiworld.world_gen[player].value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 - map_basic_settings["seed"] = self.multiworld.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint + map_basic_settings["seed"] = self.multiworld.per_slot_randoms[player].randint(0, 2 ** 32 - 1) # 32 bit uint start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index a635bb5c..5993d1b3 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -446,7 +446,7 @@ class HKWorld(World): options[option_name] = optionvalue # 32 bit int - slot_data["seed"] = self.multiworld.slot_seeds[self.player].randint(-2147483647, 2147483646) + slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) # Backwards compatibility for shop cost data (HKAP < 0.1.0) if not self.multiworld.CostSanity[self.player]: diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index f94a1159..c0a034f2 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -70,7 +70,7 @@ class MinecraftWorld(World): def _get_mc_data(self): exits = [connection[0] for connection in default_connections] return { - 'world_seed': self.multiworld.slot_seeds[self.player].getrandbits(32), + 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), 'seed_name': self.multiworld.seed_name, 'player_name': self.multiworld.get_player_name(self.player), 'player_id': self.player, diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py index e132c2ce..7b8008fb 100644 --- a/worlds/oot/Cosmetics.py +++ b/worlds/oot/Cosmetics.py @@ -769,7 +769,7 @@ patch_sets[0x1F073FD9] = { def patch_cosmetics(ootworld, rom): # Use the world's slot seed for cosmetics - random.seed(ootworld.multiworld.slot_seeds[ootworld.player]) + random.seed(ootworld.multiworld.per_slot_randoms[ootworld.player]) # try to detect the cosmetic patch data format versioned_patch_set = None diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 1569d12f..c02512a7 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2346,7 +2346,7 @@ def patch_rom(world, rom): # Write numeric seed truncated to 32 bits for rng seeding # Overwritten with new seed every time a new rng value is generated - rng_seed = world.multiworld.slot_seeds[world.player].getrandbits(32) + rng_seed = world.multiworld.per_slot_randoms[world.player].getrandbits(32) rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed) # Static initial seed value for one-time random actions like the Hylian Shield discount rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 3dd5baf7..7207cada 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -962,10 +962,10 @@ class OOTWorld(World): trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] self.trap_appearances = {} for loc_id in trap_location_ids: - self.trap_appearances[loc_id] = self.create_item(self.multiworld.slot_seeds[self.player].choice(self.fake_items).name) + self.trap_appearances[loc_id] = self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also - self.hint_rng = self.multiworld.slot_seeds[self.player] + self.hint_rng = self.multiworld.per_slot_randoms[self.player] outfile_name = self.multiworld.get_out_file_name_base(self.player) rom = Rom(file=get_options()['oot_options']['rom_file']) diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 6f3b8908..9dbc3a8b 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -359,7 +359,7 @@ def process_pokemon_data(self): def generate_output(self, output_directory: str): - random = self.multiworld.slot_seeds[self.player] + random = self.multiworld.per_slot_randoms[self.player] game_version = self.multiworld.game_version[self.player].current_key data = bytes(get_base_rom_bytes(game_version)) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index c1b775cf..127e614f 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -110,7 +110,7 @@ class RiskOfRainWorld(World): def fill_slot_data(self): return { "itemPickupStep": self.multiworld.item_pickup_step[self.player].value, - "seed": "".join(self.multiworld.slot_seeds[self.player].choice(string.digits) for _ in range(16)), + "seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), "totalLocations": self.multiworld.total_locations[self.player].value, "totalRevivals": self.multiworld.total_revivals[self.player].value, "startWithDio": self.multiworld.start_with_revive[self.player].value, diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py index d39fcc46..2d03e19d 100644 --- a/worlds/smw/Rom.py +++ b/worlds/smw/Rom.py @@ -821,7 +821,7 @@ def handle_boss_shuffle(rom, world, player): def patch_rom(world, rom, player, active_level_dict): - local_random = world.slot_seeds[player] + local_random = world.per_slot_randoms[player] goal_text = generate_goal_text(world, player) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 237e6775..a1d02222 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -38,7 +38,7 @@ class SpireWorld(World): def _get_slot_data(self): return { - 'seed': "".join(self.multiworld.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)), + 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)), 'character': self.multiworld.character[self.player], 'ascension': self.multiworld.ascension[self.player], 'heart_run': self.multiworld.heart_run[self.player]