From 449bc9330702244ed739a5b3e5142e2e8bdfc65a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 16 Jul 2022 19:36:14 +0200 Subject: [PATCH 1/6] Rogue Legacy: obliterate any outdated remnants before installer adds new files --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 1dee01af188..1005cadad0f 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -129,6 +129,7 @@ Type: dirifempty; Name: "{app}" [InstallDelete] Type: files; Name: "{app}\ArchipelagoLttPClient.exe" +Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*" [Registry] From 74b19dc1f55cef020e9a79be26a5ec404384e0cc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 7 Jul 2022 01:38:50 +0200 Subject: [PATCH 2/6] WebHost: cleanup generate and hopefully fix SQL concurrency problems --- WebHostLib/autolauncher.py | 4 +++- WebHostLib/generate.py | 38 ++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 9d7b7f4959d..6f978211fb7 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -154,8 +154,10 @@ def autogen(config: dict): while 1: time.sleep(0.1) with db_session: + # for update locks the database row(s) during transaction, preventing writes from elsewhere to_start = select( - generation for generation in Generation if generation.state == STATE_QUEUED) + generation for generation in Generation + if generation.state == STATE_QUEUED).for_update() for generation in to_start: launch_generator(generator_pool, generation) except AlreadyRunningException: diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index c33d2648a70..15067e131bd 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -4,7 +4,7 @@ import random import json import zipfile from collections import Counter -from typing import Dict, Optional as TypeOptional +from typing import Dict, Optional, Any from Utils import __version__ from flask import request, flash, redirect, url_for, session, render_template @@ -15,7 +15,7 @@ from BaseClasses import seeddigits, get_seed from Generate import handle_name, PlandoSettings import pickle -from .models import * +from .models import Generation, STATE_ERROR, STATE_QUEUED, commit, db_session, Seed, UUID from WebHostLib import app from .check import get_yaml_data, roll_options from .upload import upload_zip_to_db @@ -30,16 +30,15 @@ def get_meta(options_source: dict) -> dict: } plando_options -= {""} - meta = { + server_options = { "hint_cost": int(options_source.get("hint_cost", 10)), "forfeit_mode": options_source.get("forfeit_mode", "goal"), "remaining_mode": options_source.get("remaining_mode", "disabled"), "collect_mode": options_source.get("collect_mode", "disabled"), "item_cheat": bool(int(options_source.get("item_cheat", 1))), "server_password": options_source.get("server_password", None), - "plando_options": list(plando_options) } - return meta + return {"server_options": server_options, "plando_options": list(plando_options)} @app.route('/generate', methods=['GET', 'POST']) @@ -60,13 +59,13 @@ def generate(race=False): results, gen_options = roll_options(options, meta["plando_options"]) if race: - meta["item_cheat"] = False - meta["remaining_mode"] = "disabled" + meta["server_options"]["item_cheat"] = False + meta["server_options"]["remaining_mode"] = "disabled" if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) elif len(gen_options) > app.config["MAX_ROLL"]: - flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. " + flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " f"If you have a larger group, please generate it yourself and upload it.") elif len(gen_options) >= app.config["JOB_THRESHOLD"]: gen = Generation( @@ -92,23 +91,22 @@ def generate(race=False): return render_template("generate.html", race=race, version=__version__) -def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None): +def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None): if not meta: - meta: Dict[str, object] = {} + meta: Dict[str, Any] = {} + + meta.setdefault("server_options", {}).setdefault("hint_cost", 10) + race = meta.setdefault("race", False) - meta.setdefault("hint_cost", 10) - race = meta.get("race", False) - del (meta["race"]) - plando_options = meta.get("plando", {"bosses", "items", "connections", "texts"}) - del (meta["plando_options"]) try: target = tempfile.TemporaryDirectory() playercount = len(gen_options) seed = get_seed() - random.seed(seed) if race: - random.seed() # reset to time-based random source + random.seed() # use time-based random source + else: + random.seed(seed) seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)) @@ -120,7 +118,8 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.plando_options = PlandoSettings.from_set(plando_options) + erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options", + {"bosses", "items", "connections", "texts"})) name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): @@ -136,7 +135,7 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No erargs.name[player] = handle_name(erargs.name[player], player, name_counter) if len(set(erargs.name.values())) != len(erargs.name): raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}") - ERmain(erargs, seed, baked_server_options=meta) + ERmain(erargs, seed, baked_server_options=meta["server_options"]) return upload_to_db(target.name, sid, owner, race) except BaseException as e: @@ -148,7 +147,6 @@ def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=No meta = json.loads(gen.meta) meta["error"] = (e.__class__.__name__ + ": " + str(e)) gen.meta = json.dumps(meta) - commit() raise From b3ad76668069bf004b5854391406ae84c0faaa47 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 16 Jul 2022 13:47:26 -0400 Subject: [PATCH 3/6] SMZ3: Item link support (#756) * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions) * first working single-world randomized SM rom patches * - SM now displays message when getting an item outside for someone else (fills ROM item table) This is dependant on modifications done to sm_randomizer_rom project * First working MultiWorld SM * some missing things: - player name inject in ROM and get in client - end game get from ROM in client - send self item to server - add player names table in ROM * replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better) * - reenabled balancing * post rebase fixes * updated SmClient.py * + added VariaRandomizer LICENSE * + added sm_randomizer_rom project (which builds sm.ips) * Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning * properly revert change made to CollectionState and more cleaning * Fixed multiworld support patch not working with VariaRandomizer's * missing file commit * Fixed syntax error in unused code to satisfy Linter * Revert "Fixed multiworld support patch not working with VariaRandomizer's" This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b. * many fixes and improovement - fixed seeded generation - fixed broken logic when more than one SM world - added missing rules for inter-area transitions - added basic patch presence for logic - added DoorManager init call to reflect present patches for logic - moved CollectionState addition out of BaseClasses into SM world - added condition to apply progitempool presorting only if SM world is present - set Bosses item id to None to prevent them going into multidata - now use get_game_players * Fixed multiworld support patch not working with VariaRandomizer's Added stage_fill_hook to set morph first in progitempool Added back VariaRandomizer's standard patches * + added missing files from variaRandomizer project * + added missing variaRandomizer files (custom sprites) + started integrating VariaRandomizer options (WIP) * Some fixes for player and server name display - fixed player name of 16 characters reading too far in SM client - fixed 12 bytes SM player name limit (now 16) - fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO) - request: temporarly changed default seed names displayed in SM main menu to OWTCH * Fixed Goal completion not triggering in smClient * integrated VariaRandomizer's options into AP (WIP) - startAP is working - door rando is working - skillset is working * - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off") * skillset are now instanced per player instead of being a singleton class * RomPatches are now instanced per player instead of being a singleton class * DoorManager is now instanced per player instead of being a singleton class * - fixed the last bugs that prevented generation of >1 SM world * fixed crash when no skillset preset is specified in randoPreset (default to "casual") * maxDifficulty support and itemsounds removal - added support for maxDifficulty - removed itemsounds patch as its always applied from multiworld patch for now * Fixed bad merge * Post merge adaptation * fixed player name length fix that got lost with the merge * fixed generation with other game type than SM * added default randoPreset json for SM in playerSettings.yaml * fixed broken SM client following merge * beautified json skillset presets * Fixed ArchipelagoSmClient not building * Fixed conflict between mutliworld patch and beam_doors_plms patch - doorsColorsRando now working * SM generation now outputs APBP - Fixed paths for patches and presets when frozen * added missing file and fixed multithreading issue * temporarily set data_version = 0 * more work - added support for AP starting items - fixed client crash with gamemode being None - patch.py "compatible_version" is now 3 * commited missing asm files fixed start item reserve breaking game (was using bad write offset when patching) * Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it). fixed crash in SMClient when loosing connection to SNI * fixed No Energy Item missing its ID fixed Plando * merge post fixes * fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color) * fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses) * fixed start item x-ray HUD display * Fixed start items being sent by the server (is all handled in ROM) Start items are now not removed from itempool anymore Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though. Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified * fixed settings that could be applied to any SM players * fixed auth to server only using player name (now does as ALTTP to authenticate) * - fixed End Credits broken text * added non SM item name display * added all supported SM options in playerSettings.yaml * fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region did some cleaning (mainly reverts on unnecessary core classes * minor setting fixes and tweaks - merged Area and lightArea settings - made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating - fixed inverted layoutPatch setting * added option start_inventory_removes_from_pool fixed option names formatting fixed lint errors small code and repo cleanup * Hopefully fixed ROR2 that could not send any items * - fixed missing required change to ROR2 * fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum) * fixed typo with doors_colors_rando * fixed checksum * added custom sprites for off-world items (progression or not) the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu * - added missing change following upstream merge - changed patch filename extension from apbp to apm3 so patch can be used with the new client * added morph placement options: early means local and sphere 1 * fixed failing unit tests * - fixed broken custom_preset options * - big cleanup to remove unnecessary or unsupported features * - more cleanup * - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips - small cleanup * - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch) * fixed g4_skip patch that can be not applied if hud is enabled * - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette) * - updated basepatch to reflect g4_skip removal - moved more asm files to SMBasepatch project * - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed) * fixed wrong path if using built as exe * - cleaned exposed maxDifficulty options - removed always enabled Knows * Merged LttPClient and SMClient into SNIClient * added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service * small doc precision * - added death_link support - fixed broken Goal Completion - post merge fix * - removed now useless presets * - fixed bad internal mapping with maxDiff - increases maxDiff if only Bosses is preventing beating the game * - added support for lowercase custom preset sections (knows, settings and controller) - fixed controller settings not applying to ROM * - fixed death loop when dying with Door rando, bomb or speed booster as starting items - varia's backup save should now be usable (automatically enabled when doing door rando) * -added docstring for generated yaml * fixed bad merge * fixed broken infinity max difficulty * commented debug prints * adjusted credits to mark progression speed and difficulty as Non Available * added support for more than 255 players (will print Archipelago for higher player number) * fixed missing cleanup * added support for 65535 different player names in ROM * fixed generations failing when only bosses are unreachable * - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish * fixed failling generations when using 'fun' settings Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings * fixed debug logger * removed unsupported "suits_restriction" option * fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP) * - fixed deathlink emptying reserves - added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves * - merged death_link and death_link_survive options * fixed death_link * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression * - enabled local item dialog boxes for dungeon and keycard items when keysanity is used * - fixed ItemLink support * fixed shops sending checks * Added get_filler_item_name() returning a random junk item Co-authored-by: Fabian Dill --- worlds/smz3/TotalSMZ3/Patch.py | 1 + worlds/smz3/__init__.py | 24 +++++++++++++++++------- worlds/smz3/data/zsm.ips | Bin 1460417 -> 1460427 bytes 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 54395714ba9..d029e58473f 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -619,6 +619,7 @@ class Patch: if (self.myWorld.Config.Keysanity): self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass + self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items def WriteSMKeyCardDoors(self): if (not self.myWorld.Config.Keysanity): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 7f05e0dfd52..e440eab2c9a 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -27,14 +27,18 @@ class SMZ3CollectionState(metaclass=AutoLogicRegister): # for unit tests where MultiWorld is instantiated before worlds if hasattr(parent, "state"): self.smz3state = {player: TotalSMZ3Item.Progression([]) for player in parent.get_game_players("SMZ3")} + for player, group in parent.groups.items(): + if (group["game"] == "SMZ3"): + self.smz3state[player] = TotalSMZ3Item.Progression([]) + if player not in parent.state.smz3state: + parent.state.smz3state[player] = TotalSMZ3Item.Progression([]) else: self.smz3state = {} def copy_mixin(self, ret) -> CollectionState: - ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.world.get_game_players("SMZ3")} + ret.smz3state = {player: copy.deepcopy(self.smz3state[player]) for player in self.smz3state} return ret - class SMZ3Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -106,6 +110,7 @@ class SMZ3World(World): niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World) junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World) allJunkItems = niceItems + junkItems + self.junkItemsNames = [item.Type.name for item in junkItems] if (self.smz3World.Config.Keysanity): progressionItems = self.progression + self.dungeon + self.keyCardsItems @@ -256,11 +261,11 @@ class SMZ3World(World): base_combined_rom = basepatch.apply(base_combined_rom) patcher = TotalSMZ3Patch(self.smz3World, - [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World)], + [world.smz3World for key, world in self.world.worlds.items() if isinstance(world, SMZ3World) and hasattr(world, "smz3World")], self.world.seed_name, self.world.seed, self.local_random, - self.world.world_name_lookup, + {v: k for k, v in self.world.player_name.items()}, next(iter(loc.player for loc in self.world.get_locations() if (loc.item.name == "SilverArrows" and loc.item.player == self.player)))) patches = patcher.Create(self.smz3World.Config) patches.update(self.apply_sm_custom_sprite()) @@ -312,7 +317,7 @@ class SMZ3World(World): return slot_data def collect(self, state: CollectionState, item: Item) -> bool: - state.smz3state[item.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: state.prog_items[item.name, item.player] += 1 return True # indicate that a logical state change has occured @@ -321,7 +326,7 @@ class SMZ3World(World): def remove(self, state: CollectionState, item: Item) -> bool: name = self.collect_item(state, item, True) if name: - state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World)]) + state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) state.prog_items[name, item.player] -= 1 if state.prog_items[name, item.player] < 1: del (state.prog_items[name, item.player]) @@ -330,7 +335,9 @@ class SMZ3World(World): def create_item(self, name: str) -> Item: return SMZ3Item(name, ItemClassification.progression, - TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], player = self.player) + TotalSMZ3Item.ItemType[name], self.item_name_to_id[name], + self.player, + TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[name], self)) def pre_fill(self): from Fill import fill_restrictive @@ -364,6 +371,9 @@ class SMZ3World(World): else: return [] + def get_filler_item_name(self) -> str: + return self.world.random.choice(self.junkItemsNames) + def write_spoiler(self, spoiler_handle: TextIO): self.world.spoiler.unreachables.update(self.unreachable) diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index faf7443a573a49fd7eab0f80588382f043753394..6faeeaa2fb4bb4f2b29203806e3d43e91be85d55 100644 GIT binary patch delta 695 zcmZvXQD{WL|T;kYFxez7IGMG?dU?8>&yH-$vt)7G^Xb?RW6-rF6SCFaT zKI|!ai0whPDEG~WA@&fvi%5c?hY0d1R-l>GoJAeCMG<@WopZkb_ve4UubK_N$cAH2 zAv6M$^wbErp#O5QHSo+M1Jl&V4AXSS4%a;-;RTnr?%`gV*Xkbn#0w=w*xfC@RXFwC zG}aI6vbAP$=DV+^8GlAIM;JfK_lh7N*hZcPd3YN+3bMN=lj;$$+WyZv1M=~W+Xmi3 zo~n<65p3;F3KxBHM%QP5O=V6;V-`75Gjj9PeGC*jkccOMZi$=>z(W|Ps{zOYz)pS+vK*9%bBH}*##fA8 zNy^I0lVXvL+ zqd^eJEg$OuK`i)KTJRaJkD{cD8GO@;GC>4Hu|8YrGbNo8-RP zbjTunO^rT(&!{Rw3x`f`pzq8kKIRhIe8S2omqOSbjIu0z;}iSv+|!4*bT6YR3nY{? z+0P5#dGRXRE`8yX0`|KNJ~@tkqRA(tjE0L$F0Rh+=aaM-)#8(lO^>AA^hgJLk!!p( v#;CaP!XhtSMB$uR&6#L5XZEXgkxokJx)>WLiTZ*W*@ak;eyT6*UYPq0`-&C# delta 782 zcmZvXUr1AN6vuyOE;rYmyK5@Sv|VoxwbXn_k;G_mW$cocBZx+O^Tp^7LG)tb9nsu& z&D{3j#n&uBMAk$3Q$j{Qn7ryhyp}s_#a+Al!YrTSCSf>TgG=74R4E&Ds|lfX^b>!pkui}(90UK^P%-MAn;H3rGLAoAPB$(C4j zDJg}fN@a@^`FHTiOCciu^=MI}@@W Date: Sat, 16 Jul 2022 16:45:40 +0200 Subject: [PATCH 4/6] Subnautica: add creature scans --- worlds/subnautica/Creatures.py | 82 ++++++++++++++++++++++++++++++++ worlds/subnautica/Items.py | 2 +- worlds/subnautica/Options.py | 11 ++++- worlds/subnautica/Rules.py | 86 ++++++++++++++++++++++------------ worlds/subnautica/__init__.py | 19 ++++++-- 5 files changed, 164 insertions(+), 36 deletions(-) create mode 100644 worlds/subnautica/Creatures.py diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py new file mode 100644 index 00000000000..56e2a7efa1d --- /dev/null +++ b/worlds/subnautica/Creatures.py @@ -0,0 +1,82 @@ +from typing import Dict, Set, List + +# EN Locale Creature Name to rough depth in meters found at +all_creatures: Dict[str, int] = { + "Gasopod": 0, + "Bladderfish": 0, + "Ancient Floater": 0, + "Skyray": 0, + "Garryfish": 0, + "Peeper": 0, + "Shuttlebug": 0, + "Rabbit Ray": 0, + "Stalker": 0, + "Floater": 0, + "Holefish": 0, + "Cave Crawler": 0, + "Hoopfish": 0, + "Crashfish": 0, + "Hoverfish": 0, + "Spadefish": 0, + "Reefback Leviathan": 0, + "Reaper Leviathan": 0, + "Warper": 0, + "Boomerang": 0, + "Biter": 200, + "Sand Shark": 200, + "Bleeder": 200, + "Crabsnake": 300, + "Jellyray": 300, + "Oculus": 300, + "Mesmer": 300, + "Eyeye": 300, + "Reginald": 400, + "Sea Treader Leviathan": 400, + "Crabsquid": 400, + "Ampeel": 400, + "Boneshark": 400, + "Rockgrub": 400, + "Ghost Leviathan": 500, + "Ghost Leviathan Juvenile": 500, + "Spinefish": 600, + "Blighter": 600, + "Blood Crawler": 600, + "Ghostray": 1000, + "Amoeboid": 1000, + "River Prowler": 1000, + "Red Eyeye": 1300, + "Magmarang": 1300, + "Crimson Ray": 1300, + "Lava Larva": 1300, + "Lava Lizard": 1300, + "Sea Dragon Leviathan": 1300, + "Sea Emperor Leviathan": 1700, + "Sea Emperor Juvenile": 1700, + + # "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes) +} + +# be nice and make these require Stasis Rifle +aggressive: Set[str] = { + "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency + "Crashfish", + "Bleeder", + "Mesmer", + "Reaper Leviathan", + "Crabsquid", + "Warper", + "Crabsnake", + "Ampeel", + "Boneshark", + "Lava Lizard", + "Sea Dragon Leviathan", + "River Prowler", +} + +suffix: str = " Scan" + +creature_locations: Dict[str, int] = { + creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) +} + +all_creatures_presorted: List[str] = sorted(all_creatures) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index b55efe24535..f3a6ded5aac 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -166,7 +166,7 @@ item_table: Dict[int, ItemDict] = { 'count': 5, 'name': 'Seamoth Fragment', 'tech_type': 'SeamothFragment'}, - 35039: {'classification': ItemClassification.useful, + 35039: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Stasis Rifle Fragment', 'tech_type': 'StasisRifleFragment'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index cae7ba6c0e4..b5dc2241fb0 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,5 @@ -from Options import Choice +from Options import Choice, Range +from .Creatures import all_creatures class ItemPool(Choice): @@ -31,7 +32,15 @@ class Goal(Choice): }[self.value] +class CreatureScans(Range): + """Place items on specific creature scans. + Warning: Includes aggressive Leviathans.""" + display_name = "Creature Scans" + range_end = len(all_creatures) + + options = { "item_pool": ItemPool, "goal": Goal, + "creature_scans": CreatureScans } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 131a537f047..b8f8f1a7b48 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,112 +1,122 @@ +from typing import TYPE_CHECKING + from worlds.generic.Rules import set_rule from .Locations import location_table, LocationDict +from .Creatures import all_creatures, aggressive, suffix import math +if TYPE_CHECKING: + from . import SubnauticaWorld -def has_seaglide(state, player): + +def has_seaglide(state, player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player): +def has_modification_station(state, player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player): +def has_mobile_vehicle_bay(state, player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player): +def has_moonpool(state, player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player): +def has_vehicle_upgrade_console(state, player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player): +def has_seamoth(state, player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player): +def has_seamoth_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player): +def has_seamoth_depth_module_mk2(state, player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player): +def has_seamoth_depth_module_mk3(state, player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player): +def has_cyclops_bridge(state, player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player): +def has_cyclops_engine(state, player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player): +def has_cyclops_hull(state, player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player): +def has_cyclops(state, player: int): return has_cyclops_bridge(state, player) and \ has_cyclops_engine(state, player) and \ has_cyclops_hull(state, player) and \ has_mobile_vehicle_bay(state, player) -def has_cyclops_depth_module_mk1(state, player): +def has_cyclops_depth_module_mk1(state, player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player): +def has_cyclops_depth_module_mk2(state, player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player): +def has_cyclops_depth_module_mk3(state, player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player): +def has_prawn(state, player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player): +def has_praw_propulsion_arm(state, player: int): return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state, player): +def has_prawn_depth_module_mk1(state, player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player): +def has_prawn_depth_module_mk2(state, player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player): +def has_laser_cutter(state, player: int): return state.has("Laser Cutter Fragment", player, 3) +def has_stasis_rile(state, player: int): + return state.has("Stasis Rifle Fragment", player, 2) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player): +def has_propulsion_cannon(state, player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) -def has_cyclops_shield(state, player): +def has_cyclops_shield(state, player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -119,7 +129,7 @@ def has_cyclops_shield(state, player): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player): +def get_max_swim_depth(state, player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -130,7 +140,7 @@ def get_max_swim_depth(state, player): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player): + # if has_seaglide(state, player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -146,7 +156,7 @@ def get_max_swim_depth(state, player): # return max_depth -def get_seamoth_max_depth(state, player): +def get_seamoth_max_depth(state, player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -186,7 +196,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player): +def get_max_depth(state, player: int): # TODO, Difficulty option, we can add vehicle depth + swim depth # But at this point, we have to consider traver distance in caves, not # just depth @@ -196,7 +206,7 @@ def get_max_depth(state, player): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict): +def can_access_location(state, player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): return False @@ -225,17 +235,33 @@ def can_access_location(state, player: int, loc: LocationDict): return get_max_depth(state, player) >= depth -def set_location_rule(world, player, loc): +def set_location_rule(world, player: int, loc: LocationDict): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def set_rules(subnautica_world): +def can_scan_creature(state, player: int, creature: str) -> bool: + if not has_seaglide(state, player): + return False + if creature in aggressive and not has_stasis_rile(state, player): + return False + return get_max_depth(state, player) >= all_creatures[creature] + + +def set_creature_rule(world, player, creature_name: str): + set_rule(world.get_location(creature_name + suffix, player), + lambda state: can_scan_creature(state, player, creature_name)) + + +def set_rules(subnautica_world: "SubnauticaWorld"): player = subnautica_world.player world = subnautica_world.world for loc in location_table.values(): set_location_rule(world, player, loc) + for creature_name in subnautica_world.creatures_to_scan: + set_creature_rule(world, player, creature_name) + # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: get_max_depth(state, player) >= 1444 and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index f2fa5497cf0..9ad4feb1a40 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -5,6 +5,7 @@ from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassifi from worlds.AutoWorld import World, WebWorld from . import Items from . import Locations +from . import Creatures from . import Options from .Items import item_table from .Rules import set_rules @@ -23,6 +24,10 @@ class SubnaticaWeb(WebWorld): )] +all_locations = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} +all_locations.update(Creatures.creature_locations) + + class SubnauticaWorld(World): """ Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by @@ -33,25 +38,30 @@ class SubnauticaWorld(World): web = SubnaticaWeb() item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()} - location_name_to_id = {data["name"]: loc_id for loc_id, data in Locations.location_table.items()} + location_name_to_id = all_locations options = Options.options - data_version = 2 + data_version = 3 required_client_version = (0, 3, 3) prefill_items: List[Item] + creatures_to_scan: List[str] def generate_early(self) -> None: self.prefill_items = [ self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] + self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + self.world.creature_scans[self.player].value) def create_regions(self): self.world.regions += [ self.create_region("Menu", None, ["Lifepod 5"]), self.create_region("Planet 4546B", - Locations.events + [location["name"] for location in Locations.location_table.values()]) + Locations.events + + [location["name"] for location in Locations.location_table.values()] + + [creature+Creatures.suffix for creature in self.creatures_to_scan]) ] # refer to Rules.py @@ -64,7 +74,7 @@ class SubnauticaWorld(World): # Generate item pool pool = [] neptune_launch_platform = None - extras = 0 + extras = self.world.creature_scans[self.player].value valuable = self.world.item_pool[self.player] == Options.ItemPool.option_valuable for item in item_table.values(): for i in range(item["count"]): @@ -105,6 +115,7 @@ class SubnauticaWorld(World): slot_data: Dict[str, Any] = { "goal": goal.current_key, "vanilla_tech": vanilla_tech, + "creatures_to_scan": self.creatures_to_scan } return slot_data From 9897f4eb4bea540996c0d75cb25b6b117dfd2093 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 16 Jul 2022 13:56:23 -0400 Subject: [PATCH 5/6] LTTP: Yaml Update (#765) removes vendor option from hints, adds scam setting, and adds P option to shop shuffle. --- playerSettings.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/playerSettings.yaml b/playerSettings.yaml index ff3596a77a4..4ebae9e6d71 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -175,12 +175,15 @@ A Link to the Past: retro_caves: on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion. off: 50 - hints: # Vendors: King Zora and Bottle Merchant say what they're selling. - # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. + hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. 'on': 50 - vendors: 0 'off': 0 full: 0 + scams: # If on, these Merchants will no longer tell you what they're selling. + 'off': 50 + 'king_zora': 0 + 'bottle_merchant': 0 + 'all': 0 swordless: on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change off: 1 @@ -273,6 +276,7 @@ A Link to the Past: p: 0 # Randomize the prices of the items in shop inventories u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too + P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees ip: 0 # Shuffle inventories and randomize prices fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool From 828bcb12661fffd3e65be59fa20706d0c36347d6 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sat, 16 Jul 2022 14:00:00 -0400 Subject: [PATCH 6/6] OoT: Fix gerudo_fortress on normal (#784) --- worlds/oot/ItemPool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 24dda8e24f8..301c502a7e8 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1088,10 +1088,10 @@ def get_pool_core(world): placed_items['Hideout Jail Guard (4 Torches)'] = 'Recovery Heart' skip_in_spoiler_locations.extend(['Hideout Jail Guard (2 Torches)', 'Hideout Jail Guard (3 Torches)', 'Hideout Jail Guard (4 Torches)']) else: - placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Gerudo Fortress)' - placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Gerudo Fortress)' + placed_items['Hideout Jail Guard (1 Torch)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (2 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (3 Torches)'] = 'Small Key (Thieves Hideout)' + placed_items['Hideout Jail Guard (4 Torches)'] = 'Small Key (Thieves Hideout)' if world.shuffle_gerudo_card and world.gerudo_fortress != 'open': pool.append('Gerudo Membership Card')