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:
Fabian Dill 2023-02-02 01:14:23 +01:00 committed by GitHub
parent 4de7ebd8b0
commit 995c978628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 51 additions and 29 deletions

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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()):

View File

@ -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):

View File

@ -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,

View File

@ -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]

View File

@ -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."""

View File

@ -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

View File

@ -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]:

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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'])

View 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))

View File

@ -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,

View File

@ -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)

View File

@ -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]