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
This commit is contained in:
parent
4de7ebd8b0
commit
995c978628
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
4
Main.py
4
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()
|
||||
|
|
|
@ -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()):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue