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
|
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():
|
class MultiWorld():
|
||||||
debug_types = False
|
debug_types = False
|
||||||
player_name: Dict[int, str]
|
player_name: Dict[int, str]
|
||||||
|
@ -61,6 +75,9 @@ class MultiWorld():
|
||||||
|
|
||||||
game: Dict[int, str]
|
game: Dict[int, str]
|
||||||
|
|
||||||
|
random: random.Random
|
||||||
|
per_slot_randoms: Dict[int, random.Random]
|
||||||
|
|
||||||
class AttributeProxy():
|
class AttributeProxy():
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
self.rule = rule
|
self.rule = rule
|
||||||
|
@ -69,7 +86,8 @@ class MultiWorld():
|
||||||
return self.rule(player)
|
return self.rule(player)
|
||||||
|
|
||||||
def __init__(self, players: int):
|
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.players = players
|
||||||
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
|
||||||
self.glitch_triforce = False
|
self.glitch_triforce = False
|
||||||
|
@ -160,7 +178,7 @@ class MultiWorld():
|
||||||
set_player_attr('completion_condition', lambda state: True)
|
set_player_attr('completion_condition', lambda state: True)
|
||||||
self.custom_data = {}
|
self.custom_data = {}
|
||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
self.slot_seeds = {}
|
self.per_slot_randoms = {}
|
||||||
self.plando_options = PlandoOptions.none
|
self.plando_options = PlandoOptions.none
|
||||||
|
|
||||||
def get_all_ids(self) -> Tuple[int, ...]:
|
def get_all_ids(self) -> Tuple[int, ...]:
|
||||||
|
@ -206,8 +224,8 @@ class MultiWorld():
|
||||||
else:
|
else:
|
||||||
self.random.seed(self.seed)
|
self.random.seed(self.seed)
|
||||||
self.seed_name = name if name else str(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
|
self.per_slot_randoms = {player: random.Random(self.random.getrandbits(64)) for player in
|
||||||
range(1, self.players + 1)}
|
range(1, self.players + 1)}
|
||||||
|
|
||||||
def set_options(self, args: Namespace) -> None:
|
def set_options(self, args: Namespace) -> None:
|
||||||
for option_key in Options.common_options:
|
for option_key in Options.common_options:
|
||||||
|
@ -291,7 +309,7 @@ class MultiWorld():
|
||||||
self.state = CollectionState(self)
|
self.state = CollectionState(self)
|
||||||
|
|
||||||
def secure(self):
|
def secure(self):
|
||||||
self.random = secrets.SystemRandom()
|
self.random = ThreadBarrierProxy(secrets.SystemRandom())
|
||||||
self.is_race = True
|
self.is_race = True
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
|
|
|
@ -35,7 +35,7 @@ class AdjusterWorld(object):
|
||||||
def __init__(self, sprite_pool):
|
def __init__(self, sprite_pool):
|
||||||
import random
|
import random
|
||||||
self.sprite_pool = {1: sprite_pool}
|
self.sprite_pool = {1: sprite_pool}
|
||||||
self.slot_seeds = {1: random}
|
self.per_slot_randoms = {1: random}
|
||||||
|
|
||||||
|
|
||||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
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)
|
balance_multiworld_progression(world)
|
||||||
|
|
||||||
logger.info(f'Beginning output...')
|
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
|
outfilebase = 'AP_' + world.seed_name
|
||||||
|
|
||||||
output = tempfile.TemporaryDirectory()
|
output = tempfile.TemporaryDirectory()
|
||||||
|
|
|
@ -197,7 +197,7 @@ def set_icon(window):
|
||||||
def adjust(args):
|
def adjust(args):
|
||||||
# Create a fake world and OOTWorld to use as a base
|
# Create a fake world and OOTWorld to use as a base
|
||||||
world = MultiWorld(1)
|
world = MultiWorld(1)
|
||||||
world.slot_seeds = {1: random}
|
world.per_slot_randoms = {1: random}
|
||||||
ootworld = OOTWorld(world, 1)
|
ootworld = OOTWorld(world, 1)
|
||||||
# Set options in the fake OOTWorld
|
# Set options in the fake OOTWorld
|
||||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||||
|
|
|
@ -92,7 +92,7 @@ class LocalRom(object):
|
||||||
# cause crash to provide traceback
|
# cause crash to provide traceback
|
||||||
import xxtea
|
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'))
|
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
|
||||||
self.write_bytes(0x1800B0, bytearray(key))
|
self.write_bytes(0x1800B0, bytearray(key))
|
||||||
self.write_int16(0x180087, 1)
|
self.write_int16(0x180087, 1)
|
||||||
|
@ -384,7 +384,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
|
||||||
|
|
||||||
max_enemizer_tries = 5
|
max_enemizer_tries = 5
|
||||||
for i in range(max_enemizer_tries):
|
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),
|
enemizer_command = [os.path.abspath(enemizercli),
|
||||||
'--rom', randopatch_path,
|
'--rom', randopatch_path,
|
||||||
'--seed', enemizer_seed,
|
'--seed', enemizer_seed,
|
||||||
|
@ -414,7 +414,7 @@ def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_direct
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for j in range(i + 1, max_enemizer_tries):
|
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.
|
# 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
|
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
|
||||||
break
|
break
|
||||||
|
@ -766,7 +766,7 @@ def get_nonnative_item_sprite(item: str) -> int:
|
||||||
|
|
||||||
|
|
||||||
def patch_rom(world, rom, player, enemized):
|
def patch_rom(world, rom, player, enemized):
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.per_slot_randoms[player]
|
||||||
|
|
||||||
# patch items
|
# patch items
|
||||||
|
|
||||||
|
@ -1646,7 +1646,7 @@ def patch_rom(world, rom, player, enemized):
|
||||||
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
||||||
|
|
||||||
if world.tile_shuffle[player]:
|
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(0x4BA21, tile_set.get_speed())
|
||||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
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,
|
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,
|
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||||
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = 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
|
disable_music: bool = not music
|
||||||
# enable instant item menu
|
# enable instant item menu
|
||||||
if menuspeed == 'instant':
|
if menuspeed == 'instant':
|
||||||
|
@ -2105,7 +2105,7 @@ def write_string_to_rom(rom, target, string):
|
||||||
|
|
||||||
def write_strings(rom, world, player):
|
def write_strings(rom, world, player):
|
||||||
from . import ALTTPWorld
|
from . import ALTTPWorld
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.per_slot_randoms[player]
|
||||||
w: ALTTPWorld = world.worlds[player]
|
w: ALTTPWorld = world.worlds[player]
|
||||||
|
|
||||||
tt = TextTable()
|
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 (
|
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')):
|
world.swordless[player] or world.logic[player] == 'noglitches')):
|
||||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
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 = False
|
||||||
found_bow_alt = False
|
found_bow_alt = False
|
||||||
while prog_bow_locs and not (found_bow and found_bow_alt):
|
while prog_bow_locs and not (found_bow and found_bow_alt):
|
||||||
|
|
|
@ -38,7 +38,7 @@ class ChecksFinderWorld(World):
|
||||||
|
|
||||||
def _get_checksfinder_data(self):
|
def _get_checksfinder_data(self):
|
||||||
return {
|
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,
|
'seed_name': self.multiworld.seed_name,
|
||||||
'player_name': self.multiworld.get_player_name(self.player),
|
'player_name': self.multiworld.get_player_name(self.player),
|
||||||
'player_id': self.player,
|
'player_id': self.player,
|
||||||
|
|
|
@ -476,7 +476,7 @@ class LocalRom(object):
|
||||||
|
|
||||||
|
|
||||||
def patch_rom(world, rom, player, active_level_list):
|
def patch_rom(world, rom, player, active_level_list):
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.per_slot_randoms[player]
|
||||||
|
|
||||||
# Boomer Costs
|
# Boomer Costs
|
||||||
bonus_coin_cost = world.krematoa_bonus_coin_cost[player]
|
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]
|
for location in world.locations]
|
||||||
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
|
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):
|
def flop_random(low, high, base=None):
|
||||||
"""Guarantees 50% below base and 50% above base, uniform distribution in each direction."""
|
"""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"]
|
map_basic_settings = self.multiworld.world_gen[player].value["basic"]
|
||||||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
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
|
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
|
options[option_name] = optionvalue
|
||||||
|
|
||||||
# 32 bit int
|
# 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)
|
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
|
||||||
if not self.multiworld.CostSanity[self.player]:
|
if not self.multiworld.CostSanity[self.player]:
|
||||||
|
|
|
@ -70,7 +70,7 @@ class MinecraftWorld(World):
|
||||||
def _get_mc_data(self):
|
def _get_mc_data(self):
|
||||||
exits = [connection[0] for connection in default_connections]
|
exits = [connection[0] for connection in default_connections]
|
||||||
return {
|
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,
|
'seed_name': self.multiworld.seed_name,
|
||||||
'player_name': self.multiworld.get_player_name(self.player),
|
'player_name': self.multiworld.get_player_name(self.player),
|
||||||
'player_id': self.player,
|
'player_id': self.player,
|
||||||
|
|
|
@ -769,7 +769,7 @@ patch_sets[0x1F073FD9] = {
|
||||||
|
|
||||||
def patch_cosmetics(ootworld, rom):
|
def patch_cosmetics(ootworld, rom):
|
||||||
# Use the world's slot seed for cosmetics
|
# 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
|
# try to detect the cosmetic patch data format
|
||||||
versioned_patch_set = None
|
versioned_patch_set = None
|
||||||
|
|
|
@ -2346,7 +2346,7 @@ def patch_rom(world, rom):
|
||||||
|
|
||||||
# Write numeric seed truncated to 32 bits for rng seeding
|
# Write numeric seed truncated to 32 bits for rng seeding
|
||||||
# Overwritten with new seed every time a new rng value is generated
|
# 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)
|
rom.write_int32(rom.sym('RNG_SEED_INT'), rng_seed)
|
||||||
# Static initial seed value for one-time random actions like the Hylian Shield discount
|
# Static initial seed value for one-time random actions like the Hylian Shield discount
|
||||||
rom.write_int32(rom.sym('RANDOMIZER_RNG_SEED'), rng_seed)
|
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]
|
trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap]
|
||||||
self.trap_appearances = {}
|
self.trap_appearances = {}
|
||||||
for loc_id in trap_location_ids:
|
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
|
# 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)
|
outfile_name = self.multiworld.get_out_file_name_base(self.player)
|
||||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
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):
|
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
|
game_version = self.multiworld.game_version[self.player].current_key
|
||||||
data = bytes(get_base_rom_bytes(game_version))
|
data = bytes(get_base_rom_bytes(game_version))
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ class RiskOfRainWorld(World):
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
return {
|
return {
|
||||||
"itemPickupStep": self.multiworld.item_pickup_step[self.player].value,
|
"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,
|
"totalLocations": self.multiworld.total_locations[self.player].value,
|
||||||
"totalRevivals": self.multiworld.total_revivals[self.player].value,
|
"totalRevivals": self.multiworld.total_revivals[self.player].value,
|
||||||
"startWithDio": self.multiworld.start_with_revive[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):
|
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)
|
goal_text = generate_goal_text(world, player)
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ class SpireWorld(World):
|
||||||
|
|
||||||
def _get_slot_data(self):
|
def _get_slot_data(self):
|
||||||
return {
|
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],
|
'character': self.multiworld.character[self.player],
|
||||||
'ascension': self.multiworld.ascension[self.player],
|
'ascension': self.multiworld.ascension[self.player],
|
||||||
'heart_run': self.multiworld.heart_run[self.player]
|
'heart_run': self.multiworld.heart_run[self.player]
|
||||||
|
|
Loading…
Reference in New Issue