diff --git a/MultiServer.py b/MultiServer.py index f1fbce85..f92077ad 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -235,11 +235,11 @@ class Context: with open(multidatapath, 'rb') as f: data = f.read() - self._load(self._decompress(data), use_embedded_server_options) + self._load(self.decompress(data), use_embedded_server_options) self.data_filename = multidatapath @staticmethod - def _decompress(data: bytes) -> dict: + def decompress(data: bytes) -> dict: format_version = data[0] if format_version != 1: raise Exception("Incompatible multidata.") diff --git a/SNIClient.py b/SNIClient.py index f6509938..6215ffad 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -523,10 +523,13 @@ def launch_sni(ctx: Context): if not os.path.isdir(sni_path): sni_path = Utils.local_path(sni_path) if os.path.isdir(sni_path): - for file in os.listdir(sni_path): - lower_file = file.lower() - if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni": - sni_path = os.path.join(sni_path, file) + dir_entry: os.DirEntry + for dir_entry in os.scandir(sni_path): + if dir_entry.is_file(): + lower_file = dir_entry.name.lower() + if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or (lower_file == "sni"): + sni_path = dir_entry.path + break if os.path.isfile(sni_path): snes_logger.info(f"Attempting to start {sni_path}") diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index cfc5de81..a91ee51e 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -76,7 +76,7 @@ class WebHostContext(Context): else: self.port = get_random_port() - return self._load(self._decompress(room.seed.multidata), True) + return self._load(self.decompress(room.seed.multidata), True) @db_session def init_save(self, enabled: bool = True): diff --git a/WebHostLib/options.py b/WebHostLib/options.py index a1c7b0df..8dff42cd 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -37,8 +37,6 @@ def create(): } for game_name, world in AutoWorldRegister.world_types.items(): - if (world.hidden): - continue all_options = {**world.options, **Options.per_game_common_options} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( @@ -99,13 +97,14 @@ def create(): os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True) with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: - f.write(json.dumps(player_settings, indent=2, separators=(',', ': '))) + json.dump(player_settings, f, indent=2, separators=(',', ': ')) - weighted_settings["baseOptions"]["game"][game_name] = 0 - weighted_settings["games"][game_name] = {} - weighted_settings["games"][game_name]["gameSettings"] = game_options - weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_name_to_id.keys()) - weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_name_to_id.keys()) + if not world.hidden: + weighted_settings["baseOptions"]["game"][game_name] = 0 + weighted_settings["games"][game_name] = {} + weighted_settings["games"][game_name]["gameSettings"] = game_options + weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names) + weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names) with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f: - f.write(json.dumps(weighted_settings, indent=2, separators=(',', ': '))) + json.dump(weighted_settings, f, indent=2, separators=(',', ': ')) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 228bf375..d530f9e0 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -252,7 +252,7 @@ def get_static_room_data(room: Room): result = _multidata_cache.get(room.seed.id, None) if result: return result - multidata = Context._decompress(room.seed.multidata) + multidata = Context.decompress(room.seed.multidata) # in > 100 players this can take a bit of time and is the main reason for the cache locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations'] names: Dict[int, Dict[int, str]] = multidata["names"] diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 7095d7d0..cdd7e315 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -67,7 +67,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s multidata = None if multidata: - decompressed_multidata = MultiServer.Context._decompress(multidata) + decompressed_multidata = MultiServer.Context.decompress(multidata) player_names = {slot.player_name for slot in slots} leftover_names = [(name, index) for index, name in enumerate((name for name in decompressed_multidata["names"][0]), start=1)] @@ -100,7 +100,7 @@ def uploads(): if file.filename == '': flash('No selected file') elif file and allowed_file(file.filename): - if file.filename.endswith(".zip"): + if zipfile.is_zipfile(file.filename): with zipfile.ZipFile(file, 'r') as zfile: res = upload_zip_to_db(zfile) if type(res) == str: @@ -108,12 +108,12 @@ def uploads(): elif res: return redirect(url_for("view_seed", seed=res.id)) else: + # noinspection PyBroadException try: multidata = file.read() - MultiServer.Context._decompress(multidata) + MultiServer.Context.decompress(multidata) except: flash("Could not load multidata. File may be corrupted or incompatible.") - raise else: seed = Seed(multidata=multidata, owner=session["_id"]) flush() # place into DB and generate ids diff --git a/docs/network protocol.md b/docs/network protocol.md index 355bfdfd..7919a34e 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -55,13 +55,13 @@ Sent to clients when they connect to an Archipelago server. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. | +| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room.| -| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". | +| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". | | hint_cost | int | The amount of points it costs to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. || -| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. | +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. | | games | list\[str\] | sorted list of game names for the players, so first player's game will be games\[0\]. Matches game names in datapackage. | | datapackage_version | int | Data version of the [data package](#Data-Package-Contents) the server will send. Used to update the client's (optional) local cache. | | datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. | @@ -114,7 +114,7 @@ Sent to clients when the connection handshake is successfully completed. | ---- | ---- | ----- | | team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. | | slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. | -| players | list\[NetworkPlayer\] | List denoting other players in the multiworld, whether connected or not. See [NetworkPlayer](#NetworkPlayer) for info on the format. | +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. | | missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. | | checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. | | slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. | @@ -125,14 +125,14 @@ Sent to clients when they receive an item. | Name | Type | Notes | | ---- | ---- | ----- | | index | int | The next empty slot in the list of items for the receiving client. | -| items | list\[NetworkItem\] | The items which the client is receiving. See [NetworkItem](#NetworkItem) for more details. | +| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. | ### LocationInfo Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted. #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| locations | list\[NetworkItem\] | Contains list of item(s) in the location(s) scouted. See [NetworkItem](#NetworkItem) for more details. | +| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. | ### RoomUpdate Sent when there is a need to update information about the present game session. Generally useful for async games. @@ -143,7 +143,7 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: | Name | Type | Notes | | ---- | ---- | ----- | | hint_points | int | New argument. The client's current hint points. | -| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. | +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Changed argument. Always sends all players, whether connected or not. | | checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | | missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. | @@ -161,10 +161,10 @@ Sent to clients purely to display a message to the player. This packet differs f #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| data | list\[JSONMessagePart\] | See [JSONMessagePart](#JSONMessagePart) for more details on this type. | +| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. | | type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. | | receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. | -| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. | +| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. | | found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. | ### DataPackage @@ -173,7 +173,7 @@ Sent to clients to provide what is known as a 'data package' which contains info #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | -| data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) | +| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. | ### Bounced Sent to clients after a client requested this message be sent to them, more info in the Bounce package. @@ -213,7 +213,7 @@ Sent by the client to initiate a connection to an Archipelago game session. | game | str | The name of the game the client is playing. Example: `A Link to the Past` | | name | str | The player name for this client. | | uuid | str | Unique identifier for player client. | -| version | NetworkVersion | An object representing the Archipelago version this client supports. | +| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) | #### Authentication diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 74cf4c42..a1f474de 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -217,7 +217,34 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Left Side forest Caves', 'Cantoran', 1337176), ) - # 1337177 - 1337236 Reserved for future use + # 1337177 - 1337198 Lore Checks + if not world or is_option_enabled(world, player, "LoreChecks"): + location_table += ( + LocationData('Lower lake desolation', 'Memory - Coyote Jump (Time Messenger)', 1337177), + LocationData('Library', 'Memory - Waterway (A Message)', 1337178), + LocationData('Library top', 'Memory - Library Gap (Lachiemi Sun)', 1337179), + LocationData('Library top', 'Memory - Mr. Hat Portrait (Moonlit Night)', 1337180), + LocationData('Varndagroth tower left', 'Memory - Left Elevator (Nomads)', 1337181, lambda state: state.has('Elevator Keycard', player)), + LocationData('Varndagroth tower right (lower)', 'Memory - Siren Elevator (Childhood)', 1337182, lambda state: state._timespinner_has_keycard_B(world, player)), + LocationData('Varndagroth tower right (lower)', 'Memory - Varndagroth Right Bottom (Faron)', 1337183), + LocationData('Military Fortress', 'Memory - Bomber Climb (A Solution)', 1337184, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)), + LocationData('The lab', 'Memory - Genza\'s Secret Stash 1 (An Old Friend)', 1337185, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('The lab', 'Memory - Genza\'s Secret Stash 2 (Twilight Dinner)', 1337186, lambda state: state._timespinner_can_break_walls(world, player)), + LocationData('Emperors tower', 'Memory - Way Up There (Final Circle)', 1337187), + LocationData('Forest', 'Journal - Forest Rats (Lachiem Expedition)', 1337188), + LocationData('Forest', 'Journal - Forest Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)), + LocationData('Castle Ramparts', 'Journal - Floating in Moat (Prime Edicts)', 1337190), + LocationData('Castle Ramparts', 'Journal - Archer + Knight (Declaration of Independence)', 1337191), + LocationData('Castle Keep', 'Journal - Under the Twins (Letter of Reference)', 1337192), + LocationData('Castle Keep', 'Journal - Castle Loop Giantess (Political Advice)', 1337193), + LocationData('Royal towers (lower)', 'Journal - Aleana\'s Room (Diplomatic Missive)', 1337194, lambda state: state._timespinner_has_pink(world, player)), + LocationData('Royal towers (upper)', 'Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195), + LocationData('Royal towers (upper)', 'Journal - Aleana Boss (Stained Letter)', 1337196), + LocationData('Royal towers', 'Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197), + LocationData('Caves of Banishment (Maw)', 'Journal - Lower Left Maw Caves (Naivety)', 1337198) + ) + + # 1337199 - 1337236 Reserved for future use # 1337237 - 1337245 GyreArchives if not world or is_option_enabled(world, player, "GyreArchives"): diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index da3cff08..9bdc689e 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -50,6 +50,10 @@ class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" +class LoreChecks(Toggle): + "Memories and journal entries contain items." + display_name = "Lore Checks" + class DamageRando(Toggle): "Each orb has a high chance of having lower base damage and a low chance of having much higher base damage." display_name = "Damage Rando" @@ -68,6 +72,7 @@ timespinner_options: Dict[str, Toggle] = { #"StinkyMaw": StinkyMaw, "GyreArchives": GyreArchives, "Cantoran": Cantoran, + "LoreChecks": LoreChecks, "DamageRando": DamageRando, "DeathLink": DeathLink, } diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cba17c95..0e9d7a36 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -18,7 +18,7 @@ class TimespinnerWorld(World): game = "Timespinner" topology_present = True remote_items = False - data_version = 5 + data_version = 6 item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)}