From 83bcb441bfc52cf842d7d1f35df9247246644da4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Aug 2022 19:22:03 +0200 Subject: [PATCH 01/62] Factorio: typo --- worlds/factorio/Mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 37c503b047..9889e58bf3 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -107,7 +107,7 @@ def generate_mod(world, output_directory: str): random = multiworld.slot_seeds[player] def flop_random(low, high, base=None): - """Guarentees 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.""" if base: distance = random.random() if random.randint(0, 1): From 9341332379adb02f87921067ffed1afd87f3343a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Aug 2022 00:58:46 +0200 Subject: [PATCH 02/62] WebHost: allow newlines in data-tooltip (#921) * WebHost: allow newlines in data-tooltip * WebHost: Tooltips: strip surrounding whitespace * WebHost: unify tooltips behaviour * WebHost: unify labels around tooltips * WebHost: changing tooltips width to max-width to allow small tooltips to not have empty space. * Minor modifications to tooltips - Reduce tooltip target to (?) spans - Set fixed width of 260px on tooltips - Add space between : and (?) on player-settings - Removed cursor:pointer on tooltips - Fix labels for checkboxes on generate.html Co-authored-by: Chris Wilson --- WebHostLib/options.py | 15 ++-- WebHostLib/static/assets/player-settings.js | 10 ++- WebHostLib/static/styles/generate.css | 4 -- WebHostLib/static/styles/globalStyles.css | 4 ++ WebHostLib/static/styles/tooltip.css | 4 +- WebHostLib/templates/generate.html | 79 ++++++++++----------- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 3c481be62b..e2d362a570 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -49,6 +49,11 @@ def create(): return list(default_value) return default_value + def get_html_doc(option_type: type(Options.Option)) -> str: + if not option_type.__doc__: + return "Please document me!" + return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip() + weighted_settings = { "baseOptions": { "description": "Generated by https://archipelago.gg/", @@ -88,7 +93,7 @@ def create(): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "defaultValue": None, "options": [] } @@ -114,7 +119,7 @@ def create(): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "defaultValue": option.default if hasattr( option, "default") and option.default != "random" else option.range_start, "min": option.range_start, @@ -131,14 +136,14 @@ def create(): game_options[option_name] = { "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), } elif getattr(option, "verify_location_name", False): game_options[option_name] = { "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), } elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): @@ -146,7 +151,7 @@ def create(): game_options[option_name] = { "type": "custom-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "options": list(option.valid_keys), } diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index 21c6414df7..b77d4e877b 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -102,9 +102,15 @@ const buildOptionsTable = (settings, romOpts = false) => { // td Left const tdl = document.createElement('td'); const label = document.createElement('label'); + label.textContent = `${settings[setting].displayName}: `; label.setAttribute('for', setting); - label.setAttribute('data-tooltip', settings[setting].description); - label.innerText = `${settings[setting].displayName}:`; + + const questionSpan = document.createElement('span'); + questionSpan.classList.add('interactive'); + questionSpan.setAttribute('data-tooltip', settings[setting].description); + questionSpan.innerText = '(?)'; + + label.appendChild(questionSpan); tdl.appendChild(label); tr.appendChild(tdl); diff --git a/WebHostLib/static/styles/generate.css b/WebHostLib/static/styles/generate.css index 066fb8a7c5..478d444d40 100644 --- a/WebHostLib/static/styles/generate.css +++ b/WebHostLib/static/styles/generate.css @@ -56,7 +56,3 @@ #file-input{ display: none; } - -.interactive{ - color: #ffef00; -} diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css index c20bab6b14..d8b10d1c50 100644 --- a/WebHostLib/static/styles/globalStyles.css +++ b/WebHostLib/static/styles/globalStyles.css @@ -105,3 +105,7 @@ h5, h6{ margin-bottom: 20px; background-color: #ffff00; } + +.interactive{ + color: #ffef00; +} \ No newline at end of file diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 0c5c0c6969..7cd8463f64 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -14,7 +14,6 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, /* Base styles for the element that has a tooltip */ [data-tooltip], .tooltip { position: relative; - cursor: pointer; } /* Base styles for the entire tooltip */ @@ -55,14 +54,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, /** Content styles */ .tooltip:after, [data-tooltip]:after { + width: 260px; z-index: 10000; padding: 8px; - width: 160px; border-radius: 4px; background-color: #000; background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); + white-space: pre-wrap; font-size: 14px; line-height: 1.2; } diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 916ed72b8d..aa16a47d35 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -41,12 +41,11 @@ - - (?) - + @@ -85,12 +83,11 @@ - - (?) - + @@ -131,12 +128,11 @@ - - (?) - + @@ -162,23 +158,22 @@ - - (?) + Plando Options: + + (?) - +
- +
- +
- + From be8c3131d8423106e52e3682d0d36f1ba10d5827 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Sat, 20 Aug 2022 16:02:50 -0700 Subject: [PATCH 03/62] fix allay advancements requiring note block on the wrong one. (#896) --- worlds/minecraft/Rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 2a88087d0c..ecdb459769 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -276,10 +276,10 @@ def set_advancement_rules(world: MultiWorld, player: int): # 1.19 advancements - # can make a cake, and can reach a pillager outposts for allays - set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player)) - # find allay and craft a noteblock - set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) + # can make a cake, and a noteblock, and can reach a pillager outposts for allays + set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) + # can get to outposts. + # set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True) # craft bucket and adventure to find frog spawning biome set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) # I don't like this one its way to easy to get. just a pain to find. From fb122df5f5b9e35d99adba6d381043bb05eb07c2 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 20 Aug 2022 18:09:35 -0500 Subject: [PATCH 04/62] RoR2: code cleanup and styling consistency (#833) * build locations dict dynamically from the TotalLocations option. Minor styling cleanup * Minor items styling cleanup. remove unused event items * minor options cleanup. clarify preset toggle slightly better * make items.py more readable. add chaos weights dict to use as reference point for generation * small rules styling and consistency cleanup * create less regions and other init cleanup * move region creation to less function calls and move revivals calculation * typing * use enum instead of hardcoded ints. fix bug i introduced * better typing --- worlds/ror2/Items.py | 245 ++++++++++++++++++++------------------- worlds/ror2/Locations.py | 16 +-- worlds/ror2/Options.py | 11 +- worlds/ror2/Rules.py | 23 ++-- worlds/ror2/__init__.py | 114 +++++++++--------- 5 files changed, 206 insertions(+), 203 deletions(-) diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py index c4ece1dcb3..9efbc713d8 100644 --- a/worlds/ror2/Items.py +++ b/worlds/ror2/Items.py @@ -1,143 +1,154 @@ +from typing import Dict from BaseClasses import Item -import typing +from .Options import ItemWeights + class RiskOfRainItem(Item): game: str = "Risk of Rain 2" + # 37000 - 38000 -item_table = { - "Dio's Best Friend": 37001, - "Common Item": 37002, - "Uncommon Item": 37003, - "Legendary Item": 37004, - "Boss Item": 37005, - "Lunar Item": 37006, - "Equipment": 37007, - "Item Scrap, White": 37008, - "Item Scrap, Green": 37009, - "Item Scrap, Red": 37010, - "Item Scrap, Yellow": 37011, - "Victory": None, - "Beat Level One": None, - "Beat Level Two": None, - "Beat Level Three": None, - "Beat Level Four": None, - "Beat Level Five": None, +item_table: Dict[str, int] = { + "Dio's Best Friend": 37001, + "Common Item": 37002, + "Uncommon Item": 37003, + "Legendary Item": 37004, + "Boss Item": 37005, + "Lunar Item": 37006, + "Equipment": 37007, + "Item Scrap, White": 37008, + "Item Scrap, Green": 37009, + "Item Scrap, Red": 37010, + "Item Scrap, Yellow": 37011 } -default_weights = { - "Item Scrap, Green": 16, - "Item Scrap, Red": 4, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 32, - "Common Item": 64, - "Uncommon Item": 32, - "Legendary Item": 8, - "Boss Item": 4, - "Lunar Item": 16, - "Equipment": 32 +default_weights: Dict[str, int] = { + "Item Scrap, Green": 16, + "Item Scrap, Red": 4, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 32, + "Common Item": 64, + "Uncommon Item": 32, + "Legendary Item": 8, + "Boss Item": 4, + "Lunar Item": 16, + "Equipment": 32 } -new_weights = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 75, - "Uncommon Item": 40, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 10, - "Equipment": 20 +new_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 75, + "Uncommon Item": 40, + "Legendary Item": 10, + "Boss Item": 5, + "Lunar Item": 10, + "Equipment": 20 } -uncommon_weights = { - "Item Scrap, Green": 45, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 45, - "Uncommon Item": 100, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 15, - "Equipment": 20 +uncommon_weights: Dict[str, int] = { + "Item Scrap, Green": 45, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 45, + "Uncommon Item": 100, + "Legendary Item": 10, + "Boss Item": 5, + "Lunar Item": 15, + "Equipment": 20 } -legendary_weights = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 50, - "Uncommon Item": 25, - "Legendary Item": 100, - "Boss Item": 5, - "Lunar Item": 15, - "Equipment": 20 +legendary_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 50, + "Uncommon Item": 25, + "Legendary Item": 100, + "Boss Item": 5, + "Lunar Item": 15, + "Equipment": 20 } -lunartic_weights = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 100, - "Equipment": 0 +lunartic_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Lunar Item": 100, + "Equipment": 0 } -no_scraps_weights = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 100, - "Uncommon Item": 40, - "Legendary Item": 15, - "Boss Item": 5, - "Lunar Item": 10, - "Equipment": 25 +chaos_weights: Dict[str, int] = { + "Item Scrap, Green": 80, + "Item Scrap, Red": 45, + "Item Scrap, Yellow": 30, + "Item Scrap, White": 100, + "Common Item": 100, + "Uncommon Item": 70, + "Legendary Item": 30, + "Boss Item": 20, + "Lunar Item": 60, + "Equipment": 40 } -even_weights = { - "Item Scrap, Green": 1, - "Item Scrap, Red": 1, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 1, - "Common Item": 1, - "Uncommon Item": 1, - "Legendary Item": 1, - "Boss Item": 1, - "Lunar Item": 1, - "Equipment": 1 +no_scraps_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 100, + "Uncommon Item": 40, + "Legendary Item": 15, + "Boss Item": 5, + "Lunar Item": 10, + "Equipment": 25 } -scraps_only = { - "Item Scrap, Green": 70, - "Item Scrap, White": 100, - "Item Scrap, Red": 30, - "Item Scrap, Yellow": 5, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Equipment": 0 +even_weights: Dict[str, int] = { + "Item Scrap, Green": 1, + "Item Scrap, Red": 1, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 1, + "Common Item": 1, + "Uncommon Item": 1, + "Legendary Item": 1, + "Boss Item": 1, + "Lunar Item": 1, + "Equipment": 1 } -item_pool_weights: typing.Dict[int, typing.Dict[str, int]] = { - 0: default_weights, - 1: new_weights, - 2: uncommon_weights, - 3: legendary_weights, - 4: lunartic_weights, - 6: no_scraps_weights, - 7: even_weights, - 8: scraps_only +scraps_only: Dict[str, int] = { + "Item Scrap, Green": 70, + "Item Scrap, White": 100, + "Item Scrap, Red": 30, + "Item Scrap, Yellow": 5, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Lunar Item": 0, + "Equipment": 0 } -lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id} +item_pool_weights: Dict[int, Dict[str, int]] = { + ItemWeights.option_default: default_weights, + ItemWeights.option_new: new_weights, + ItemWeights.option_uncommon: uncommon_weights, + ItemWeights.option_legendary: legendary_weights, + ItemWeights.option_lunartic: lunartic_weights, + ItemWeights.option_chaos: chaos_weights, + ItemWeights.option_no_scraps: no_scraps_weights, + ItemWeights.option_even: even_weights, + ItemWeights.option_scraps_only: scraps_only +} + +lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py index ae6ccea2b2..e4ebe8ddfa 100644 --- a/worlds/ror2/Locations.py +++ b/worlds/ror2/Locations.py @@ -1,19 +1,13 @@ +from typing import Dict from BaseClasses import Location -import typing +from .Options import TotalLocations + class RiskOfRainLocation(Location): game: str = "Risk of Rain 2" -# 37000 - 38000 -base_location_table = { - "Victory": None, -} # 37006 - 37506 -item_pickups = { - f"ItemPickup{i}": 37005+i for i in range(1, 501) +item_pickups: Dict[str, int] = { + f"ItemPickup{i+1}": 37000+i for i in range(TotalLocations.range_end) } - -location_table = {**base_location_table, **item_pickups} - -lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in location_table.items()} diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 727d01ffaa..a95cbf597a 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -1,4 +1,4 @@ -import typing +from typing import Dict from Options import Option, DefaultOnToggle, Range, Choice @@ -36,7 +36,8 @@ class AllowLunarItems(DefaultOnToggle): class StartWithRevive(DefaultOnToggle): """Start the game with a `Dio's Best Friend` item.""" display_name = "Start with a Revive" - + + class FinalStageDeath(DefaultOnToggle): """Death on the final boss stage counts as a win.""" display_name = "Final Stage Death is Win" @@ -124,7 +125,7 @@ class Equipment(Range): class ItemPoolPresetToggle(DefaultOnToggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" - display_name = "Item Weight Presets" + display_name = "Use Item Weight Presets" class ItemWeights(Choice): @@ -150,7 +151,7 @@ class ItemWeights(Choice): # define a dictionary for the weights of the generated item pool. -ror2_weights: typing.Dict[str, type(Option)] = { +ror2_weights: Dict[str, type(Option)] = { "green_scrap": GreenScrap, "red_scrap": RedScrap, "yellow_scrap": YellowScrap, @@ -163,7 +164,7 @@ ror2_weights: typing.Dict[str, type(Option)] = { "equipment": Equipment } -ror2_options: typing.Dict[str, type(Option)] = { +ror2_options: Dict[str, type(Option)] = { "total_locations": TotalLocations, "total_revivals": TotalRevivals, "start_with_revive": StartWithRevive, diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 64d741f99f..bf00f617d8 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -2,29 +2,32 @@ from BaseClasses import MultiWorld from worlds.generic.Rules import set_rule, add_rule -def set_rules(world: MultiWorld, player: int): - total_locations = world.total_locations[player] # total locations for current player +def set_rules(world: MultiWorld, player: int) -> None: + total_locations = world.total_locations[player].value # total locations for current player event_location_step = 25 # set an event location at these locations for "spheres" divisions = total_locations // event_location_step + total_revivals = world.worlds[player].total_revivals # pulling this info we calculated in generate_basic if divisions: for i in range(1, divisions): # since divisions is the floor of total_locations / 25 event_loc = world.get_location(f"Pickup{i * event_location_step}", player) - set_rule(event_loc, lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) + set_rule(event_loc, + lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) for n in range(i * event_location_step, (i + 1) * event_location_step): # we want to create a rule for each of the 25 locations per division if n == i * event_location_step: - set_rule(world.get_location(f"ItemPickup{n}", player), lambda state, event_item=event_loc.item.name: state.has(event_item, player)) + set_rule(world.get_location(f"ItemPickup{n}", player), + lambda state, event_item=event_loc.item.name: state.has(event_item, player)) else: set_rule(world.get_location(f"ItemPickup{n}", player), - lambda state, n = n: state.can_reach(f"ItemPickup{n - 1}", 'Location', player)) + lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player)) for i in range(divisions * event_location_step, total_locations+1): - set_rule(world.get_location(f"ItemPickup{i}", player), lambda state, i=i: state.can_reach(f"ItemPickup{i - 1}", "Location", player)) - + set_rule(world.get_location(f"ItemPickup{i}", player), + lambda state, i=i: state.can_reach(f"ItemPickup{i - 1}", "Location", player)) set_rule(world.get_location("Victory", player), lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player)) - if world.total_revivals[player] or world.start_with_revive[player]: - total_revivals = world.total_revivals[player] * world.total_locations[player] // 100 + if total_revivals or world.start_with_revive[player].value: add_rule(world.get_location("Victory", player), - lambda state: state.has("Dio's Best Friend", player, total_revivals + world.start_with_revive[player].value)) + lambda state: state.has("Dio's Best Friend", player, + total_revivals + world.start_with_revive[player])) world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 9d0d693b61..af65a15ea4 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,10 +1,11 @@ import string +from typing import Dict, List from .Items import RiskOfRainItem, item_table, item_pool_weights -from .Locations import location_table, RiskOfRainLocation, base_location_table +from .Locations import RiskOfRainLocation, item_pickups from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ror2_options +from .Options import ror2_options, ItemWeights from worlds.AutoWorld import World, WebWorld client_version = 1 @@ -32,34 +33,31 @@ class RiskOfRainWorld(World): topology_present = False item_name_to_id = item_table - location_name_to_id = location_table + location_name_to_id = item_pickups - data_version = 3 + data_version = 4 forced_auto_forfeit = True web = RiskOfWeb() + total_revivals: int - def generate_basic(self): + def generate_early(self) -> None: + # figure out how many revivals should exist in the pool + self.total_revivals = int(self.world.total_revivals[self.player].value / 100 * + self.world.total_locations[self.player].value) + + def generate_basic(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend if self.world.start_with_revive[self.player].value: self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) # if presets are enabled generate junk_pool from the selected preset pool_option = self.world.item_weights[self.player].value - if self.world.item_pool_presets[self.player].value: + junk_pool: Dict[str, int] = {} + if self.world.item_pool_presets[self.player]: # generate chaos weights if the preset is chosen - if pool_option == 5: - junk_pool = { - "Item Scrap, Green": self.world.random.randint(0, 80), - "Item Scrap, Red": self.world.random.randint(0, 45), - "Item Scrap, Yellow": self.world.random.randint(0, 30), - "Item Scrap, White": self.world.random.randint(0, 100), - "Common Item": self.world.random.randint(0, 100), - "Uncommon Item": self.world.random.randint(0, 70), - "Legendary Item": self.world.random.randint(0, 30), - "Boss Item": self.world.random.randint(0, 20), - "Lunar Item": self.world.random.randint(0, 60), - "Equipment": self.world.random.randint(0, 40) - } + if pool_option == ItemWeights.option_chaos: + for name, max_value in item_pool_weights[pool_option].items(): + junk_pool[name] = self.world.random.randint(0, max_value) else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets @@ -77,37 +75,43 @@ class RiskOfRainWorld(World): } # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled - if not self.world.enable_lunar[self.player]: - if not pool_option == 4: - junk_pool.pop("Lunar Item") + if not (self.world.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic): + junk_pool.pop("Lunar Item") # Generate item pool - itempool = [] - + itempool: List = [] # Add revive items for the player - itempool += ["Dio's Best Friend"] * int(self.world.total_revivals[self.player] / 100 * self.world.total_locations[self.player]) + itempool += ["Dio's Best Friend"] * self.total_revivals # Fill remaining items with randomly generated junk itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=self.world.total_locations[self.player] - - int(self.world.total_revivals[self.player] / 100 * self.world.total_locations[self.player])) + k=self.world.total_locations[self.player].value - self.total_revivals) # Convert itempool into real items itempool = list(map(lambda name: self.create_item(name), itempool)) self.world.itempool += itempool - def set_rules(self): + def set_rules(self) -> None: set_rules(self.world, self.player) - def create_regions(self): - create_regions(self.world, self.player) - create_events(self.world, self.player, int(self.world.total_locations[self.player])) + def create_regions(self) -> None: + menu = create_region(self.world, self.player, "Menu") + petrichor = create_region(self.world, self.player, "Petrichor V", + [f"ItemPickup{i + 1}" for i in range(self.world.total_locations[self.player].value)]) + + connection = Entrance(self.player, "Lobby", menu) + menu.exits.append(connection) + connection.connect(petrichor) + + self.world.regions += [menu, petrichor] + + create_events(self.world, self.player) def fill_slot_data(self): return { "itemPickupStep": self.world.item_pickup_step[self.player].value, - "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)), + "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for _ in range(16)), "totalLocations": self.world.total_locations[self.player].value, "totalRevivals": self.world.total_revivals[self.player].value, "startWithDio": self.world.start_with_revive[self.player].value, @@ -116,49 +120,39 @@ class RiskOfRainWorld(World): def create_item(self, name: str) -> Item: item_id = item_table[name] - item = RiskOfRainItem(name, ItemClassification.filler, item_id, self.player) if name == "Dio's Best Friend": - item.classification = ItemClassification.progression + classification = ItemClassification.progression elif name in {"Equipment", "Legendary Item"}: - item.classification = ItemClassification.useful + classification = ItemClassification.useful + else: + classification = ItemClassification.filler + item = RiskOfRainItem(name, classification, item_id, self.player) return item -def create_events(world: MultiWorld, player: int, total_locations: int): +def create_events(world: MultiWorld, player: int) -> None: + total_locations = world.total_locations[player].value num_of_events = total_locations // 25 if total_locations / 25 == num_of_events: num_of_events -= 1 + world_region = world.get_region("Petrichor V", player) + for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world.get_region('Petrichor V', player)) + event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) event_loc.access_rule(lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", player)) - world.get_region('Petrichor V', player).locations.append(event_loc) + world_region.locations.append(event_loc) + + victory_event = RiskOfRainLocation(player, "Victory", None, world_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) + world_region.locations.append(victory_event) -# generate locations based on player setting -def create_regions(world, player: int): - world.regions += [ - create_region(world, player, 'Menu', None, ['Lobby']), - create_region(world, player, 'Petrichor V', - [location for location in base_location_table] + - [f"ItemPickup{i}" for i in range(1, 1 + world.total_locations[player])]) - ] - - world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player)) - world.get_location("Victory", player).place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, - None, player)) - - -def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, RegionType.Generic, name, player) - ret.world = world +def create_region(world: MultiWorld, player: int, name: str, locations: List[str] = None) -> Region: + ret = Region(name, RegionType.Generic, name, player, world) if locations: for location in locations: - loc_id = location_table[location] + loc_id = item_pickups[location] location = RiskOfRainLocation(player, location, loc_id, ret) ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - return ret From bba82ccd6c0c97286288718b7f71150cdebbb507 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 20 Aug 2022 16:17:23 -0700 Subject: [PATCH 05/62] WebHost: Remove "Wiki" link from footer (#943) --- WebHostLib/templates/islandFooter.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 66995f011d..243eea7c1a 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -6,8 +6,6 @@ - Source Code - - Wiki - - Contributors - Bug Report From 484ee9f0651724469287da1bc78b14a9d2d8e391 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 20 Aug 2022 16:55:41 -0700 Subject: [PATCH 06/62] OoT: More item.type bugs. (#930) --- worlds/oot/Hints.py | 2 ++ worlds/oot/Items.py | 6 ++++++ worlds/oot/Rules.py | 5 +++-- worlds/oot/__init__.py | 16 ++++++++-------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index b8ae7dfafc..aaafeb20b8 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -129,6 +129,8 @@ def getItemGenericName(item): def isRestrictedDungeonItem(dungeon, item): + if not isinstance(item, OOTItem): + return False if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon': return item in dungeon.dungeon_items if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon': diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index b4c0719700..31e6c31f62 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -22,6 +22,12 @@ def ap_id_to_oot_data(ap_id): raise Exception(f'Could not find desired item ID: {ap_id}') +def oot_is_item_of_type(item, item_type): + if not isinstance(item, OOTItem): + return False + return item.type == item_type + + class OOTItem(Item): game: str = "Ocarina of Time" type: str diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index c55407b485..915aae77c1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -3,6 +3,7 @@ import logging from .SaveContext import SaveContext from .Regions import TimeOfDay +from .Items import oot_is_item_of_type from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item @@ -138,7 +139,7 @@ def set_rules(ootworld): # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. location = world.get_location('Sheik in Ice Cavern', player) - add_item_rule(location, lambda item: item.player == player and item.type == 'Song') + add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) if ootworld.skip_child_zelda: # If skip child zelda is on, the item at Song from Impa must be giveable by the save context. @@ -181,7 +182,7 @@ def set_shop_rules(ootworld): wallet = ootworld.parser.parse_rule('Progressive_Wallet') wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)') - for location in filter(lambda location: location.item and location.item.type == 'Shop', ootworld.get_locations()): + for location in filter(lambda location: location.item and oot_is_item_of_type(location.item, 'Shop'), ootworld.get_locations()): # Add wallet requirements if location.item.name in ['Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)']: add_rule(location, wallet) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index fb90b04e77..b4635ad77f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -8,7 +8,7 @@ logger = logging.getLogger("Ocarina of Time") from .Location import OOTLocation, LocationFactory, location_name_to_id from .Entrance import OOTEntrance from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError -from .Items import OOTItem, item_table, oot_data_to_ap_id +from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules @@ -793,7 +793,7 @@ class OOTWorld(World): # This includes all locations for which show_in_spoiler is false, and shuffled shop items. for loc in self.get_locations(): if loc.address is not None and ( - not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop') + not loc.show_in_spoiler or oot_is_item_of_type(loc.item, 'Shop') or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None @@ -869,11 +869,11 @@ class OOTWorld(World): autoworld.major_item_locations.append(loc) if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or - (loc.item.type == 'Song' or - (loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or - (loc.item.type == 'HideoutSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or - (loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or - (loc.item.type == 'GanonBossKey' and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): + (oot_is_item_of_type(loc.item, 'Song') or + (oot_is_item_of_type(loc.item, 'SmallKey') and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'BossKey') and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'GanonBossKey') and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): if loc.player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[loc.player][hint_area]['weight'] += 1 @@ -888,7 +888,7 @@ class OOTWorld(World): elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth for player in (barren_hint_players | woth_hint_players): for loc in world.worlds[player].get_locations(): - if loc.item.code and (not loc.locked or loc.item.type == 'Song'): + if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): if player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[player][hint_area]['weight'] += 1 From bf217dcf85ce1c2b577c8825df76ef68669d850d Mon Sep 17 00:00:00 2001 From: wordfcuk <80899010+wordfcuk@users.noreply.github.com> Date: Sun, 21 Aug 2022 17:30:30 +0200 Subject: [PATCH 07/62] RoR2: Fixed the link to the game settings page (#945) --- worlds/ror2/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index dc1c3769c7..e0088e47da 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -29,7 +29,7 @@ You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) h about why Archipelago uses YAML files and what they're for. ### Where do I get a YAML? -You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago +You can use the [game settings page](/games/Risk%20of%20Rain%202/player-settings) here on the Archipelago website to generate a YAML using a graphical interface. From a4a8894d22167ef80e261e1bef88b72107754f9f Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sun, 21 Aug 2022 19:20:35 -0400 Subject: [PATCH 08/62] Add /SNI to .gitignore (#949) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 58122d64a2..8a7246210f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ README.html .vs/ EnemizerCLI/ /Players/ +/SNI/ /options.yaml /config.yaml /logs/ From 9553627136c37bd08d8b538c47ae4a54c96e907b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 22 Aug 2022 05:50:01 +0200 Subject: [PATCH 09/62] Witness: More bug fixes (#937) * Fixed disable_non_randomized and other bugs * Slight performance & code sensibility increase * Added River Shortcut to Garden as a disabled check in disable_non_randomized * Changed no progression items exception to a warning * Added a list of disabled panels to slot_data for disable_non_randomized, so the client can automatically disable the right panels in the future * Made no progression exception conditional on playercount --- worlds/witness/__init__.py | 12 ++++++--- worlds/witness/locations.py | 2 ++ worlds/witness/player_logic.py | 27 ++++++++++++------- .../witness/settings/Disable_Unrandomized.txt | 12 +++++---- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 19c9b97240..a6a9f9f6f8 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -13,6 +13,7 @@ from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options, get_option_value from .utils import best_junk_to_add_based_on_weights +from logging import warning class WitnessWebWorld(WebWorld): @@ -56,15 +57,20 @@ class WitnessWorld(World): 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, 'item_id_to_door_hexes': self.items.ITEM_ID_TO_DOOR_HEX, 'door_hexes': self.items.DOORS, - 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME + 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, + 'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS, } def generate_early(self): if not (is_option_enabled(self.world, self.player, "shuffle_symbols") or get_option_value(self.world, self.player, "shuffle_doors") or is_option_enabled(self.world, self.player, "shuffle_lasers")): - raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" - " Shuffle or Laser Shuffle") + if self.world.players == 1: + warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" + " Shuffle or Laser Shuffle if that doesn't seem right.") + else: + raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle," + " Door Shuffle or Laser Shuffle.") self.player_logic = WitnessPlayerLogic(self.world, self.player) self.locat = WitnessPlayerLocations(self.world, self.player, self.player_logic) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index f6fcad70ce..9b7d60ea16 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -277,6 +277,8 @@ class WitnessPlayerLocations: if not is_option_enabled(world, player, "shuffle_postgame"): self.CHECK_LOCATIONS -= postgame + self.CHECK_LOCATIONS.discard(StaticWitnessLogic.CHECKS_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index efbb177f00..6fe45b107f 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -36,6 +36,9 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ + if panel_hex in self.COMPLETELY_DISABLED_CHECKS: + return frozenset() + check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) @@ -72,7 +75,9 @@ class WitnessPlayerLogic: for option_panel in option: dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) - if option_panel in {"7 Lasers", "11 Lasers"}: + if option_panel in self.COMPLETELY_DISABLED_CHECKS: + new_items = frozenset() + elif option_panel in {"7 Lasers", "11 Lasers"}: new_items = frozenset({frozenset([option_panel])}) # If a panel turns on when a panel in a different region turns on, # the latter panel will be an "event panel", unless it ends up being @@ -204,9 +209,11 @@ class WitnessPlayerLogic: elif get_option_value(world, player, "victory_condition") == 3: self.VICTORY_LOCATION = "0xFFF00" - self.COMPLETELY_DISABLED_CHECKS.add( - self.VICTORY_LOCATION - ) + if get_option_value(world, player, "challenge_lasers") <= 7: + adjustment_linesets_in_order.append([ + "Requirement Changes:", + "0xFFF00 - 11 Lasers - True", + ]) if is_option_enabled(world, player, "disable_non_randomized_puzzles"): adjustment_linesets_in_order.append(get_disable_unrandomized_list()) @@ -356,6 +363,12 @@ class WitnessPlayerLogic: "0x2700B": "Open Door to Treehouse Laser House", "0x00055": "Orchard Apple Trees 4 Turns On", "0x17DDB": "Left Orange Bridge Fully Extended", + "0x03535": "Shipwreck Video Pattern Knowledge", + "0x03542": "Mountain Video Pattern Knowledge", + "0x0339E": "Desert Video Pattern Knowledge", + "0x03481": "Tutorial Video Pattern Knowledge", + "0x03702": "Jungle Video Pattern Knowledge", + "0x0356B": "Challenge Video Pattern Knowledge", } self.ALWAYS_EVENT_NAMES_BY_HEX = { @@ -371,12 +384,6 @@ class WitnessPlayerLogic: "0x0C2B2": "Bunker Laser Activation", "0x00BF6": "Swamp Laser Activation", "0x028A4": "Treehouse Laser Activation", - "0x03535": "Shipwreck Video Pattern Knowledge", - "0x03542": "Mountain Video Pattern Knowledge", - "0x0339E": "Desert Video Pattern Knowledge", - "0x03481": "Tutorial Video Pattern Knowledge", - "0x03702": "Jungle Video Pattern Knowledge", - "0x0356B": "Challenge Video Pattern Knowledge", "0x09F7F": "Mountaintop Trap Door Turns On", "0x17C34": "Mountain Access", } diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 43c2596405..f03f5496c2 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -1,15 +1,15 @@ Event Items: -Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x00B8D,0x17CF7 -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 +Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 Bunker Laser Activation - 0x00061,0x17D01,0x17C42 Shadows Laser Activation - 0x00021,0x17D28,0x17C71 Requirement Changes: -0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 | 0x17CA4 +0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 0x28B39 - True - Reflection 0x17CAB - True - True +0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: 0x03505 (Tutorial Gate Close) @@ -61,7 +61,7 @@ Disabled Locations: 0x193AA (Monastery Branch Avoid 2) 0x193AB (Monastery Branch Follow 1) 0x193A6 (Monastery Branch Follow 2) -0x17CA4 (Monastery Laser) - 0x193A6 - True +0x17CA4 (Monastery Laser) 0x18590 (Tree Outlines) - True - Symmetry & Environment 0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment 0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment @@ -103,4 +103,6 @@ Disabled Locations: 0x17E67 (Bunker Drop-Down Door Squares 2) 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) -0x0042D (Mountaintop River Shape) \ No newline at end of file +0x0042D (Mountaintop River Shape) + +0x17CAA (River Door to Garden Panel) \ No newline at end of file From 6a6dfcbaffa1f745a1d27056f2740831d885a1e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Aug 2022 07:24:36 +0200 Subject: [PATCH 10/62] Core: add some types to generic.Rules --- worlds/generic/Rules.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index f75b74d0b6..f53c417e1c 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -49,11 +49,11 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]): location.progress_type = LocationProgressType.EXCLUDED -def set_rule(spot, rule: CollectionRule): +def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): spot.access_rule = rule -def add_rule(spot, rule: CollectionRule, combine='and'): +def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'): old_rule = spot.access_rule if combine == 'or': spot.access_rule = lambda state: rule(state) or old_rule(state) @@ -61,35 +61,37 @@ def add_rule(spot, rule: CollectionRule, combine='and'): spot.access_rule = lambda state: rule(state) and old_rule(state) -def forbid_item(location, item: str, player: int): +def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) -def forbid_items_for_player(location, items: typing.Set[str], player: int): +def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) -def forbid_items(location, items: typing.Set[str]): +def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): """unused, but kept as a debugging tool.""" old_rule = location.item_rule location.item_rule = lambda i: i.name not in items and old_rule(i) -def add_item_rule(location, rule: ItemRule): +def add_item_rule(location: "BaseClasses.Location", rule: ItemRule): old_rule = location.item_rule location.item_rule = lambda item: rule(item) and old_rule(item) -def item_in_locations(state, item: str, player: int, locations: typing.Sequence): +def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, + locations: typing.Sequence["BaseClasses.Location"]) -> bool: for location in locations: if item_name(state, location[0], location[1]) == (item, player): return True return False -def item_name(state, location: str, player: int) -> typing.Optional[typing.Tuple[str, int]]: +def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ + typing.Optional[typing.Tuple[str, int]]: location = state.world.get_location(location, player) if location.item is None: return None From 87d91aeef39657eb67ff09ec8d029f2d48f2f85e Mon Sep 17 00:00:00 2001 From: N00byKing Date: Sun, 21 Aug 2022 20:45:39 +0200 Subject: [PATCH 11/62] sm64ex: Option for 1Up Block Rando --- worlds/sm64ex/Locations.py | 47 ++++++++++++++++++++++++++++++-------- worlds/sm64ex/Options.py | 7 ++++++ worlds/sm64ex/Rules.py | 3 ++- worlds/sm64ex/__init__.py | 44 +++++++++++++++++++++++++++++------ 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index f33c4306a1..1995abf425 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -46,6 +46,9 @@ locCCM_table = { "CCM: Snowman's Lost His Head": 3626025, "CCM: Wall Kicks Will Work": 3626026, "CCM: Bob-omb Buddy": 3626203, + "CCM: 1Up Block Near Snowman": 3626215, + "CCM: 1Up Block Ice Pillar": 3626216, + "CCM: 1Up Block Secret Slide": 3626217 } #Big Boo's Haunt @@ -55,7 +58,8 @@ locBBH_table = { "BBH: Secret of the Haunted Books": 3626030, "BBH: Seek the 8 Red Coins": 3626031, "BBH: Big Boo's Balcony": 3626032, - "BBH: Eye to Eye in the Secret Room": 3626033 + "BBH: Eye to Eye in the Secret Room": 3626033, + "BBH: 1Up Block Top of Mansion": 3626218 } #Hazy Maze Cave @@ -65,7 +69,9 @@ locHMC_table = { "HMC: Metal-Head Mario Can Move!": 3626037, "HMC: Navigating the Toxic Maze": 3626038, "HMC: A-Maze-Ing Emergency Exit": 3626039, - "HMC: Watch for Rolling Rocks": 3626040 + "HMC: Watch for Rolling Rocks": 3626040, + "HMC: 1Up Block above Pit": 3626219, + "HMC: 1Up Block Past Rolling Rocks": 3626220, } #Lethal Lava Land @@ -87,6 +93,9 @@ locSSL_table = { "SSL: Free Flying for 8 Red Coins": 3626053, "SSL: Pyramid Puzzle": 3626054, "SSL: Bob-omb Buddy": 3626207, + "SSL: 1Up Block Outside Pyramid": 3626221, + "SSL: 1Up Block Pyramid Left Path": 3626222, + "SSL: 1Up Block Pyramid Back": 3626223 } #Dire, Dire Docks @@ -108,6 +117,8 @@ locSL_table = { "SL: Shell Shreddin' for Red Coins": 3626067, "SL: Into the Igloo": 3626068, "SL: Bob-omb Buddy": 3626209, + "SL: 1Up Block Near Moneybags": 3626224, + "SL: 1Up Block inside Igloo": 3626225 } #Wet-Dry World @@ -119,6 +130,7 @@ locWDW_table = { "WDW: Go to Town for Red Coins": 3626074, "WDW: Quick Race Through Downtown!": 3626075, "WDW: Bob-omb Buddy": 3626210, + "WDW: 1Up Block in Downtown": 3626226 } #Tall, Tall Mountain @@ -130,6 +142,7 @@ locTTM_table = { "TTM: Breathtaking View from Bridge": 3626081, "TTM: Blast to the Lonely Mushroom": 3626082, "TTM: Bob-omb Buddy": 3626211, + "TTM: 1Up Block on Red Mushroom": 3626227 } #Tiny-Huge Island @@ -141,6 +154,9 @@ locTHI_table = { "THI: Wiggler's Red Coins": 3626088, "THI: Make Wiggler Squirm": 3626089, "THI: Bob-omb Buddy": 3626212, + "THI: 1Up Block THI Small near Start": 3626228, + "THI: 1Up Block THI Large near Start": 3626229, + "THI: 1Up Block Windy Area": 3626230 } #Tick Tock Clock @@ -150,7 +166,9 @@ locTTC_table = { "TTC: Get a Hand": 3626093, "TTC: Stomp on the Thwomp": 3626094, "TTC: Timed Jumps on Moving Bars": 3626095, - "TTC: Stop Time for Red Coins": 3626096 + "TTC: Stop Time for Red Coins": 3626096, + "TTC: 1Up Block Midway Up": 3626231, + "TTC: 1Up Block at the Top": 3626232 } #Rainbow Ride @@ -162,6 +180,9 @@ locRR_table = { "RR: Tricky Triangles!": 3626102, "RR: Somewhere Over the Rainbow": 3626103, "RR: Bob-omb Buddy": 3626214, + "RR: 1Up Block Top of Red Coin Maze": 3626233, + "RR: 1Up Block Under Fly Guy": 3626234, + "RR: 1Up Block On House in the Sky": 3626235 } loc100Coin_table = { @@ -193,7 +214,9 @@ locSA_table = { locBitDW_table = { "Bowser in the Dark World Red Coins": 3626105, - "Bowser in the Dark World Key": 3626178 + "Bowser in the Dark World Key": 3626178, + "Bowser in the Dark World 1Up Block on Tower": 3626236, + "Bowser in the Dark World 1Up Block near Goombas": 3626237 } locTotWC_table = { @@ -203,25 +226,31 @@ locTotWC_table = { locCotMC_table = { "Cavern of the Metal Cap Switch": 3626182, - "Cavern of the Metal Cap Red Coins": 3626133 + "Cavern of the Metal Cap Red Coins": 3626133, + "Cavern of the Metal Cap 1Up Block": 3626241 } locVCutM_table = { "Vanish Cap Under the Moat Switch": 3626183, - "Vanish Cap Under the Moat Red Coins": 3626147 + "Vanish Cap Under the Moat Red Coins": 3626147, + "Vanish Cap Under the Moat 1Up Block": 3626242 } locBitFS_table = { "Bowser in the Fire Sea Red Coins": 3626112, - "Bowser in the Fire Sea Key": 3626179 + "Bowser in the Fire Sea Key": 3626179, + "Bowser in the Fire Sea 1Up Block Swaying Stairs": 3626238, + "Bowser in the Fire Sea 1Up Block Near Poles": 3626239 } locWMotR_table = { - "Wing Mario Over the Rainbow": 3626154 + "Wing Mario Over the Rainbow Red Coins": 3626154, + "Wing Mario Over the Rainbow 1Up Block": 3626242 } locBitS_table = { - "Bowser in the Sky Red Coins": 3626119 + "Bowser in the Sky Red Coins": 3626119, + "Bowser in the Sky 1Up Block": 3626240 } #Secret Stars found inside the Castle diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 594b0561c0..7d9a75dde9 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -68,6 +68,12 @@ class BuddyChecks(Toggle): """Bob-omb Buddies are checks, Cannon Unlocks are items""" display_name = "Bob-omb Buddy Checks" +class ExclamationBoxes(Choice): + """Include 1Up Exclamation Boxes during randomization""" + display_name = "Randomize 1Up !-Blocks" + option_Off = 0 + option_1Ups_Only = 1 + class ProgressiveKeys(DefaultOnToggle): """Keys will first grant you access to the Basement, then to the Secound Floor""" display_name = "Progressive Keys" @@ -87,4 +93,5 @@ sm64_options: typing.Dict[str,type(Option)] = { "StarsToFinish": StarsToFinish, "death_link": DeathLink, "BuddyChecks": BuddyChecks, + "ExclamationBoxes": ExclamationBoxes } \ No newline at end of file diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index a4a82b2737..eae9868583 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -109,7 +109,8 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) #Rules for Secret Stars - add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.has("Wing Cap", player)) + add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player)) + add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player)) add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index e0f911fbd9..447a09d431 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -34,7 +34,7 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 6 + data_version = 7 required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] @@ -71,7 +71,6 @@ class SM64World(World): return item def generate_basic(self): - staritem = self.create_item("Power Star") starcount = self.world.AmountOfStars[self.player].value if (not self.world.EnableCoinStars[self.player].value): starcount = max(35,self.world.AmountOfStars[self.player].value-15) @@ -79,17 +78,15 @@ class SM64World(World): self.world.BasementStarDoorCost[self.player].value, self.world.SecondFloorStarDoorCost[self.player].value, self.world.MIPS1Cost[self.player].value, self.world.MIPS2Cost[self.player].value, self.world.StarsToFinish[self.player].value) - self.world.itempool += [staritem for i in range(0,starcount)] - mushroomitem = self.create_item("1Up Mushroom") - self.world.itempool += [mushroomitem for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] + self.world.itempool += [self.create_item("Power Star") for i in range(0,starcount)] + self.world.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] if (not self.world.ProgressiveKeys[self.player].value): key1 = self.create_item("Basement Key") key2 = self.create_item("Second Floor Key") self.world.itempool += [key1,key2] else: - key = self.create_item("Progressive Key") - self.world.itempool += [key,key] + self.world.itempool += [self.create_item("Progressive Key") for i in range(0,2)] wingcap = self.create_item("Wing Cap") metalcap = self.create_item("Metal Cap") @@ -110,6 +107,39 @@ class SM64World(World): self.world.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.world.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) + if (self.world.ExclamationBoxes[self.player].value > 0): + self.world.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)] + else: + self.world.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("BBH: 1Up Block Top of Mansion", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("HMC: 1Up Block above Pit", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("HMC: 1Up Block Past Rolling Rocks", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Outside Pyramid", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Pyramid Left Path", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Pyramid Back", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SL: 1Up Block Near Moneybags", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SL: 1Up Block inside Igloo", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("WDW: 1Up Block in Downtown", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTM: 1Up Block on Red Mushroom", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block THI Small near Start", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block THI Large near Start", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block Windy Area", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTC: 1Up Block Midway Up", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTC: 1Up Block at the Top", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block Top of Red Coin Maze", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block Under Fly Guy", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block On House in the Sky", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Dark World 1Up Block on Tower", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Dark World 1Up Block near Goombas", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Cavern of the Metal Cap 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Vanish Cap Under the Moat 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Fire Sea 1Up Block Swaying Stairs", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Fire Sea 1Up Block Near Poles", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Wing Mario Over the Rainbow 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Sky 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + def get_filler_item_name(self) -> str: return "1Up Mushroom" From 11cbc0b40b6a6b68e7dea99b3a6ad1c17e00c739 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Mon, 22 Aug 2022 14:30:42 -0700 Subject: [PATCH 12/62] Factorio: Make the energy bridge a different color. (#952) --- worlds/factorio/data/mod_template/data.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index c5f2745bd1..d790831478 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -1,4 +1,15 @@ {% from "macros.lua" import dict_to_lua %} +-- TODO: Replace the tinting code with an actual rendered picture of the energy bridge icon. +-- This tint is so that one is less likely to accidentally mass-produce energy-bridges, then wonder why their rocket is not building. +function energy_bridge_tint() + return { r = 0, g = 1, b = 0.667, a = 1} +end +function tint_icon(obj, tint) + obj.icons = { {icon = obj.icon, icon_size = obj.icon_size, icon_mipmaps = obj.icon_mipmaps, tint = tint} } + obj.icon = nil + obj.icon_size = nil + obj.icon_mipmaps = nil +end local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"]) energy_bridge.name = "ap-energy-bridge" energy_bridge.minable.result = "ap-energy-bridge" @@ -6,12 +17,20 @@ energy_bridge.localised_name = "Archipelago EnergyLink Bridge" energy_bridge.energy_source.buffer_capacity = "5MJ" energy_bridge.energy_source.input_flow_limit = "1MW" energy_bridge.energy_source.output_flow_limit = "1MW" +tint_icon(energy_bridge, energy_bridge_tint()) +energy_bridge.picture.layers[1].tint = energy_bridge_tint() +energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) energy_bridge_item.name = "ap-energy-bridge" energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge" energy_bridge_item.place_result = energy_bridge.name +tint_icon(energy_bridge_item, energy_bridge_tint()) data.raw["item"]["ap-energy-bridge"] = energy_bridge_item local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"]) From c695f91198f1094badfee98d15082024ebe00322 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Aug 2022 23:35:41 +0200 Subject: [PATCH 13/62] Subnautica: add Options to Creature Scans (#950) --- worlds/subnautica/Creatures.py | 25 +++++++-- worlds/subnautica/Items.py | 4 +- worlds/subnautica/Options.py | 20 ++++++- worlds/subnautica/Rules.py | 100 +++++++++++++++++++-------------- worlds/subnautica/__init__.py | 11 +++- 5 files changed, 109 insertions(+), 51 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 56e2a7efa1..a9f5e850e1 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -50,10 +50,8 @@ all_creatures: Dict[str, int] = { "Lava Larva": 1300, "Lava Lizard": 1300, "Sea Dragon Leviathan": 1300, - "Sea Emperor Leviathan": 1700, + "Cuddlefish": 300, "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 @@ -73,10 +71,29 @@ aggressive: Set[str] = { "River Prowler", } +containment: Set[str] = { # creatures that have to be raised from eggs + "Cuddlefish", +} + +hatchable: Set[str] = { # aggressive creatures that can be grown from eggs as alternative to stasis + "Ampeel", # warning: electric shocks + "Crabsquid", # warning: electric shocks + "Crabsnake", + "Boneshark", + "Crashfish", + "Gasopod", + "Lava Lizard", + "Mesmer", + "Sand Shark", + "Stalker", +} + suffix: str = " Scan" creature_locations: Dict[str, int] = { - creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) + creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) } all_creatures_presorted: List[str] = sorted(all_creatures) +all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] + diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 0f05d5e31a..9917921a4b 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -42,7 +42,7 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Stillsuit', 'tech_type': 'Stillsuit'}, - 35008: {'classification': ItemClassification.filler, + 35008: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Alien Containment Fragment', 'tech_type': 'BaseWaterParkFragment'}, @@ -222,7 +222,7 @@ item_table: Dict[int, ItemDict] = { 'count': 2, 'name': 'Observatory Fragment', 'tech_type': 'BaseObservatoryFragment'}, - 35053: {'classification': ItemClassification.useful, + 35053: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Multipurpose Room', 'tech_type': 'BaseRoom'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f9f3f56756..020b5d1916 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Range, DeathLink +from Options import Choice, Range, DeathLink, Toggle from .Creatures import all_creatures @@ -33,12 +33,27 @@ class Goal(Choice): class CreatureScans(Range): - """Place items on specific creature scans. + """Place items on specific, randomly chosen, creature scans. Warning: Includes aggressive Leviathans.""" display_name = "Creature Scans" range_end = len(all_creatures) +class AggressiveScanLogic(Toggle): + """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle. + Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. + Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. + None: Aggressive Creatures are assumed to not need any tools to scan. + + Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" + display_name = "Aggressive Creature Scan Logic" + option_stasis = 0 + option_containment = 1 + option_either = 2 + option_none = 3 + + class SubnauticaDeathLink(DeathLink): """When you die, everyone dies. Of course the reverse is true too. Note: can be toggled via in-game console command "deathlink".""" @@ -48,5 +63,6 @@ options = { "item_pool": ItemPool, "goal": Goal, "creature_scans": CreatureScans, + "creature_scan_logic": AggressiveScanLogic, "death_link": SubnauticaDeathLink, } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index b8f8f1a7b4..20c6a35c84 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,122 +1,128 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Callable -from worlds.generic.Rules import set_rule +from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict from .Creatures import all_creatures, aggressive, suffix +from .Options import AggressiveScanLogic import math if TYPE_CHECKING: from . import SubnauticaWorld + from BaseClasses import CollectionState, Location -def has_seaglide(state, player: int): +def has_seaglide(state: "CollectionState", player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player: int): +def has_modification_station(state: "CollectionState", player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player: int): +def has_mobile_vehicle_bay(state: "CollectionState", player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player: int): +def has_moonpool(state: "CollectionState", player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player: int): +def has_vehicle_upgrade_console(state: "CollectionState", player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player: int): +def has_seamoth(state: "CollectionState", player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player: int): +def has_seamoth_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player: int): +def has_seamoth_depth_module_mk2(state: "CollectionState", player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player: int): +def has_seamoth_depth_module_mk3(state: "CollectionState", player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player: int): +def has_cyclops_bridge(state: "CollectionState", player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player: int): +def has_cyclops_engine(state: "CollectionState", player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player: int): +def has_cyclops_hull(state: "CollectionState", player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player: int): +def has_cyclops(state: "CollectionState", 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: int): +def has_cyclops_depth_module_mk1(state: "CollectionState", player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player: int): +def has_cyclops_depth_module_mk2(state: "CollectionState", player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player: int): +def has_cyclops_depth_module_mk3(state: "CollectionState", player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player: int): +def has_prawn(state: "CollectionState", player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player: int): +def has_prawn_propulsion_arm(state: "CollectionState", 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: int): +def has_prawn_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player: int): +def has_prawn_depth_module_mk2(state: "CollectionState", player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player: int): +def has_laser_cutter(state: "CollectionState", player: int): return state.has("Laser Cutter Fragment", player, 3) -def has_stasis_rile(state, player: int): +def has_stasis_rifle(state: "CollectionState", player: int): return state.has("Stasis Rifle Fragment", player, 2) +def has_containment(state: "CollectionState", player: int): + return state.has("Alien Containment Fragment", player, 2) and state.has("Multipurpose Room", player) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player: int): +def has_propulsion_cannon(state: "CollectionState", player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ - (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) + (has_prawn(state, player) and has_prawn_propulsion_arm(state, player)) -def has_cyclops_shield(state, player: int): +def has_cyclops_shield(state: "CollectionState", player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -129,7 +135,7 @@ def has_cyclops_shield(state, player: int): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player: int): +def get_max_swim_depth(state: "CollectionState", player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -140,7 +146,7 @@ def get_max_swim_depth(state, player: int): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player: int): + # if has_seaglide(state: "CollectionState", player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -156,7 +162,7 @@ def get_max_swim_depth(state, player: int): # return max_depth -def get_seamoth_max_depth(state, player: int): +def get_seamoth_max_depth(state: "CollectionState", player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -170,7 +176,7 @@ def get_seamoth_max_depth(state, player: int): return 0 -def get_cyclops_max_depth(state, player): +def get_cyclops_max_depth(state: "CollectionState", player): if has_cyclops(state, player): if has_cyclops_depth_module_mk3(state, player): return 1700 @@ -184,7 +190,7 @@ def get_cyclops_max_depth(state, player): return 0 -def get_prawn_max_depth(state, player): +def get_prawn_max_depth(state: "CollectionState", player): if has_prawn(state, player): if has_prawn_depth_module_mk2(state, player): return 1700 @@ -196,7 +202,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player: int): +def get_max_depth(state: "CollectionState", 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 @@ -206,7 +212,7 @@ def get_max_depth(state, player: int): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict) -> bool: +def can_access_location(state: "CollectionState", 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 @@ -239,17 +245,25 @@ 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 can_scan_creature(state, player: int, creature: str) -> bool: +def can_scan_creature(state: "CollectionState", 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), +def set_creature_rule(world, player: int, creature_name: str) -> "Location": + location = world.get_location(creature_name + suffix, player) + set_rule(location, lambda state: can_scan_creature(state, player, creature_name)) + return location + + +aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { + AggressiveScanLogic.option_stasis: has_stasis_rifle, + AggressiveScanLogic.option_containment: has_containment, + AggressiveScanLogic.option_either: lambda state, player: + has_stasis_rifle(state, player) or has_containment(state, player) +} def set_rules(subnautica_world: "SubnauticaWorld"): @@ -259,8 +273,12 @@ def set_rules(subnautica_world: "SubnauticaWorld"): 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) + if subnautica_world.creatures_to_scan: + aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + for creature_name in subnautica_world.creatures_to_scan: + location = set_creature_rule(world, player, creature_name) + if creature_name in aggressive and aggressive_rule: + add_rule(location, lambda state: aggressive_rule(state, player)) # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 6fa064d53a..6562d93db0 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations option_definitions = Options.options - data_version = 5 + data_version = 6 required_client_version = (0, 3, 4) prefill_items: List[Item] @@ -52,7 +52,14 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: + valid_creatures = Creatures.all_creatures_presorted_without_containment + self.world.creature_scans[self.player].value = min(len( + Creatures.all_creatures_presorted_without_containment), + self.world.creature_scans[self.player].value) + else: + valid_creatures = Creatures.all_creatures_presorted + self.creatures_to_scan = self.world.random.sample(valid_creatures, self.world.creature_scans[self.player].value) def create_regions(self): From b66a2657261dadc897f2f70cba2f49f70f20f0fc Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 22 Aug 2022 16:50:16 -0500 Subject: [PATCH 14/62] Docs: Make webworld attribute descriptions docstrings instead of comments for nice IDE things (#929) --- worlds/AutoWorld.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 1ca5b53422..02b94c5fb7 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -97,22 +97,22 @@ def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None: class WebWorld: """Webhost integration""" - # display a settings page. Can be a link to an out-of-ap settings tool too. + settings_page: Union[bool, str] = True - - # docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md' + """display a settings page. Can be a link to a specific page or external tool.""" + game_info_languages: List[str] = ['en'] + """docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'""" - # docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial - # class is to be used for one guide. tutorials: List["Tutorial"] + """docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide.""" - # Choose a theme for your /game/* pages - # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone theme = "grass" + """Choose a theme for you /game/* pages. + Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone""" - # display a link to a bug report page, most likely a link to a GitHub issue page. bug_report_page: Optional[str] + """display a link to a bug report page, most likely a link to a GitHub issue page.""" class World(metaclass=AutoWorldRegister): From d66f981be6213144c1dfbf77a93d3f45c72df562 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 22 Aug 2022 17:39:55 -0500 Subject: [PATCH 15/62] Github: templates and new user interface (#870) * move some docs out of readme and link with the headers * PR template * bug report template * task and feature request templates * md cleanup * forgot the template * make expected results separate section * move pr template to .github. remove assignment field on tasks * add headers to pr template * Requested changes * suggested changes from @black-sliver and @SoldierofOrder * Update docs/code_of_conduct.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Update docs/contributing.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Update docs/contributing.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Hussein Farran Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yaml | 35 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yaml | 17 ++++++++++ .github/ISSUE_TEMPLATE/task.yaml | 10 ++++++ .github/pull_request_template.md | 12 +++++++ README.md | 22 ++----------- docs/code_of_conduct.md | 11 +++++++ docs/contributing.md | 12 +++++++ 7 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .github/ISSUE_TEMPLATE/task.yaml create mode 100644 .github/pull_request_template.md create mode 100644 docs/code_of_conduct.md create mode 100644 docs/contributing.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..dff9a56651 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,35 @@ +name: Bug Report +description: File a bug report. +title: "Bug: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! If this bug occurred during local generation check your + Archipelago install for a log (probably `C:\ProgramData\Archipelago\logs`) + and upload it with this report, as well as all yaml files used. + - type: textarea + id: what-happened + attributes: + label: What happened? + validations: + required: true + - type: textarea + id: expected-results + attributes: + label: What were the expected results? + validations: + required: true + - type: dropdown + id: version + attributes: + label: Software + description: Where did this bug occur? + options: + - Website + - Local generation + - While playing + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000000..84cee1b7f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,17 @@ +name: Feature Request +description: Request a feature! +title: "Category: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Please replace `Category` in the title with what this feature will be targeting, such as Core generation, + website, documentation, or a game. + Note: this is not for requesting new games to be added. If you would like to request a game, the best place to + ask is about it is in the [discord](https://archipelago.gg/discord). + - type: textarea + id: feature + attributes: + label: What feature would you like to see? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 0000000000..fb677c684f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,10 @@ +name: Task +description: Submit a task to be done. If this is not targeting core, it should likely be elsewhere. +title: "Core: " +labels: + - core + - enhancement +body: + - type: textarea + attributes: + label: What task needs to be completed? \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..c7c6471dd0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +Please format your title with what portion of the project this pull request is +targeting and what it's changing. + +ex. "MyGame4: implement new game" or "Docs: add new guide for customizing MyGame3" + +## What is this fixing or adding? + + +## How was this tested? + + +## If this makes graphical changes, please attach screenshots. diff --git a/README.md b/README.md index 9403159c74..c8362dddd0 100644 --- a/README.md +++ b/README.md @@ -61,26 +61,10 @@ This project makes use of multiple other projects. We wouldn't be here without t * [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer) ## Contributing -Contributions are welcome. We have a few asks of any new contributors. - -* Ensure that all changes which affect logic are covered by unit tests. -* Do not introduce any unit test failures/regressions. - -Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.) - -For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. +For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md) ## FAQ -For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/) +For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/) ## Code of Conduct -We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to: - -* Be welcoming and inclusive in tone and language. -* Be respectful of others and their abilities. -* Show empathy when speaking with others. -* Be gracious and accept feedback and constructive criticism. - -These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private, such as private messaging or emails. - -Any incidents of abuse may be reported directly to Ijwu at hmfarran@gmail.com. +Please refer to our [code of conduct.](/docs/code_of_conduct.md) diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md new file mode 100644 index 0000000000..dd3d154a02 --- /dev/null +++ b/docs/code_of_conduct.md @@ -0,0 +1,11 @@ +# Code of Conduct +We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to: + +* Be welcoming and inclusive in tone and language. +* Be respectful of others and their abilities. +* Show empathy when speaking with others. +* Be gracious and accept feedback and constructive criticism. + +These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails. + +Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000..adbbf0dea1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,12 @@ +# Contributing +Contributions are welcome. We have a few requests of any new contributors. + +* Ensure that all changes which affect logic are covered by unit tests. +* Do not introduce any unit test failures/regressions. +* Follow styling as designated in our [styling documentation](/docs/style.md). + +Otherwise, we tend to judge code on a case to case basis. + +For adding a new game to Archipelago and other documentation on how Archipelago functions, please see +[the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev +channel in our [Discord](https://archipelago.gg/discord). From 7f41cafffc3d97971aec36fa86d6c7576867c8bb Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 22 Aug 2022 19:01:21 -0400 Subject: [PATCH 16/62] Explaining the "Style Lockdown" (#940) * First pass at a contribution guide for the website. Suggestions are welcome. * Attempt to make the WebHost change guide describe the intent of the style restrictions more accurately. * Try to improve the explanation of the intention behind the style restrictions. --- WebHostLib/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 WebHostLib/README.md diff --git a/WebHostLib/README.md b/WebHostLib/README.md new file mode 100644 index 0000000000..52d4963aee --- /dev/null +++ b/WebHostLib/README.md @@ -0,0 +1,46 @@ +# WebHost + +## Contribution Guidelines +**Thank you for your interest in contributing to the Archipelago website!** +Much of the content on the website is generated automatically, but there are some things +that need a personal touch. For those things, we rely on contributions from both the core +team and the community. The current primary maintainer of the website is Farrak Kilhn. +He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`. + +### Small Changes +Little changes like adding a button or a couple new select elements are perfectly fine. +Tweaks to style specific to a PR's content are also probably not a problem. For example, if +you build a new page which needs two side by side tables, and you need to write a CSS file +specific to your page, that is perfectly reasonable. + +### Content Additions +Once you develop a new feature or add new content the website, make a pull request. It will +be reviewed by the community and there will probably be some discussion around it. Depending +on the size of the feature, and if new styles are required, there may be an additional step +before the PR is accepted wherein Farrak works with the designer to implement styles. + +### Restrictions on Style Changes +A professional designer is paid to develop the styles and assets for the Archipelago website. +In an effort to maintain a consistent look and feel, pull requests which *exclusively* +change site styles are rejected. Please note this applies to code which changes the overall +look and feel of the site, not to small tweaks to CSS for your custom page. The intention +behind these restrictions is to maintain a curated feel for the design of the site. If +any PR affects the overall feel of the site but includes additive changes, there will +likely be a conversation about how to implement those changes without compromising the +curated site style. It is therefore worth noting there are a couple files which, if +changed in your pull request, will cause it to draw additional scrutiny. + +These closely guarded files are: +- `globalStyles.css` +- `islandFooter.css` +- `landing.css` +- `markdown.css` +- `tooltip.css` + +### Site Themes +There are several themes available for game pages. It is possible to request a new theme in +the `#art-and-design` channel on Discord. Because themes are created by the designer, they +are not free, and take some time to create. Farrak works closely with the designer to implement +these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year +are added. If a proposed theme seems like a cool idea and the community likes it, there is a +good chance it will become a reality. From 0a5b24be2b714b741d837f4392a455788c4b07ce Mon Sep 17 00:00:00 2001 From: Jarno Date: Tue, 23 Aug 2022 01:02:10 +0200 Subject: [PATCH 17/62] [Core] Phase out Print packets and added Countdown type to print json (#812) * [Core] Added Countdown type to print json to distinct the count down message from other types * Added backward compatibility check * Fixed review comments * Updated header category * Apply suggestions from code review Co-authored-by: Hussein Farran * Completely phased out Print in favor of PrintJson * Updated docs to warn about phasing out of Print * Removed faulty import Co-authored-by: Hussein Farran --- MultiServer.py | 38 ++++++++++++++++++++++++++++++-------- docs/network protocol.md | 16 ++++++++++++++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8a1844bf92..6354f8e7a9 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -36,6 +36,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ SlotType min_client_version = Version(0, 1, 6) +print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7 colorama.init() # functions callable on storable data on the server by clients @@ -291,20 +292,27 @@ class Context: # text - def notify_all(self, text): + def notify_all(self, text: str): logging.info("Notice (all): %s" % text) - self.broadcast_all([{"cmd": "Print", "text": text}]) + broadcast_text_all(self, text) def notify_client(self, client: Client, text: str): if not client.auth: return logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) - asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}])) + if client.version >= print_command_compatability_threshold: + asyncio.create_task(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}])) + else: + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}])) def notify_client_multiple(self, client: Client, texts: typing.List[str]): if not client.auth: return - asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) + if client.version >= print_command_compatability_threshold: + asyncio.create_task(self.send_msgs(client, + [{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts])) + else: + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) # loading @@ -721,19 +729,33 @@ async def on_client_left(ctx: Context, client: Client): ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) -async def countdown(ctx: Context, timer): - ctx.notify_all(f'[Server]: Starting countdown of {timer}s') +async def countdown(ctx: Context, timer: int): + broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s") if ctx.countdown_timer: ctx.countdown_timer = timer # timer is already running, set it to a different time else: ctx.countdown_timer = timer while ctx.countdown_timer > 0: - ctx.notify_all(f'[Server]: {ctx.countdown_timer}') + broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}") ctx.countdown_timer -= 1 await asyncio.sleep(1) - ctx.notify_all(f'[Server]: GO') + broadcast_countdown(ctx, 0, f"[Server]: GO") ctx.countdown_timer = 0 +def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}): + old_clients, new_clients = [], [] + + for teams in ctx.clients.values(): + for clients in teams.values(): + for client in clients: + new_clients.append(client) if client.version >= print_command_compatability_threshold \ + else old_clients.append(client) + + ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }]) + ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) + +def broadcast_countdown(ctx: Context, timer: int, message: str): + broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer }) def get_players_string(ctx: Context): auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} diff --git a/docs/network protocol.md b/docs/network protocol.md index b12768e2c9..342514248d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -152,7 +152,8 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: All arguments for this packet are optional, only changes are sent. ### Print -Sent to clients purely to display a message to the player. +Sent to clients purely to display a message to the player. +* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)* #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | @@ -164,10 +165,21 @@ Sent to clients purely to display a message to the player. This packet differs f | Name | Type | Notes | | ---- | ---- | ----- | | 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. | +| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. | | receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. | | item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. | | found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. | +| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. | + +##### PrintJsonType +PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal. + +Currently defined types are: +| Type | Notes | +| ---- | ----- | +| ItemSend | The message is in response to a player receiving an item. | +| Hint | The message is in response to a player hinting. | +| Countdown | The message contains information about the current server Countdown. | ### DataPackage Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info. From e548abd332ba68d208e67fee98c574596ad251e4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 01:02:29 +0200 Subject: [PATCH 18/62] Subnautica: use correct option parent class (#954) * Subnautica: use correct option parent class * Update Options.py --- worlds/subnautica/Options.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 020b5d1916..f68e12d2c0 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,6 @@ -from Options import Choice, Range, DeathLink, Toggle +import typing + +from Options import Choice, Range, DeathLink from .Creatures import all_creatures @@ -39,7 +41,7 @@ class CreatureScans(Range): range_end = len(all_creatures) -class AggressiveScanLogic(Toggle): +class AggressiveScanLogic(Choice): """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle. Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. From c390801c4c03f83a7aba47912fe40bc4220d1a2c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 01:07:17 +0200 Subject: [PATCH 19/62] Test: verify file webhost file creations work to some degree (#953) WebHost: fix some file creation paths --- WebHostLib/options.py | 13 ++++++++----- test/webhost/TestFileGeneration.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 test/webhost/TestFileGeneration.py diff --git a/WebHostLib/options.py b/WebHostLib/options.py index e2d362a570..daa742d90e 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,6 +1,6 @@ import logging import os -from Utils import __version__ +from Utils import __version__, local_path from jinja2 import Template import yaml import json @@ -9,14 +9,13 @@ import typing from worlds.AutoWorld import AutoWorldRegister import Options -target_folder = os.path.join("WebHostLib", "static", "generated") - handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", "exclude_locations"} def create(): - os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True) + target_folder = local_path("WebHostLib", "static", "generated") + os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True) def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]): data = {} @@ -66,12 +65,16 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): all_options = {**Options.per_game_common_options, **world.option_definitions} - res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( + with open(local_path("WebHostLib", "templates", "options.yaml")) as f: + file_data = f.read() + res = Template(file_data).render( options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, dictify_range=dictify_range, default_converter=default_converter, ) + del file_data + with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f: f.write(res) diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py new file mode 100644 index 0000000000..7f56864ea3 --- /dev/null +++ b/test/webhost/TestFileGeneration.py @@ -0,0 +1,23 @@ +"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors.""" + +import os +import unittest + +import WebHost + + +class TestFileGeneration(unittest.TestCase): + def setUp(self) -> None: + self.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib") + # should not create the folder *here* + self.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib") + + def testOptions(self): + WebHost.create_options_files() + self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs"))) + self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs"))) + + def testTutorial(self): + WebHost.create_ordered_tutorials_file() + self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json"))) + self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json"))) From fab12dca0bc32720be596e4ad86fe78c0bcaeab7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Aug 2022 19:20:22 +0200 Subject: [PATCH 20/62] SC2: add anti air to Devil's Playground Victory People seem to be on the mission long enough to get attacked by Mutalisks, so Victory should require anti air. Optional Objectives are doable quite comfortably before Mutalisks show up, allowing the anti-air to be on them for later in the mission. --- worlds/sc2wol/Locations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 3425dc7199..f69abd48e3 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -176,7 +176,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_competent_anti_air(world, player) and state.has('Science Vessel', player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, - lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + lambda state: state._sc2wol_has_anti_air(world, player) and ( + state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), From 33103b209d2b917f6975c69d4179a9886014b026 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 22:18:24 +0200 Subject: [PATCH 21/62] WebHost: fix error on save --- WebHostLib/customserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 01f1fd25e5..da7b54ba6d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -103,7 +103,7 @@ class WebHostContext(Context): room.multisave = pickle.dumps(self.get_save()) # saving only occurs on activity, so we can "abuse" this information to mark this as last_activity if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again - room.last_activity = datetime.utcnow() + room.last_activity = datetime.datetime.utcnow() return True def get_save(self) -> dict: From 295ea97544f0deb41e51e16d4ac221c630713b80 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 23:04:20 +0200 Subject: [PATCH 22/62] Subnautica: increment client version --- worlds/subnautica/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 6562d93db0..806c1b195e 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -42,7 +42,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 6 - required_client_version = (0, 3, 4) + required_client_version = (0, 3, 5) prefill_items: List[Item] creatures_to_scan: List[str] From 1aaf89ff2cfbf440c9e2269ebfc1aec55ff04eb8 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 23 Aug 2022 17:20:39 -0400 Subject: [PATCH 23/62] SC2: Switched mission item group to a list comprehension to fix missile shuffle errors (#959) --- worlds/sc2wol/Items.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 59b59bc137..8da40de5ab 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -1,5 +1,6 @@ from BaseClasses import Item, ItemClassification import typing +from .MissionTables import vanilla_mission_req_table class ItemData(typing.NamedTuple): @@ -153,12 +154,7 @@ basic_unit: typing.Tuple[str, ...] = ( item_name_groups = {} for item, data in item_table.items(): item_name_groups.setdefault(data.type, []).append(item) -item_name_groups["Missions"] = ["Beat Liberation Day", "Beat The Outlaws", "Beat Zero Hour", "Beat Evacuation", - "None Outbreak", "Beat Safe Haven", "Beat Haven's Fall", "Beat Smash and Grab", "Beat The Dig", - "Beat The Moebius Factor", "Beat Supernova", "Beat Maw of the Void", "Beat Devil's Playground", - "Beat Welcome to the Jungle", "Beat Breakout", "Beat Ghost of a Chance", - "Beat The Great Train Robbery", "Beat Cutthroat", "Beat Engine of Destruction", - "Beat Media Blitz", "Beat Piercing the Shroud"] +item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table] filler_items: typing.Tuple[str, ...] = ( '+15 Starting Minerals', From 0d6cbd9093361f734c0d6af939c8bc1e32207c43 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 23:33:30 +0200 Subject: [PATCH 24/62] Core: convert item name groups to frozenset Some worlds define them in lists, this speeds up lookup via state.has_group() or similar --- worlds/AutoWorld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 02b94c5fb7..8d9a1b0829 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -27,7 +27,8 @@ class AutoWorldRegister(type): # build rest dct["item_names"] = frozenset(dct["item_name_to_id"]) - dct["item_name_groups"] = dct.get("item_name_groups", {}) + dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set + in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) From a78863fde1ae6517d99917d54f8935966d151044 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 26 Aug 2022 02:12:37 -0500 Subject: [PATCH 25/62] Docs: Update community supported libraries in api doc (#788) * Docs: Update client supported libraries in api doc * left align table column * Update table of languages to include Haxe lib and remarks * Reformat table * Changed verbiage on SNI remark --- docs/network protocol.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 342514248d..3315ddec2d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -13,9 +13,18 @@ These steps should be followed in order to establish a gameplay connection with In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. -There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp) +There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier. -For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) +| Language/Runtime | Project | Remarks | +|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | | +| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). | +| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | | +| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | | +| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only | +| | [APCpp](https://github.com/N00byKing/APCpp) | CMake | +| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported | +| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. From a175aa93e7185d929a471ac0ec4173e78be564cf Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 26 Aug 2022 01:31:30 -0700 Subject: [PATCH 26/62] Factorio: Detect if more than one AP factorio mod is loaded. (#964) --- worlds/factorio/data/mod_template/settings.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 7703ebe2e5..73e131a60e 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -1,3 +1,21 @@ +-- Find out if more than one AP mod is loaded, and if so, error out. +function mod_is_AP(str) + -- lua string.match is way more restrictive than regex. Regex would be "^AP-W?\d{20}-P[1-9]\d*-.+$" + local result = string.match(str, "^AP%-W?%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-P[1-9]%d-%-.+$") + if result ~= nil then + log("Archipelago Mod: " .. result .. " is loaded.") + end + return result ~= nil +end +local ap_mod_count = 0 +for name, _ in pairs(mods) do + if mod_is_AP(name) then + ap_mod_count = ap_mod_count + 1 + if ap_mod_count > 1 then + error("More than one Archipelago Factorio mod is loaded.") + end + end +end data:extend({ { type = "bool-setting", From af19180ff0834bf8af09a1e989c44fa794cc3802 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Sat, 20 Aug 2022 04:35:46 +0000 Subject: [PATCH 27/62] SM: Fix rolling saves, add SRAM features - fix receiving items in an old save (issue #855) by moving receive queue's read pointer to a per-saveslot value - clear SRAM over $70:2000, and invalidate save data, when booting a new seed number for the first time - copy important ROM data to SRAM so future clients don't have to read ROM --- .../multiworld-basepatch.ips | Bin 17952 -> 18193 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 801 ++++++++++-------- .../sm-basepatch-symbols.json | 74 +- 3 files changed, 500 insertions(+), 375 deletions(-) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index d7fd17613e8f2af2b5cc6ed5db11efbe9d227f07..7ac3ea018475a0db495d6d540c9dda5f66f1391f 100644 GIT binary patch delta 499 zcmX|-PiPZC6vkh6(=?ca4VZ&}Vuq0{5d;TMo&_<~KTz}}9z$n`_Dqx~_b0GY=;FgE&o_ewRsM z6u;h2Vsnx4y>Sqp#q`_3L&n3jr=WX;ZkC?5{9Ao$>QZtAuoxjoY+a>2Bro&gDL-B#&u+)JeSKm9bBrWrO3JH=ddlGKyiAi&2&ro{n^*xHbPqe+AI% zIpOfSk=&Z!ob3|l1H4q*#Bo?Ma*i`MEEv&Y1pm8oUzAYe_%5R+P@kYCQCAsvPvkG1 zFX%gU0HYY)PSNF#6%wzXno~!8G&-ptSdX~S-JC&0^W-2w*(d*X53D_+n qEe}V|L_gfCnwGwXb8zSnQoG8bze6`HcSXCL7+MA`Tg|sxIrj%vd*R;z delta 239 zcmbQ($GD(}ae|S2LBkG)hVKk4G4~sHFl=UKU{jJgvE$V%M()iFEJ`ODHwLOPG1WJ2 zu4Hmo@?!jO$Y4Fg|8pN$lmr`gJTh3>pY3rgiUTP4Pp#nOzf&JnRx&UwVqlohpinTM zK?R64fLN#C#ft|FEEis`{CBD!s8FNoSH@&!H66M23~B}aEI@&bRSOsxvOPcs0lED^ zVU3LJ$-Zg@%s}Sk18ROu?;9oysEcuLXaMPv0;!m6sIDgH*|1|j!-E|kxc4(X*s)4! fQp4m7b$!174cY61IO`-b1sIf?8#d2XFOdfTlAm2m diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym index 751f470f53..5def2b7d9c 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -2,14 +2,14 @@ ; generated by asar [labels] -B8:8026 :neg_1_1 +B8:80C1 :neg_1_1 85:B9B4 :neg_1_2 85:B9E6 :neg_1_3 B8:C81F :neg_1_4 B8:C831 :neg_1_5 B8:C843 :neg_1_6 B8:800C :pos_1_0 -B8:81DE :pos_1_1 +B8:82D7 :pos_1_1 84:FA6B :pos_1_2 84:FA75 :pos_1_3 B8:C862 :pos_1_4 @@ -20,7 +20,7 @@ B8:C87C :pos_1_6 85:990F CLIPLEN_end 85:990C CLIPLEN_no_multi 85:FF1D CLIPSET -B8:80EF COLLECTTANK +B8:81E8 COLLECTTANK 85:FF45 MISCFX 84:8BF2 NORMAL 85:FF4E SETFX @@ -38,6 +38,11 @@ CE:FF00 config_multiworld CE:FF08 config_player_id CE:FF06 config_remote_items CE:FF02 config_sprite +B8:8119 copy_config_to_sram +B8:80FD copy_memory +B8:8117 copy_memory_done +B8:8109 copy_memory_even +B8:810F copy_memory_loop 84:F894 h_item 84:F8AD i_chozo_item 84:F8B4 i_hidden_item @@ -46,11 +51,11 @@ B8:885C i_item_setup_shared B8:8878 i_item_setup_shared_all_items B8:8883 i_item_setup_shared_alwaysloaded 84:FA79 i_live_pickup -B8:817F i_live_pickup_multiworld -B8:81C4 i_live_pickup_multiworld_end -B8:819B i_live_pickup_multiworld_local_item_or_offworld -B8:81B0 i_live_pickup_multiworld_own_item -B8:81BC i_live_pickup_multiworld_own_item1 +B8:8278 i_live_pickup_multiworld +B8:82BD i_live_pickup_multiworld_end +B8:8294 i_live_pickup_multiworld_local_item_or_offworld +B8:82A9 i_live_pickup_multiworld_own_item +B8:82B5 i_live_pickup_multiworld_own_item1 84:FA1E i_load_custom_graphics 84:FA39 i_load_custom_graphics_all_items 84:FA49 i_load_custom_graphics_alwaysloaded @@ -85,22 +90,27 @@ B8:81BC i_live_pickup_multiworld_own_item1 85:B9CA message_write_placeholders_loop 85:B9DC message_write_placeholders_notfound 85:B9DF message_write_placeholders_value_ok -B8:8092 mw_display_item_sent -B8:80FF mw_handle_queue -B8:8178 mw_handle_queue_end -B8:8101 mw_handle_queue_loop -B8:8151 mw_handle_queue_new_remote_item -B8:816D mw_handle_queue_next -B8:8163 mw_handle_queue_perform_receive -B8:81C8 mw_hook_main_game +B8:818B mw_display_item_sent +B8:81F8 mw_handle_queue +B8:8271 mw_handle_queue_end +B8:81FA mw_handle_queue_loop +B8:824A mw_handle_queue_new_remote_item +B8:8266 mw_handle_queue_next +B8:825C mw_handle_queue_perform_receive +B8:82C1 mw_hook_main_game B8:8011 mw_init -B8:8044 mw_init_end +B8:8066 mw_init_continuereset +B8:80EA mw_init_end B8:8000 mw_init_memory -B8:8083 mw_load_sram -B8:80B0 mw_receive_item -B8:80E8 mw_receive_item_end -B8:8070 mw_save_sram -B8:8049 mw_write_message +B8:803B mw_init_reset_sram +B8:8051 mw_init_smstringdata +B8:8174 mw_load_sram +B8:8182 mw_load_sram_done +B8:8185 mw_load_sram_setnewgame +B8:81A9 mw_receive_item +B8:81E1 mw_receive_item_end +B8:8169 mw_save_sram +B8:8142 mw_write_message 84:F888 nonprog_item_eight_palette_indices 89:9200 offworld_graphics_data_item 89:9100 offworld_graphics_data_progression_item @@ -125,7 +135,7 @@ B8:8049 mw_write_message 84:F96E p_visible_item_end 84:F95B p_visible_item_loop 84:F967 p_visible_item_trigger -B8:81DF patch_load_multiworld +B8:82D8 patch_load_multiworld 84:FA7E perform_item_pickup 84:F886 plm_graphics_entry_offworld_item 84:F87C plm_graphics_entry_offworld_progression_item @@ -144,17 +154,19 @@ B8:C808 start_item_data_minor B8:C818 start_item_data_reserve B8:C856 update_graphic 84:F890 v_item +B8:80EF write_repeated_memory +B8:80F4 write_repeated_memory_loop [source files] 0000 e25029c5 main.asm 0001 06780555 ../common/nofanfare.asm -0002 e76d1f83 ../common/multiworld.asm +0002 4f9a780e ../common/multiworld.asm 0003 613d24e1 ../common/itemextras.asm 0004 d6616c0c ../common/items.asm 0005 440b54fe ../common/startitem.asm [rom checksum] -09b134c5 +ad81eda1 [addr-to-line mapping] ff:ffff 0000:00000001 @@ -204,330 +216,423 @@ ff:ffff 0000:00000001 84:8bf2 0001:00000152 84:8bf6 0001:00000153 84:8bf7 0001:00000153 -b8:8000 0002:00000019 -b8:8002 0002:0000001a -b8:8006 0002:0000001b -b8:8008 0002:0000001c -b8:800c 0002:00000020 -b8:800e 0002:00000021 -b8:8010 0002:00000022 -b8:8011 0002:00000025 -b8:8012 0002:00000025 -b8:8013 0002:00000025 -b8:8014 0002:00000025 +b8:8000 0002:0000005a +b8:8002 0002:0000005b +b8:8006 0002:0000005c +b8:8008 0002:0000005d +b8:800c 0002:00000061 +b8:800e 0002:00000062 +b8:8010 0002:00000063 +b8:8011 0002:00000066 +b8:8012 0002:00000066 +b8:8013 0002:00000066 +b8:8014 0002:00000066 b8:8015 0000:00000013 -b8:8017 0002:00000029 -b8:801b 0002:0000002a -b8:801e 0002:0000002b -b8:8020 0002:0000002d -b8:8023 0002:0000002e -b8:8026 0002:00000031 -b8:802a 0002:00000032 -b8:802e 0002:00000033 -b8:8032 0002:00000034 -b8:8036 0002:00000035 -b8:8037 0002:00000035 -b8:8038 0002:00000036 -b8:803b 0002:00000037 -b8:803d 0002:00000039 -b8:8040 0002:0000003a -b8:8044 0002:0000003d -b8:8045 0002:0000003d -b8:8046 0002:0000003d -b8:8047 0002:0000003d -b8:8048 0002:0000003e -b8:8049 0002:00000043 -b8:804a 0002:00000043 -b8:804b 0002:00000044 -b8:804c 0002:00000044 -b8:804d 0002:00000045 -b8:8051 0002:00000046 -b8:8054 0002:00000046 -b8:8055 0002:00000047 -b8:8056 0002:00000048 -b8:805a 0002:00000049 -b8:805b 0002:0000004a -b8:805f 0002:0000004b -b8:8060 0002:0000004c -b8:8064 0002:0000004e -b8:8068 0002:0000004f -b8:8069 0002:00000050 -b8:806d 0002:00000051 -b8:806e 0002:00000051 -b8:806f 0002:00000052 -b8:8070 0002:00000055 -b8:8071 0002:00000055 -b8:8072 0000:00000013 -b8:8074 0002:00000057 -b8:8078 0002:00000058 -b8:807c 0002:00000059 -b8:807d 0002:00000059 -b8:807e 0002:0000005b -b8:807f 0002:0000005c -b8:8082 0002:0000005d -b8:8083 0002:00000060 -b8:8084 0002:00000060 -b8:8085 0000:00000013 -b8:8087 0002:00000062 -b8:808b 0002:00000063 -b8:808f 0002:00000064 -b8:8090 0002:00000064 -b8:8091 0002:00000065 -b8:8092 0002:0000006a -b8:8094 0002:0000006b -b8:8096 0002:0000006e -b8:8099 0002:0000006f -b8:809b 0002:00000070 -b8:809e 0002:00000071 -b8:80a0 0002:00000072 -b8:80a3 0002:00000073 -b8:80a7 0002:00000074 -b8:80a9 0002:00000075 -b8:80ab 0002:00000076 -b8:80ad 0002:00000077 -b8:80af 0002:00000078 -b8:80b0 0002:0000007c -b8:80b1 0002:0000007c -b8:80b2 0002:0000007d -b8:80b5 0002:0000007e -b8:80b7 0002:0000007f -b8:80ba 0002:00000080 -b8:80bc 0002:00000081 -b8:80bd 0002:00000082 -b8:80be 0002:00000084 -b8:80c1 0002:00000085 -b8:80c3 0002:00000086 -b8:80c6 0002:00000087 -b8:80c7 0002:00000088 -b8:80ca 0002:00000089 -b8:80cb 0002:00000089 -b8:80cc 0002:0000008a -b8:80d0 0002:0000008b -b8:80d1 0002:0000008c -b8:80d4 0002:0000008d -b8:80d8 0002:0000008e -b8:80da 0002:00000090 -b8:80dd 0002:00000091 -b8:80df 0002:00000092 -b8:80e2 0002:00000093 -b8:80e4 0002:00000095 -b8:80e8 0002:00000097 -b8:80ea 0002:00000098 -b8:80ec 0002:00000099 -b8:80ed 0002:00000099 -b8:80ee 0002:0000009a -b8:80ef 0002:000000a5 -b8:80f0 0002:000000a6 -b8:80f4 0002:000000a7 -b8:80f5 0002:000000a8 -b8:80f9 0002:000000a9 -b8:80fa 0002:000000ab -b8:80fe 0002:000000ac -b8:80ff 0002:000000de -b8:8100 0002:000000de -b8:8101 0002:000000e1 -b8:8105 0002:000000e2 -b8:8109 0002:000000e3 -b8:810b 0002:000000e5 -b8:810d 0002:000000e5 -b8:810e 0002:000000e8 -b8:8112 0002:000000e9 -b8:8114 0002:000000ea -b8:8118 0002:000000eb -b8:811a 0002:000000ec -b8:811e 0002:000000ed -b8:8121 0002:000000ee -b8:8123 0002:000000ef -b8:8125 0002:000000f0 -b8:8129 0002:000000f1 -b8:812b 0002:000000f2 -b8:812d 0002:000000f3 -b8:8130 0002:000000f4 -b8:8133 0002:000000f5 -b8:8135 0002:000000f6 -b8:813d 0002:000000fa -b8:813e 0002:000000fb -b8:813f 0002:000000fc -b8:8143 0002:000000ff -b8:8147 0002:00000100 -b8:814b 0002:00000101 -b8:814d 0002:00000103 -b8:814e 0002:00000104 -b8:814f 0002:00000105 -b8:8151 0002:0000010a -b8:8152 0002:0000010b -b8:8156 0002:0000010e -b8:815a 0002:0000010f -b8:815e 0002:00000110 -b8:8162 0002:00000111 -b8:8163 0002:00000115 -b8:8165 0002:00000116 -b8:8168 0002:00000117 -b8:816a 0002:00000118 -b8:816d 0002:0000011b -b8:8171 0002:0000011c -b8:8172 0002:0000011d -b8:8176 0002:0000011f -b8:8178 0002:00000122 -b8:817a 0002:00000123 -b8:817c 0002:00000124 -b8:817d 0002:00000124 -b8:817e 0002:00000125 -b8:817f 0002:00000129 -b8:8180 0002:00000129 -b8:8181 0002:00000129 -b8:8182 0002:0000012a -b8:8186 0002:0000012b -b8:8189 0002:0000012b -b8:818a 0002:0000012d -b8:818e 0002:0000012e -b8:818f 0002:0000012f -b8:8193 0002:00000130 -b8:8196 0002:00000131 -b8:8198 0002:00000133 -b8:819b 0002:00000136 -b8:819f 0002:00000137 -b8:81a3 0002:00000138 -b8:81a5 0002:0000013a -b8:81a9 0002:0000013b -b8:81aa 0002:0000013d -b8:81ae 0002:0000013e -b8:81b0 0002:00000141 -b8:81b4 0002:00000142 -b8:81b7 0002:00000143 -b8:81b9 0002:00000144 -b8:81bc 0002:00000147 -b8:81bd 0002:00000148 -b8:81be 0002:00000149 -b8:81c2 0002:0000014a -b8:81c4 0002:0000014d -b8:81c5 0002:0000014d -b8:81c6 0002:0000014d -b8:81c7 0002:0000014e -b8:81c8 0002:00000152 -b8:81cc 0002:00000153 -b8:81d0 0002:00000154 -b8:81d2 0002:00000155 -b8:81d6 0002:00000156 -b8:81d9 0002:00000157 -b8:81db 0002:00000158 -b8:81de 0002:0000015a -b8:81df 0002:0000015d -b8:81e3 0002:0000015e -b8:81e4 0002:0000015f -b8:81e7 0002:00000160 -b8:81eb 0002:00000162 -b8:81ec 0002:00000163 -b8:81ed 0002:00000164 -b8:81ee 0002:00000165 -b8:81ef 0002:00000166 -8b:914a 0002:0000016b -81:80f7 0002:0000016e -81:8027 0002:00000171 -82:8bb3 0002:00000174 -85:b9a3 0002:0000020e -85:b9a4 0002:0000020e -85:b9a5 0002:00000211 -85:b9a7 0002:00000212 -85:b9ad 0002:00000212 -85:b9ae 0002:00000213 -85:b9b1 0002:00000214 -85:b9b2 0002:00000215 -85:b9b3 0002:00000215 -85:b9b4 0002:00000219 -85:b9b7 0002:0000021a -85:b9bb 0002:0000021b -85:b9bd 0002:0000021b -85:b9bf 0002:0000021c -85:b9c2 0002:0000021d -85:b9c4 0002:0000021f -85:b9c5 0002:00000220 -85:b9c7 0002:00000224 -85:b9ca 0002:00000226 -85:b9cd 0002:00000227 -85:b9cf 0002:00000228 -85:b9d1 0002:00000229 -85:b9d5 0002:0000022a -85:b9d7 0002:0000022b -85:b9d9 0002:0000022c -85:b9da 0002:0000022d -85:b9dc 0002:0000022f -85:b9df 0002:00000231 -85:b9e2 0002:00000231 -85:b9e3 0002:00000232 -85:b9e6 0002:00000234 -85:b9ea 0002:00000235 -85:b9ed 0002:00000236 -85:b9ee 0002:00000237 -85:b9ef 0002:00000237 -85:b9f0 0002:00000238 -85:b9f4 0002:00000239 -85:b9f5 0002:0000023a -85:b9f9 0002:0000023b -85:b9fb 0002:0000023c -85:b9fc 0002:0000023d -85:b9fd 0002:0000023e -85:ba00 0002:0000023f -85:ba02 0002:00000240 -85:ba04 0002:00000243 -85:ba05 0002:00000243 -85:ba06 0002:00000244 -85:ba09 0002:00000245 -85:ba8a 0002:00000253 -85:ba8c 0002:00000254 -85:ba8f 0002:00000255 -85:ba92 0002:00000256 -85:ba95 0002:0000025e -85:ba96 0002:0000025f -85:ba98 0002:00000260 -85:ba9b 0002:00000261 -85:ba9d 0002:00000262 -85:ba9f 0002:00000263 -85:baa2 0002:00000264 -85:baa4 0002:00000265 -85:baa7 0002:00000266 -85:baa9 0002:00000269 -85:baaa 0002:0000026a -85:baab 0002:0000026b -85:baac 0002:0000026c -85:baae 0002:0000026d -85:baaf 0002:0000026e -85:bab0 0002:0000026f -85:bab1 0002:00000274 -85:bab4 0002:00000275 -85:bab5 0002:00000276 -85:bab8 0002:00000277 -85:bab9 0002:00000278 -85:baba 0002:00000279 -85:babb 0002:0000027a -85:babc 0002:00000285 -85:babd 0002:00000286 -85:babf 0002:00000287 -85:bac2 0002:00000288 -85:bac4 0002:00000289 -85:bac7 0002:0000028a -85:bac9 0002:0000028d -85:baca 0002:0000028e -85:bacb 0002:0000028f -85:bacd 0002:00000290 -85:bace 0002:00000292 -85:bacf 0002:00000293 -85:bad1 0002:00000294 -85:bad4 0002:00000295 -85:bad6 0002:00000296 -85:bad9 0002:00000297 -85:badb 0002:00000298 -85:badc 0002:0000029a -85:badd 0002:0000029b -85:badf 0002:0000029c -85:bae2 0002:0000029d -85:bae4 0002:0000029e -85:bae7 0002:0000029f -85:bae9 0002:000002a0 -85:8246 0002:000002a5 -85:8249 0002:000002a6 -85:824b 0002:000002a7 -85:82f9 0002:000002ab +b8:8017 0002:0000006a +b8:801b 0002:0000006b +b8:801e 0002:0000006c +b8:8020 0002:0000006d +b8:8024 0002:0000006e +b8:8028 0002:0000006f +b8:802a 0002:00000070 +b8:802e 0002:00000071 +b8:8032 0002:00000072 +b8:8034 0002:00000074 +b8:8038 0002:00000075 +b8:803b 0002:00000078 +b8:803c 0002:00000079 +b8:803f 0002:0000007a +b8:8042 0002:0000007b +b8:8045 0002:0000007c +b8:8048 0002:0000007d +b8:8049 0002:0000007e +b8:804a 0002:0000007f +b8:804e 0002:00000080 +b8:804f 0002:00000082 +b8:8066 0002:00000086 +b8:8068 0002:00000087 +b8:8069 0002:00000088 +b8:806a 0002:00000089 +b8:806c 0002:0000008a +b8:806e 0002:0000008b +b8:8070 0002:0000008c +b8:8072 0002:0000008d +b8:8075 0002:0000008e +b8:8077 0002:0000008f +b8:807a 0002:00000090 +b8:807d 0002:00000091 +b8:807f 0002:00000092 +b8:8083 0002:00000094 +b8:8085 0002:00000095 +b8:8087 0002:00000096 +b8:8089 0002:00000097 +b8:808b 0002:00000098 +b8:808d 0002:00000099 +b8:808f 0002:0000009a +b8:8092 0002:0000009b +b8:8094 0002:0000009c +b8:8097 0002:0000009d +b8:809a 0002:0000009e +b8:809c 0002:0000009f +b8:80a0 0002:000000a1 +b8:80a3 0002:000000a2 +b8:80a7 0002:000000a3 +b8:80ab 0002:000000a4 +b8:80af 0002:000000a5 +b8:80b3 0002:000000a6 +b8:80b7 0002:000000a8 +b8:80bb 0002:000000b0 +b8:80be 0002:000000b1 +b8:80c1 0002:000000b3 +b8:80c2 0002:000000b4 +b8:80c3 0002:000000b5 +b8:80c7 0002:000000b6 +b8:80cb 0002:000000b7 +b8:80cd 0002:000000c4 +b8:80d1 0002:000000c5 +b8:80d4 0002:000000c6 +b8:80d6 0002:000000c7 +b8:80da 0002:000000c8 +b8:80dd 0002:000000c9 +b8:80df 0002:000000ce +b8:80e2 0002:000000cf +b8:80e6 0002:000000d0 +b8:80ea 0002:000000d3 +b8:80eb 0002:000000d3 +b8:80ec 0002:000000d3 +b8:80ed 0002:000000d3 +b8:80ee 0002:000000d4 +b8:80ef 0002:000000db +b8:80f0 0002:000000dc +b8:80f1 0002:000000dd +b8:80f2 0002:000000de +b8:80f3 0002:000000df +b8:80f4 0002:000000e1 +b8:80f7 0002:000000e2 +b8:80f8 0002:000000e3 +b8:80f9 0002:000000e4 +b8:80fa 0002:000000e5 +b8:80fc 0002:000000e7 +b8:80fd 0002:000000ee +b8:80fe 0002:000000ef +b8:80ff 0002:000000f0 +b8:8100 0002:000000f1 +b8:8102 0002:000000f3 +b8:8104 0002:000000f4 +b8:8105 0002:000000f5 +b8:8107 0002:000000f6 +b8:8109 0002:000000f8 +b8:810b 0002:000000f9 +b8:810c 0002:000000fa +b8:810d 0002:000000fb +b8:810f 0002:000000fd +b8:8111 0002:000000fe +b8:8113 0002:000000ff +b8:8114 0002:00000100 +b8:8115 0002:00000101 +b8:8117 0002:00000103 +b8:8118 0002:00000104 +b8:8119 0002:00000108 +b8:811d 0002:00000109 +b8:8121 0002:0000010a +b8:8125 0002:0000010b +b8:8129 0002:0000010c +b8:812d 0002:0000010d +b8:8131 0002:0000010e +b8:8135 0002:0000010f +b8:8139 0002:00000110 +b8:813d 0002:00000111 +b8:8141 0002:00000112 +b8:8142 0002:00000118 +b8:8143 0002:00000118 +b8:8144 0002:00000119 +b8:8145 0002:00000119 +b8:8146 0002:0000011a +b8:814a 0002:0000011b +b8:814d 0002:0000011b +b8:814e 0002:0000011c +b8:814f 0002:0000011d +b8:8153 0002:0000011e +b8:8154 0002:0000011f +b8:8158 0002:00000120 +b8:8159 0002:00000121 +b8:815d 0002:00000123 +b8:8161 0002:00000124 +b8:8162 0002:00000125 +b8:8166 0002:00000126 +b8:8167 0002:00000126 +b8:8168 0002:00000127 +b8:8169 0002:0000012c +b8:816a 0002:0000012c +b8:816b 0000:00000013 +b8:816d 0002:0000012f +b8:816e 0002:0000012f +b8:816f 0002:00000131 +b8:8170 0002:00000132 +b8:8173 0002:00000133 +b8:8174 0002:00000138 +b8:8175 0002:00000138 +b8:8176 0000:00000013 +b8:8178 0002:0000013a +b8:817c 0002:0000013b +b8:8180 0002:0000013c +b8:8182 0002:0000013e +b8:8183 0002:0000013e +b8:8184 0002:0000013f +b8:8185 0002:00000147 +b8:8189 0002:00000148 +b8:818b 0002:0000014e +b8:818d 0002:0000014f +b8:818f 0002:00000152 +b8:8192 0002:00000153 +b8:8194 0002:00000154 +b8:8197 0002:00000155 +b8:8199 0002:00000156 +b8:819c 0002:00000157 +b8:81a0 0002:00000158 +b8:81a2 0002:00000159 +b8:81a4 0002:0000015a +b8:81a6 0002:0000015b +b8:81a8 0002:0000015c +b8:81a9 0002:00000160 +b8:81aa 0002:00000160 +b8:81ab 0002:00000161 +b8:81ae 0002:00000162 +b8:81b0 0002:00000163 +b8:81b3 0002:00000164 +b8:81b5 0002:00000165 +b8:81b6 0002:00000166 +b8:81b7 0002:00000168 +b8:81ba 0002:00000169 +b8:81bc 0002:0000016a +b8:81bf 0002:0000016b +b8:81c0 0002:0000016c +b8:81c3 0002:0000016d +b8:81c4 0002:0000016d +b8:81c5 0002:0000016e +b8:81c9 0002:0000016f +b8:81ca 0002:00000170 +b8:81cd 0002:00000171 +b8:81d1 0002:00000172 +b8:81d3 0002:00000174 +b8:81d6 0002:00000175 +b8:81d8 0002:00000176 +b8:81db 0002:00000177 +b8:81dd 0002:00000179 +b8:81e1 0002:0000017b +b8:81e3 0002:0000017c +b8:81e5 0002:0000017d +b8:81e6 0002:0000017d +b8:81e7 0002:0000017e +b8:81e8 0002:00000189 +b8:81e9 0002:0000018a +b8:81ed 0002:0000018b +b8:81ee 0002:0000018c +b8:81f2 0002:0000018d +b8:81f3 0002:0000018f +b8:81f7 0002:00000190 +b8:81f8 0002:000001c2 +b8:81f9 0002:000001c2 +b8:81fa 0002:000001c5 +b8:81fe 0002:000001c6 +b8:8202 0002:000001c7 +b8:8204 0002:000001c9 +b8:8206 0002:000001c9 +b8:8207 0002:000001cc +b8:820b 0002:000001cd +b8:820d 0002:000001ce +b8:8211 0002:000001cf +b8:8213 0002:000001d0 +b8:8217 0002:000001d1 +b8:821a 0002:000001d2 +b8:821c 0002:000001d3 +b8:821e 0002:000001d4 +b8:8222 0002:000001d5 +b8:8224 0002:000001d6 +b8:8226 0002:000001d7 +b8:8229 0002:000001d8 +b8:822c 0002:000001d9 +b8:822e 0002:000001da +b8:8236 0002:000001de +b8:8237 0002:000001df +b8:8238 0002:000001e0 +b8:823c 0002:000001e3 +b8:8240 0002:000001e4 +b8:8244 0002:000001e5 +b8:8246 0002:000001e7 +b8:8247 0002:000001e8 +b8:8248 0002:000001e9 +b8:824a 0002:000001ee +b8:824b 0002:000001ef +b8:824f 0002:000001f2 +b8:8253 0002:000001f3 +b8:8257 0002:000001f4 +b8:825b 0002:000001f5 +b8:825c 0002:000001f9 +b8:825e 0002:000001fa +b8:8261 0002:000001fb +b8:8263 0002:000001fc +b8:8266 0002:000001ff +b8:826a 0002:00000200 +b8:826b 0002:00000201 +b8:826f 0002:00000203 +b8:8271 0002:00000206 +b8:8273 0002:00000207 +b8:8275 0002:00000208 +b8:8276 0002:00000208 +b8:8277 0002:00000209 +b8:8278 0002:0000020d +b8:8279 0002:0000020d +b8:827a 0002:0000020d +b8:827b 0002:0000020e +b8:827f 0002:0000020f +b8:8282 0002:0000020f +b8:8283 0002:00000211 +b8:8287 0002:00000212 +b8:8288 0002:00000213 +b8:828c 0002:00000214 +b8:828f 0002:00000215 +b8:8291 0002:00000217 +b8:8294 0002:0000021a +b8:8298 0002:0000021b +b8:829c 0002:0000021c +b8:829e 0002:0000021e +b8:82a2 0002:0000021f +b8:82a3 0002:00000221 +b8:82a7 0002:00000222 +b8:82a9 0002:00000225 +b8:82ad 0002:00000226 +b8:82b0 0002:00000227 +b8:82b2 0002:00000228 +b8:82b5 0002:0000022b +b8:82b6 0002:0000022c +b8:82b7 0002:0000022d +b8:82bb 0002:0000022e +b8:82bd 0002:00000231 +b8:82be 0002:00000231 +b8:82bf 0002:00000231 +b8:82c0 0002:00000232 +b8:82c1 0002:00000236 +b8:82c5 0002:00000237 +b8:82c9 0002:00000238 +b8:82cb 0002:00000239 +b8:82cf 0002:0000023a +b8:82d2 0002:0000023b +b8:82d4 0002:0000023c +b8:82d7 0002:0000023e +b8:82d8 0002:00000241 +b8:82dc 0002:00000243 +b8:82dd 0002:00000244 +b8:82de 0002:00000245 +b8:82df 0002:00000246 +b8:82e0 0002:00000247 +8b:914a 0002:0000024c +81:80f7 0002:0000024f +81:8027 0002:00000252 +82:8bb3 0002:00000255 +85:b9a3 0002:000002ef +85:b9a4 0002:000002ef +85:b9a5 0002:000002f2 +85:b9a7 0002:000002f3 +85:b9ad 0002:000002f3 +85:b9ae 0002:000002f4 +85:b9b1 0002:000002f5 +85:b9b2 0002:000002f6 +85:b9b3 0002:000002f6 +85:b9b4 0002:000002fa +85:b9b7 0002:000002fb +85:b9bb 0002:000002fc +85:b9bd 0002:000002fc +85:b9bf 0002:000002fd +85:b9c2 0002:000002fe +85:b9c4 0002:00000300 +85:b9c5 0002:00000301 +85:b9c7 0002:00000305 +85:b9ca 0002:00000307 +85:b9cd 0002:00000308 +85:b9cf 0002:00000309 +85:b9d1 0002:0000030a +85:b9d5 0002:0000030b +85:b9d7 0002:0000030c +85:b9d9 0002:0000030d +85:b9da 0002:0000030e +85:b9dc 0002:00000310 +85:b9df 0002:00000312 +85:b9e2 0002:00000312 +85:b9e3 0002:00000313 +85:b9e6 0002:00000315 +85:b9ea 0002:00000316 +85:b9ed 0002:00000317 +85:b9ee 0002:00000318 +85:b9ef 0002:00000318 +85:b9f0 0002:00000319 +85:b9f4 0002:0000031a +85:b9f5 0002:0000031b +85:b9f9 0002:0000031c +85:b9fb 0002:0000031d +85:b9fc 0002:0000031e +85:b9fd 0002:0000031f +85:ba00 0002:00000320 +85:ba02 0002:00000321 +85:ba04 0002:00000324 +85:ba05 0002:00000324 +85:ba06 0002:00000325 +85:ba09 0002:00000326 +85:ba8a 0002:00000334 +85:ba8c 0002:00000335 +85:ba8f 0002:00000336 +85:ba92 0002:00000337 +85:ba95 0002:0000033f +85:ba96 0002:00000340 +85:ba98 0002:00000341 +85:ba9b 0002:00000342 +85:ba9d 0002:00000343 +85:ba9f 0002:00000344 +85:baa2 0002:00000345 +85:baa4 0002:00000346 +85:baa7 0002:00000347 +85:baa9 0002:0000034a +85:baaa 0002:0000034b +85:baab 0002:0000034c +85:baac 0002:0000034d +85:baae 0002:0000034e +85:baaf 0002:0000034f +85:bab0 0002:00000350 +85:bab1 0002:00000355 +85:bab4 0002:00000356 +85:bab5 0002:00000357 +85:bab8 0002:00000358 +85:bab9 0002:00000359 +85:baba 0002:0000035a +85:babb 0002:0000035b +85:babc 0002:00000366 +85:babd 0002:00000367 +85:babf 0002:00000368 +85:bac2 0002:00000369 +85:bac4 0002:0000036a +85:bac7 0002:0000036b +85:bac9 0002:0000036e +85:baca 0002:0000036f +85:bacb 0002:00000370 +85:bacd 0002:00000371 +85:bace 0002:00000373 +85:bacf 0002:00000374 +85:bad1 0002:00000375 +85:bad4 0002:00000376 +85:bad6 0002:00000377 +85:bad9 0002:00000378 +85:badb 0002:00000379 +85:badc 0002:0000037b +85:badd 0002:0000037c +85:badf 0002:0000037d +85:bae2 0002:0000037e +85:bae4 0002:0000037f +85:bae7 0002:00000380 +85:bae9 0002:00000381 +85:8246 0002:00000386 +85:8249 0002:00000387 +85:824b 0002:00000388 +85:82f9 0002:0000038c b8:885c 0003:00000045 b8:885d 0003:00000045 b8:885e 0003:00000046 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json index 63198cde72..222548ba1e 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -4,7 +4,7 @@ "CLIPLEN_end": "85:990F", "CLIPLEN_no_multi": "85:990C", "CLIPSET": "85:FF1D", - "COLLECTTANK": "B8:80EF", + "COLLECTTANK": "B8:81E8", "MISCFX": "85:FF45", "NORMAL": "84:8BF2", "SETFX": "85:FF4E", @@ -22,6 +22,11 @@ "config_player_id": "CE:FF08", "config_remote_items": "CE:FF06", "config_sprite": "CE:FF02", + "copy_config_to_sram": "B8:8119", + "copy_memory": "B8:80FD", + "copy_memory_done": "B8:8117", + "copy_memory_even": "B8:8109", + "copy_memory_loop": "B8:810F", "h_item": "84:F894", "i_chozo_item": "84:F8AD", "i_hidden_item": "84:F8B4", @@ -30,11 +35,11 @@ "i_item_setup_shared_all_items": "B8:8878", "i_item_setup_shared_alwaysloaded": "B8:8883", "i_live_pickup": "84:FA79", - "i_live_pickup_multiworld": "B8:817F", - "i_live_pickup_multiworld_end": "B8:81C4", - "i_live_pickup_multiworld_local_item_or_offworld": "B8:819B", - "i_live_pickup_multiworld_own_item": "B8:81B0", - "i_live_pickup_multiworld_own_item1": "B8:81BC", + "i_live_pickup_multiworld": "B8:8278", + "i_live_pickup_multiworld_end": "B8:82BD", + "i_live_pickup_multiworld_local_item_or_offworld": "B8:8294", + "i_live_pickup_multiworld_own_item": "B8:82A9", + "i_live_pickup_multiworld_own_item1": "B8:82B5", "i_load_custom_graphics": "84:FA1E", "i_load_custom_graphics_all_items": "84:FA39", "i_load_custom_graphics_alwaysloaded": "84:FA49", @@ -69,22 +74,27 @@ "message_write_placeholders_loop": "85:B9CA", "message_write_placeholders_notfound": "85:B9DC", "message_write_placeholders_value_ok": "85:B9DF", - "mw_display_item_sent": "B8:8092", - "mw_handle_queue": "B8:80FF", - "mw_handle_queue_end": "B8:8178", - "mw_handle_queue_loop": "B8:8101", - "mw_handle_queue_new_remote_item": "B8:8151", - "mw_handle_queue_next": "B8:816D", - "mw_handle_queue_perform_receive": "B8:8163", - "mw_hook_main_game": "B8:81C8", + "mw_display_item_sent": "B8:818B", + "mw_handle_queue": "B8:81F8", + "mw_handle_queue_end": "B8:8271", + "mw_handle_queue_loop": "B8:81FA", + "mw_handle_queue_new_remote_item": "B8:824A", + "mw_handle_queue_next": "B8:8266", + "mw_handle_queue_perform_receive": "B8:825C", + "mw_hook_main_game": "B8:82C1", "mw_init": "B8:8011", - "mw_init_end": "B8:8044", + "mw_init_continuereset": "B8:8066", + "mw_init_end": "B8:80EA", "mw_init_memory": "B8:8000", - "mw_load_sram": "B8:8083", - "mw_receive_item": "B8:80B0", - "mw_receive_item_end": "B8:80E8", - "mw_save_sram": "B8:8070", - "mw_write_message": "B8:8049", + "mw_init_reset_sram": "B8:803B", + "mw_init_smstringdata": "B8:8051", + "mw_load_sram": "B8:8174", + "mw_load_sram_done": "B8:8182", + "mw_load_sram_setnewgame": "B8:8185", + "mw_receive_item": "B8:81A9", + "mw_receive_item_end": "B8:81E1", + "mw_save_sram": "B8:8169", + "mw_write_message": "B8:8142", "nonprog_item_eight_palette_indices": "84:F888", "offworld_graphics_data_item": "89:9200", "offworld_graphics_data_progression_item": "89:9100", @@ -109,7 +119,7 @@ "p_visible_item_end": "84:F96E", "p_visible_item_loop": "84:F95B", "p_visible_item_trigger": "84:F967", - "patch_load_multiworld": "B8:81DF", + "patch_load_multiworld": "B8:82D8", "perform_item_pickup": "84:FA7E", "plm_graphics_entry_offworld_item": "84:F886", "plm_graphics_entry_offworld_progression_item": "84:F87C", @@ -128,14 +138,24 @@ "start_item_data_reserve": "B8:C818", "update_graphic": "B8:C856", "v_item": "84:F890", + "write_repeated_memory": "B8:80EF", + "write_repeated_memory_loop": "B8:80F4", "ITEM_RAM": "7E:09A2", "SRAM_MW_ITEMS_RECV": "70:2000", - "SRAM_MW_ITEMS_RECV_RPTR": "70:2600", - "SRAM_MW_ITEMS_RECV_WPTR": "70:2602", - "SRAM_MW_ITEMS_RECV_SPTR": "70:2604", - "SRAM_MW_ITEMS_SENT_RPTR": "70:2680", - "SRAM_MW_ITEMS_SENT_WPTR": "70:2682", + "SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602", + "ReceiveQueueCompletedCount_InRamThatGetsSavedToSaveSlot": "7e:d8ae", + "SRAM_MW_ITEMS_SENT_RCOUNT": "70:2680", + "SRAM_MW_ITEMS_SENT_WCOUNT": "70:2682", "SRAM_MW_ITEMS_SENT": "70:2700", - "SRAM_MW_INITIALIZED": "70:26fe", + "SRAM_MW_SM": "70:3000", + "SRAM_MW_ROMTITLE": "70:3015", + "SRAM_MW_SEEDINT": "70:3060", + "SRAM_MW_INITIALIZED": "70:3064", + "SRAM_MW_CONFIG_ENABLED": "70:3070", + "SRAM_MW_CONFIG_CUSTOM_SPRITE": "70:3072", + "SRAM_MW_CONFIG_DEATHLINK": "70:3074", + "SRAM_MW_CONFIG_REMOTE_ITEMS": "70:3076", + "SRAM_MW_CONFIG_PLAYER_ID": "70:3078", + "varia_seedint_location": "df:ff00", "CollectedItems": "7E:D86E" } \ No newline at end of file From 4c94bb0ad57a1c3b7ab7caaf8f0ab249ba787d00 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:44:09 +0000 Subject: [PATCH 28/62] WebHost: sort game list case-insensitively again --- Utils.py | 4 ++-- WebHost.py | 2 +- WebHostLib/templates/supportedGames.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Utils.py b/Utils.py index c621e31c9a..4b2300a870 100644 --- a/Utils.py +++ b/Utils.py @@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset def sorter(element: str) -> str: parts = element.split(maxsplit=1) if parts[0].lower() in ignore: - return parts[1] + return parts[1].lower() else: - return element + return element.lower() return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index db802193a6..2ce0764214 100644 --- a/WebHost.py +++ b/WebHost.py @@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"]) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index fe81463a46..82f6348db2 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -1,7 +1,7 @@ {% extends 'pageWrapper.html' %} {% block head %} - Player Settings + Supported Games {% endblock %} From cc8ce32c61b30df6d8ee5f3563218fb8ddf76d0c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 09:21:47 +0200 Subject: [PATCH 29/62] Options: fix corner case where Toggle.value and Toggle.__int__ would be bool Which lead to a connect failure in Raft --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index a4f559a532..7eb108c99d 100644 --- a/Options.py +++ b/Options.py @@ -298,7 +298,7 @@ class Toggle(NumericOption): if type(data) == str: return cls.from_text(data) else: - return cls(data) + return cls(int(data)) @classmethod def get_option_name(cls, value): From 6d6111de2a91c66a73f81bc6ad4c6021ac9f2aee Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 02:28:46 +0200 Subject: [PATCH 30/62] Launcher: add ModuleUpdate --- Launcher.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Launcher.py b/Launcher.py index 53032ea251..92f43cd26c 100644 --- a/Launcher.py +++ b/Launcher.py @@ -10,16 +10,20 @@ Scroll down to components= to add components to the launcher as well as setup.py import argparse -from os.path import isfile -import sys -from typing import Iterable, Sequence, Callable, Union, Optional -import subprocess import itertools -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\ - is_windows, is_macos, is_linux -from shutil import which import shlex +import subprocess +import sys from enum import Enum, auto +from os.path import isfile +from shutil import which +from typing import Iterable, Sequence, Callable, Union, Optional + +import ModuleUpdate +ModuleUpdate.update() + +from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ + is_windows, is_macos, is_linux def open_host_yaml(): From b1ffbc49c97334bfaff06fb4951a2277c30c362e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 28 Aug 2022 18:30:19 +0200 Subject: [PATCH 31/62] LttPAdjuster: fix GUI for invalid sprite files (#885) * LttPAdjuster: ignore invalid sprite files * LttPAdjuster: ignore .gitignore in sprites * LttPAdjuster: log and show message for invalid sprites * Alttp: set sprite.valid to False for bad zspr and apsprite ... ... when throwing exceptions Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- LttPAdjuster.py | 16 +++++++++- worlds/alttp/Rom.py | 74 +++++++++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3de6e3b13a..f516a20ec0 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -752,6 +752,7 @@ class SpriteSelector(): self.window['pady'] = 5 self.spritesPerRow = 32 self.all_sprites = [] + self.invalid_sprites = [] self.sprite_pool = spritePool def open_custom_sprite_dir(_evt): @@ -833,6 +834,13 @@ class SpriteSelector(): self.window.focus() tkinter_center_window(self.window) + if self.invalid_sprites: + invalid = sorted(self.invalid_sprites) + logging.warning(f"The following sprites are invalid: {', '.join(invalid)}") + msg = f"{invalid[0]} " + msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid" + messagebox.showerror("Invalid sprites detected", msg, parent=self.window) + def remove_from_sprite_pool(self, button, spritename): self.callback(("remove", spritename)) self.spritePoolButtons.buttons.remove(button) @@ -897,7 +905,13 @@ class SpriteSelector(): sprites = [] for file in os.listdir(path): - sprites.append((file, Sprite(os.path.join(path, file)))) + if file == '.gitignore': + continue + sprite = Sprite(os.path.join(path, file)) + if sprite.valid: + sprites.append((file, sprite)) + else: + self.invalid_sprites.append(file) sprites.sort(key=lambda s: str.lower(s[1].name or "").strip()) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index dd5cc8c4dc..9ca3a355e1 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.Options import smallkey_shuffle @@ -551,18 +551,22 @@ class Sprite(): Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette def from_ap_sprite(self, filedata): - filedata = filedata.decode("utf-8-sig") - import yaml - obj = yaml.safe_load(filedata) - if obj["min_format_version"] > 1: - raise Exception("Sprite file requires an updated reader.") - self.author_name = obj["author"] - self.name = obj["name"] - if obj["data"]: # skip patching for vanilla content - data = bsdiff4.patch(Sprite.base_data, obj["data"]) - self.sprite = data[:self.sprite_size] - self.palette = data[self.sprite_size:self.palette_size] - self.glove_palette = data[self.sprite_size + self.palette_size:] + # noinspection PyBroadException + try: + obj = parse_yaml(filedata.decode("utf-8-sig")) + if obj["min_format_version"] > 1: + raise Exception("Sprite file requires an updated reader.") + self.author_name = obj["author"] + self.name = obj["name"] + if obj["data"]: # skip patching for vanilla content + data = bsdiff4.patch(Sprite.base_data, obj["data"]) + self.sprite = data[:self.sprite_size] + self.palette = data[self.sprite_size:self.palette_size] + self.glove_palette = data[self.sprite_size + self.palette_size:] + except Exception: + logger = logging.getLogger("apsprite") + logger.exception("Error parsing apsprite file") + self.valid = False @property def author_game_display(self) -> str: @@ -659,7 +663,7 @@ class Sprite(): @staticmethod def parse_zspr(filedata, expected_kind): - logger = logging.getLogger('ZSPR') + logger = logging.getLogger("ZSPR") headerstr = "<4xBHHIHIHH6x" headersize = struct.calcsize(headerstr) if len(filedata) < headersize: @@ -667,7 +671,7 @@ class Sprite(): version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from( headerstr, filedata) if version not in [1]: - logger.error('Error parsing ZSPR file: Version %g not supported', version) + logger.error("Error parsing ZSPR file: Version %g not supported", version) return None if kind != expected_kind: return None @@ -676,36 +680,42 @@ class Sprite(): stream.seek(headersize) def read_utf16le(stream): - "Decodes a null-terminated UTF-16_LE string of unknown size from a stream" + """Decodes a null-terminated UTF-16_LE string of unknown size from a stream""" raw = bytearray() while True: char = stream.read(2) - if char in [b'', b'\x00\x00']: + if char in [b"", b"\x00\x00"]: break raw += char - return raw.decode('utf-16_le') + return raw.decode("utf-16_le") - sprite_name = read_utf16le(stream) - author_name = read_utf16le(stream) - author_credits_name = stream.read().split(b"\x00", 1)[0].decode() + # noinspection PyBroadException + try: + sprite_name = read_utf16le(stream) + author_name = read_utf16le(stream) + author_credits_name = stream.read().split(b"\x00", 1)[0].decode() - # Ignoring the Author Rom name for the time being. + # Ignoring the Author Rom name for the time being. - real_csum = sum(filedata) % 0x10000 - if real_csum != csum or real_csum ^ 0xFFFF != icsum: - logger.warning('ZSPR file has incorrect checksum. It may be corrupted.') + real_csum = sum(filedata) % 0x10000 + if real_csum != csum or real_csum ^ 0xFFFF != icsum: + logger.warning("ZSPR file has incorrect checksum. It may be corrupted.") - sprite = filedata[sprite_offset:sprite_offset + sprite_size] - palette = filedata[palette_offset:palette_offset + palette_size] + sprite = filedata[sprite_offset:sprite_offset + sprite_size] + palette = filedata[palette_offset:palette_offset + palette_size] - if len(sprite) != sprite_size or len(palette) != palette_size: - logger.error('Error parsing ZSPR file: Unexpected end of file') + if len(sprite) != sprite_size or len(palette) != palette_size: + logger.error("Error parsing ZSPR file: Unexpected end of file") + return None + + return sprite, palette, sprite_name, author_name, author_credits_name + + except Exception: + logger.exception("Error parsing ZSPR file") return None - return (sprite, palette, sprite_name, author_name, author_credits_name) - def decode_palette(self): - "Returns the palettes as an array of arrays of 15 colors" + """Returns the palettes as an array of arrays of 15 colors""" def array_chunk(arr, size): return list(zip(*[iter(arr)] * size)) From 26aed9351ee73696e34d2172f66748508ec87840 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Aug 2022 20:58:26 -0700 Subject: [PATCH 32/62] Factorio: Fix a bug with single craft free samples. (#974) --- worlds/factorio/data/mod_template/control.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 63473808c6..51cd21e4da 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e function add_samples(force, name, count) local function add_to_table(t) + if count <= 0 then + -- Fixes a bug with single craft, if a recipe gives 0 of a given item. + return + end t[name] = (t[name] or 0) + count end -- Add to global table of earned samples for future new players From 3eb9e7050f7dae3c8da5f53b38b1fb60b9385f24 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:04:02 -0400 Subject: [PATCH 33/62] DKC3: Fix Wrinkly Softlock (#963) --- worlds/dkc3/Rom.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 90c4507e44..2bb5221a60 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -502,6 +502,10 @@ def patch_rom(world, rom, player, active_level_list): # Make Swanky free rom.write_byte(0x348C48, 0x00) + rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA])) + # Banana Bird Costs if world.goal[player] == "banana_bird_hunt": banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0) From 45fb7353208f272822cde22719aa4d404d66a3e0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 29 Aug 2022 17:16:13 -0500 Subject: [PATCH 34/62] Clients: allow games without datapackage (#978) --- CommonClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index f830035425..5af8e8cd88 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -345,6 +345,8 @@ class CommonContext: cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {}) needed_updates: typing.Set[str] = set() for game in relevant_games: + if game not in remote_datepackage_versions: + continue remote_version: int = remote_datepackage_versions[game] if remote_version == 0: # custom datapackage for this game From 4a2a184db11ff97a32c1f15106db191460cd234a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:12:33 +0200 Subject: [PATCH 35/62] Core: remove game-specific arguments from Generate (#971) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 11 +---------- Main.py | 1 - worlds/alttp/EntranceRandomizer.py | 2 -- worlds/alttp/__init__.py | 13 +++++++++---- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Generate.py b/Generate.py index 1cad836345..d13a78b375 100644 --- a/Generate.py +++ b/Generate.py @@ -63,7 +63,7 @@ class PlandoSettings(enum.IntFlag): def __str__(self) -> str: if self.value: - return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value) return "Off" @@ -84,11 +84,6 @@ def mystery_argparse(): parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1)) parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) - parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], - help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path - parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], - help="Path to the 1.0 JP SM Baserom.") - parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path)) parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path), help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults["race"]) @@ -183,10 +178,6 @@ def main(args=None, callback=ERmain): Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) - erargs.lttp_rom = args.lttp_rom - erargs.sm_rom = args.sm_rom - erargs.enemizercli = args.enemizercli - settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) for fname, yamls in weights_cache.items()} diff --git a/Main.py b/Main.py index 48095e06bd..acff74595a 100644 --- a/Main.py +++ b/Main.py @@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() world.player_name = args.name.copy() - world.enemizer = args.enemizercli world.sprite = args.sprite.copy() world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index f1748b9a0f..47c36b6cde 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False): Alternatively, can be a ALttP Rom patched with a Link sprite that will be extracted. ''') - parser.add_argument('--gui', help='Launch the GUI', action='store_true') - parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos', "singularity"]) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index e7f111c3b7..3e32584352 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -4,6 +4,7 @@ import random import threading import typing +import Utils from BaseClasses import Item, CollectionState, Tutorial from .Dungeons import create_dungeons from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect @@ -136,6 +137,10 @@ class ALTTPWorld(World): create_items = generate_itempool + enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \ + if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \ + else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"]) + def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() self.dungeon_specific_item_names = set() @@ -150,12 +155,12 @@ class ALTTPWorld(World): raise FileNotFoundError(rom_file) def generate_early(self): + if self.use_enemizer(): + check_enemizer(self.enemizer_path) + player = self.player world = self.world - if self.use_enemizer(): - check_enemizer(world.enemizer) - # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -360,7 +365,7 @@ class ALTTPWorld(World): patch_rom(world, rom, player, use_enemizer) if use_enemizer: - patch_enemizer(world, player, rom, world.enemizer, output_directory) + patch_enemizer(world, player, rom, self.enemizer_path, output_directory) if world.is_race: patch_race_rom(rom, world, player) From 60d1a27079239bbcf08337e3322a4ab632480e2d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:14:34 +0200 Subject: [PATCH 36/62] Subnautica: revamp aggressive creature scans (#966) * add forgotten aggressive creatures * fix logic requirements * added option to opt out of aggressive creature scans --- worlds/subnautica/Creatures.py | 29 +++++++++++++++++++++++++++-- worlds/subnautica/Locations.py | 7 ++++++- worlds/subnautica/Options.py | 15 ++++++++++++++- worlds/subnautica/Rules.py | 28 ++++++++++++++++++++++------ worlds/subnautica/__init__.py | 17 +++++++++-------- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index a9f5e850e1..687c3732a9 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -1,3 +1,4 @@ +import functools from typing import Dict, Set, List # EN Locale Creature Name to rough depth in meters found at @@ -58,13 +59,18 @@ all_creatures: Dict[str, int] = { aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", + "Biter", "Bleeder", + "Blighter", + "Blood Crawler", "Mesmer", "Reaper Leviathan", "Crabsquid", "Warper", "Crabsnake", "Ampeel", + "Stalker", + "Sand Shark", "Boneshark", "Lava Lizard", "Sea Dragon Leviathan", @@ -94,6 +100,25 @@ 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) -all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] +class Definitions: + """Only compute lists if needed and then cache them.""" + + @functools.cached_property + def all_creatures_presorted(self) -> List[str]: + return sorted(all_creatures) + + @functools.cached_property + def all_creatures_presorted_without_containment(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in containment] + + @functools.cached_property + def all_creatures_presorted_without_stasis(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable] + + @functools.cached_property + def all_creatures_presorted_without_aggressive(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive] + +# only singleton needed +Definitions: Definitions = Definitions() diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 3effd1eac3..2dfeaf3bf3 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False): need_propulsion_cannon: bool -events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] +events: List[str] = [ + "Neptune Launch", + "Disable Quarantine", + "Full Infection", + "Repair Aurora Drive", +] location_table: Dict[int, LocationDict] = { 33000: {'can_slip_through': False, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f68e12d2c0..57bd23fdb7 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,7 +1,7 @@ import typing from Options import Choice, Range, DeathLink -from .Creatures import all_creatures +from .Creatures import all_creatures, Definitions class ItemPool(Choice): @@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice): Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. None: Aggressive Creatures are assumed to not need any tools to scan. + Removed: No Creatures needing Stasis or Containment will be in the pool at all. Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper. Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" display_name = "Aggressive Creature Scan Logic" option_stasis = 0 option_containment = 1 option_either = 2 option_none = 3 + option_removed = 4 + + def get_pool(self) -> typing.List[str]: + if self == self.option_removed: + return Definitions.all_creatures_presorted_without_aggressive + elif self == self.option_stasis: + return Definitions.all_creatures_presorted_without_containment + elif self == self.option_containment: + return Definitions.all_creatures_presorted_without_stasis + else: + return Definitions.all_creatures_presorted class SubnauticaDeathLink(DeathLink): diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 20c6a35c84..8925f1e829 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING, Dict, Callable +from typing import TYPE_CHECKING, Dict, Callable, Optional from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict -from .Creatures import all_creatures, aggressive, suffix +from .Creatures import all_creatures, aggressive, suffix, hatchable, containment from .Options import AggressiveScanLogic import math @@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location": return location +def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \ + Optional[Callable[["CollectionState", int], bool]]: + """Get logic rule for a creature scan location.""" + if creature_name not in hatchable and option != option.option_none: # can only be done via stasis + return has_stasis_rifle + # otherwise allow option preference + return aggression_rules.get(option.value, None) + + aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { AggressiveScanLogic.option_stasis: has_stasis_rifle, AggressiveScanLogic.option_containment: has_containment, @@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(world, player, loc) if subnautica_world.creatures_to_scan: - aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + option = world.creature_scan_logic[player] + for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(world, player, creature_name) - if creature_name in aggressive and aggressive_rule: - add_rule(location, lambda state: aggressive_rule(state, player)) + if creature_name in containment: # there is no other way, hard-required containment + add_rule(location, lambda state: has_containment(state, player)) + elif creature_name in aggressive: + rule = get_aggression_rule(option, creature_name) + if rule: + add_rule(location, + lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player)) # Victory locations - set_rule(world.get_location("Neptune Launch", player), lambda state: + set_rule(world.get_location("Neptune Launch", player), + lambda state: get_max_depth(state, player) >= 1444 and has_mobile_vehicle_bay(state, player) and state.has("Neptune Launch Platform", player) and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 806c1b195e..a4447ccbc1 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -52,14 +52,15 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: - valid_creatures = Creatures.all_creatures_presorted_without_containment - self.world.creature_scans[self.player].value = min(len( - Creatures.all_creatures_presorted_without_containment), - self.world.creature_scans[self.player].value) - else: - valid_creatures = Creatures.all_creatures_presorted - self.creatures_to_scan = self.world.random.sample(valid_creatures, + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] + creature_pool = scan_option.get_pool() + + self.world.creature_scans[self.player].value = min( + len(creature_pool), + self.world.creature_scans[self.player].value + ) + + self.creatures_to_scan = self.world.random.sample(creature_pool, self.world.creature_scans[self.player].value) def create_regions(self): From 2a7babce6871797a1f037be4e5a8bd828fe2c505 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Tue, 30 Aug 2022 08:16:21 -0700 Subject: [PATCH 37/62] SM+SMZ3: don't abandon checks that happen while disconnected from AP (#946) --- SNIClient.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index aad231691b..c97d0d6c0d 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -149,8 +149,8 @@ class Context(CommonContext): def event_invalid_slot(self): if self.snes_socket is not None and not self.snes_socket.closed: asyncio.create_task(self.snes_socket.close()) - raise Exception('Invalid ROM detected, ' - 'please verify that you have loaded the correct rom and reconnect your snes (/snes)') + raise Exception("Invalid ROM detected, " + "please verify that you have loaded the correct rom and reconnect your snes (/snes)") async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -158,7 +158,7 @@ class Context(CommonContext): if self.rom is None: self.awaiting_rom = True snes_logger.info( - 'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)') + "No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)") return self.awaiting_rom = False self.auth = self.rom @@ -262,7 +262,7 @@ async def deathlink_kill_player(ctx: Context): SNES_RECONNECT_DELAY = 5 -# LttP +# FXPAK Pro protocol memory mapping used by SNI ROM_START = 0x000000 WRAM_START = 0xF50000 WRAM_SIZE = 0x20000 @@ -293,21 +293,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5 DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte # SM -SM_ROMNAME_START = 0x007FC0 +SM_ROMNAME_START = ROM_START + 0x007FC0 SM_INGAME_MODES = {0x07, 0x09, 0x0b} SM_ENDGAME_MODES = {0x26, 0x27} SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A} -SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes -SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte -SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte +# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue +SM_RECV_QUEUE_START = SRAM_START + 0x2000 +SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602 +SM_SEND_QUEUE_START = SRAM_START + 0x2700 +SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680 +SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682 SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte # SMZ3 -SMZ3_ROMNAME_START = 0x00FFC0 +SMZ3_ROMNAME_START = ROM_START + 0x00FFC0 SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b} SMZ3_ENDGAME_MODES = {0x26, 0x27} @@ -1083,6 +1086,9 @@ async def game_watcher(ctx: Context): if ctx.awaiting_rom: await ctx.server_auth(False) + elif ctx.server is None: + snes_logger.warning("ROM detected but no active multiworld server connection. " + + "Connect using command: /connect server:port") if ctx.auth and ctx.auth != ctx.rom: snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -1159,6 +1165,9 @@ async def game_watcher(ctx: Context): await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) await track_locations(ctx, roomid, roomdata) elif ctx.game == GAME_SM: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in SM_DEATH_MODES @@ -1169,22 +1178,22 @@ async def game_watcher(ctx: Context): ctx.finished_game = True continue - data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4) if data is None: continue recv_index = data[0] | (data[1] << 8) - recv_item = data[2] | (data[3] << 8) + recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT while (recv_index < recv_item): itemAdress = recv_index * 8 - message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8) + message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8) # worldId = message[0] | (message[1] << 8) # unused # itemId = message[2] | (message[3] << 8) # unused itemIndex = (message[4] | (message[5] << 8)) >> 3 recv_index += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, + snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from worlds.sm.Locations import locations_start_id @@ -1196,12 +1205,11 @@ async def game_watcher(ctx: Context): f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2) if data is None: continue - # recv_itemOutPtr = data[0] | (data[1] << 8) # unused - itemOutPtr = data[2] | (data[3] << 8) + itemOutPtr = data[0] | (data[1] << 8) from worlds.sm.Items import items_start_id from worlds.sm.Locations import locations_start_id @@ -1214,10 +1222,10 @@ async def game_watcher(ctx: Context): locationId = 0x00 #backward compat playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes( + snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes( [playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF])) itemOutPtr += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, + snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), @@ -1225,6 +1233,9 @@ async def game_watcher(ctx: Context): ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) elif ctx.game == GAME_SMZ3: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2) if (currentGame is not None): if (currentGame[0] != 0): From a753905ee4536e4c28ef25dd0b230fe03fa683b1 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:54:40 -0700 Subject: [PATCH 38/62] OoT bug fixes (#955) * OoT: fix shop patching crash due to Item changes * OoT: more informative failure in triforce piece replacement * OoT: in triforce hunt, remove ganon BK from pool and lock the door * OoT: no longer store trap information on the item --- worlds/oot/ItemPool.py | 4 ++++ worlds/oot/Items.py | 1 - worlds/oot/Options.py | 4 ++-- worlds/oot/Patches.py | 30 ++++++++++++++---------------- worlds/oot/__init__.py | 11 ++++++++--- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 301c502a7e..12c9c26292 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1388,6 +1388,10 @@ def get_pool_core(world): remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap'] junk_candidates = [item for item in pool if item in remove_junk_pool] + if len(pending_junk_pool) > len(junk_candidates): + excess = len(pending_junk_pool) - len(junk_candidates) + if world.triforce_hunt: + raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).") while pending_junk_pool: pending_item = pending_junk_pool.pop() if not junk_candidates: diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index 31e6c31f62..06164091a7 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -49,7 +49,6 @@ class OOTItem(Item): self.type = type self.index = index self.special = special or {} - self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 50b6c26c86..ea9a8160fb 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -158,12 +158,12 @@ class TriforceGoal(Range): """Number of Triforce pieces required to complete the game.""" display_name = "Required Triforce Pieces" range_start = 1 - range_end = 100 + range_end = 80 default = 20 class ExtraTriforces(Range): - """Percentage of additional Triforce pieces in the pool, separate from the item pool setting.""" + """Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items.""" display_name = "Percentage of Extra Triforce Pieces" range_start = 0 range_end = 100 diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 7bf31c4f7a..322d2d838a 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item): def get_override_table(world): - return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player)))) + return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player)))) override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c @@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table): return b''.join(sorted(itertools.starmap(override_struct.pack, override_table))) -def get_override_entry(player_id, location): +def get_override_entry(ootworld, location): scene = location.scene default = location.default - player_id = 0 if player_id == location.item.player else min(location.item.player, 255) + player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255) if location.item.game != 'Ocarina of Time': # This is an AP sendable. It's guaranteed to not be None. if location.item.advancement: @@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location): if location.item.trap: item_id = 0x7C # Ice Trap ID, to get "X is a fool" message - looks_like_item_id = location.item.looks_like_item.index + looks_like_item_id = ootworld.trap_appearances[location.address].index else: looks_like_item_id = 0 @@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world): return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] # If boss door, set the door's unlock flag - if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A): + if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or ( + world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt): if actor_id == 0x002E and actor_type == 0x05: return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] @@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F rom.write_int16(location.address1, location.item.index) else: if location.item.trap: - item_display = location.item.looks_like_item - elif location.item.game != "Ocarina of Time": - item_display = location.item - if location.item.advancement: - item_display.index = 0xCB - else: - item_display.index = 0xCC - item_display.special = {} + item_display = world.trap_appearances[location.address] else: item_display = location.item # bottles in shops should look like empty bottles # so that that are different than normal shop refils - if 'shop_object' in item_display.special: - rom_item = read_rom_item(rom, item_display.special['shop_object']) + if location.item.trap or location.item.game == "Ocarina of Time": + if 'shop_object' in item_display.special: + rom_item = read_rom_item(rom, item_display.special['shop_object']) + else: + rom_item = read_rom_item(rom, item_display.index) else: - rom_item = read_rom_item(rom, item_display.index) + display_index = 0xCB if location.item.advancement else 0xCC + rom_item = read_rom_item(rom, display_index) shop_objs.add(rom_item['object_id']) shop_id = world.current_shop_id diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b4635ad77f..a9b7d5a1b6 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -178,6 +178,10 @@ class OOTWorld(World): if self.skip_child_zelda: self.shuffle_weird_egg = False + # Ganon boss key should not be in itempool in triforce hunt + if self.triforce_hunt: + self.shuffle_ganon_bosskey = 'remove' + # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] @@ -803,9 +807,10 @@ class OOTWorld(World): with i_o_limiter: # Make traps appear as other random items - ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap] - for trap in ice_traps: - trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) + 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.world.slot_seeds[self.player].choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also self.hint_rng = self.world.slot_seeds[self.player] From fcfc2c2e100189e363cff2f47a30b0bbc8154c57 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:10:18 +0200 Subject: [PATCH 39/62] WebHost: fix local_path on python 3.8 (#981) * WebHost: fix local_path on python 3.8 `__file__` is relative in 3.8, so `os.path.dirname(__file__)` ends up being an empty string breaking calls to `local_path()` (without arguments) * WebHost: add comment to local_path override --- WebHost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHost.py b/WebHost.py index 2ce0764214..4c07e8b185 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,7 +12,7 @@ ModuleUpdate.update() # in case app gets imported by something like gunicorn import Utils -Utils.local_path.cached_path = os.path.dirname(__file__) +Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 from WebHostLib import register, app as raw_app from waitress import serve From 8da1cfeeb752b1aee83ee1ec53e9da8493bc544e Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:14:17 -0400 Subject: [PATCH 40/62] SM: remove events from data package (#973) --- SNIClient.py | 6 +++--- worlds/sm/Items.py | 14 -------------- worlds/sm/Locations.py | 14 -------------- worlds/sm/__init__.py | 17 ++++++++--------- 4 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 worlds/sm/Items.py delete mode 100644 worlds/sm/Locations.py diff --git a/SNIClient.py b/SNIClient.py index c97d0d6c0d..3d90fafc17 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1196,7 +1196,7 @@ async def game_watcher(ctx: Context): snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - from worlds.sm.Locations import locations_start_id + from worlds.sm import locations_start_id location_id = locations_start_id + itemIndex ctx.locations_checked.add(location_id) @@ -1211,8 +1211,8 @@ async def game_watcher(ctx: Context): itemOutPtr = data[0] | (data[1] << 8) - from worlds.sm.Items import items_start_id - from worlds.sm.Locations import locations_start_id + from worlds.sm import items_start_id + from worlds.sm import locations_start_id if itemOutPtr < len(ctx.items_received): item = ctx.items_received[itemOutPtr] itemId = item.item - items_start_id diff --git a/worlds/sm/Items.py b/worlds/sm/Items.py deleted file mode 100644 index ff8970b64d..0000000000 --- a/worlds/sm/Items.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.rando.Items import ItemManager - -items_start_id = 83000 - -def gen_special_id(): - special_id_value_start = 32 - while True: - yield special_id_value_start - special_id_value_start += 1 - -gen_run = gen_special_id() - -lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items()) -lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/Locations.py b/worlds/sm/Locations.py deleted file mode 100644 index 4e80ab00e6..0000000000 --- a/worlds/sm/Locations.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.graph.location import locationsDict - -locations_start_id = 82000 - -def gen_boss_id(): - boss_id_value_start = 256 - while True: - yield boss_id_value_start - boss_id_value_start += 1 - -gen_run = gen_boss_id() - -lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items()) -lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 5da1c40f75..fbf3825e0f 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -11,8 +11,6 @@ from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils logger = logging.getLogger("Super Metroid") -from .Locations import lookup_name_to_id as locations_lookup_name_to_id -from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options @@ -68,6 +66,8 @@ class SMWeb(WebWorld): ["Farrak Kilhn"] )] +locations_start_id = 82000 +items_start_id = 83000 class SMWorld(World): """ @@ -78,12 +78,11 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 1 + data_version = 2 option_definitions = sm_options - item_names: Set[str] = frozenset(items_lookup_name_to_id) - location_names: Set[str] = frozenset(locations_lookup_name_to_id) - item_name_to_id = items_lookup_name_to_id - location_name_to_id = locations_lookup_name_to_id + + item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} + location_name_to_id = {key: locations_start_id + value.Id for key, value in locationsDict.items() if value.Id != None} web = SMWeb() remote_items: bool = False @@ -701,8 +700,8 @@ class SMWorld(World): dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss])) def create_locations(self, player: int): - for name, id in locations_lookup_name_to_id.items(): - self.locations[name] = SMLocation(player, name, id) + for name in locationsDict: + self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None)) def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): From c617bba95993a8b7099a678b59ff1062d932266d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 31 Aug 2022 20:55:15 +0200 Subject: [PATCH 41/62] SC2: client revamp (#967) SC2 client now relies almost entirely on the map file and server for the locations and just facilitates them, should make it significantly more resilient to objectives being added or removed * SC2: fix client crash on printjson messages with more [ than ] * SC2: move text to queue, that actually clears memory * SC2: Announce which mission is being loaded Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- CommonClient.py | 7 +- Starcraft2Client.py | 431 ++++++++++++++------------------- Utils.py | 2 +- worlds/sc2wol/MissionTables.py | 4 +- worlds/sc2wol/__init__.py | 1 + 5 files changed, 187 insertions(+), 258 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 5af8e8cd88..574da16f2a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -152,8 +152,9 @@ class CommonContext: # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] - missing_locations: typing.Set[int] + missing_locations: typing.Set[int] # server state checked_locations: typing.Set[int] # server state + server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations locations_info: typing.Dict[int, NetworkItem] # internals @@ -184,8 +185,9 @@ class CommonContext: self.locations_checked = set() # local state self.locations_scouted = set() self.items_received = [] - self.missing_locations = set() + self.missing_locations = set() # server state self.checked_locations = set() # server state + self.server_locations = set() # all locations the server knows of, missing_location | checked_locations self.locations_info = {} self.input_queue = asyncio.Queue() @@ -634,6 +636,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): # when /missing is used for the client side view of what is missing. ctx.missing_locations = set(args["missing_locations"]) ctx.checked_locations = set(args["checked_locations"]) + ctx.server_locations = ctx.missing_locations | ctx. checked_locations elif cmd == 'ReceivedItems': start_index = args["index"] diff --git a/Starcraft2Client.py b/Starcraft2Client.py index dc63e9a456..b8f6086914 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -1,31 +1,31 @@ from __future__ import annotations -import multiprocessing -import logging import asyncio +import copy +import ctypes +import logging +import multiprocessing import os.path +import re +import sys +import typing +import queue +from pathlib import Path import nest_asyncio import sc2 - -from sc2.main import run_game -from sc2.data import Race from sc2.bot_ai import BotAI +from sc2.data import Race +from sc2.main import run_game from sc2.player import Bot -from worlds.sc2wol.Regions import MissionInfo -from worlds.sc2wol.MissionTables import lookup_id_to_mission +from MultiServer import mark_raw +from Utils import init_logging, is_windows +from worlds.sc2wol import SC2WoLWorld from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET -from worlds.sc2wol import SC2WoLWorld - -from pathlib import Path -import re -from MultiServer import mark_raw -import ctypes -import sys - -from Utils import init_logging, is_windows +from worlds.sc2wol.MissionTables import lookup_id_to_mission +from worlds.sc2wol.Regions import MissionInfo if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -35,10 +35,12 @@ sc2_logger = logging.getLogger("Starcraft2") import colorama -from NetUtils import * +from NetUtils import ClientStatus, RawJSONtoTextParser from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser nest_asyncio.apply() +max_bonus: int = 8 +victory_modulo: int = 100 class StarcraftClientProcessor(ClientCommandProcessor): @@ -98,13 +100,13 @@ class StarcraftClientProcessor(ClientCommandProcessor): def _cmd_available(self) -> bool: """Get what missions are currently available to play""" - request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui) + request_available_missions(self.ctx) return True def _cmd_unfinished(self) -> bool: """Get what missions are currently available to play and have not had all locations checked""" - request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) + request_unfinished_missions(self.ctx) return True @mark_raw @@ -125,18 +127,19 @@ class SC2Context(CommonContext): items_handling = 0b111 difficulty = -1 all_in_choice = 0 - mission_req_table = None - items_rec_to_announce = [] - rec_announce_pos = 0 - items_sent_to_announce = [] - sent_announce_pos = 0 - announcements = [] - announcement_pos = 0 + mission_req_table: typing.Dict[str, MissionInfo] = {} + announcements = queue.Queue() sc2_run_task: typing.Optional[asyncio.Task] = None - missions_unlocked = False + missions_unlocked: bool = False # allow launching missions ignoring requirements current_tooltip = None last_loc_list = None difficulty_override = -1 + mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + raw_text_parser: RawJSONtoTextParser + + def __init__(self, *args, **kwargs): + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -149,30 +152,32 @@ class SC2Context(CommonContext): self.difficulty = args["slot_data"]["game_difficulty"] self.all_in_choice = args["slot_data"]["all_in_map"] slot_req_table = args["slot_data"]["mission_req"] - self.mission_req_table = {} - # Compatibility for 0.3.2 server data. - if "category" not in next(iter(slot_req_table)): - for i, mission_data in enumerate(slot_req_table.values()): - mission_data["category"] = wol_default_categories[i] - for mission in slot_req_table: - self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + self.mission_req_table = { + mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table + } + + self.build_location_to_mission_mapping() # Look for and set SC2PATH. # check_game_install_path() returns True if and only if it finds + sets SC2PATH. if "SC2PATH" not in os.environ and check_game_install_path(): check_mod_install() - if cmd in {"PrintJSON"}: - if "receiving" in args: - if self.slot_concerns_self(args["receiving"]): - self.announcements.append(args["data"]) - return - if "item" in args: - if self.slot_concerns_self(args["item"].player): - self.announcements.append(args["data"]) + def on_print_json(self, args: dict): + if "receiving" in args and self.slot_concerns_self(args["receiving"]): + relevant = True + elif "item" in args and self.slot_concerns_self(args["item"].player): + relevant = True + else: + relevant = False + + if relevant: + self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) + + super(SC2Context, self).on_print_json(args) def run_gui(self): - from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation + from kvui import GameManager, HoverBehavior, ServerToolTip from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -190,6 +195,7 @@ class SC2Context(CommonContext): class MissionButton(HoverableButton): tooltip_text = StringProperty("Test") + ctx: SC2Context def __init__(self, *args, **kwargs): super(HoverableButton, self).__init__(*args, **kwargs) @@ -210,10 +216,7 @@ class SC2Context(CommonContext): self.ctx.current_tooltip = self.layout def on_leave(self): - if self.ctx.current_tooltip: - App.get_running_app().root.remove_widget(self.ctx.current_tooltip) - - self.ctx.current_tooltip = None + self.ctx.ui.clear_tooltip() @property def ctx(self) -> CommonContext: @@ -235,13 +238,20 @@ class SC2Context(CommonContext): mission_panel = None last_checked_locations = {} mission_id_to_button = {} - launching = False + launching: typing.Union[bool, int] = False # if int -> mission ID refresh_from_launching = True first_check = True + ctx: SC2Context def __init__(self, ctx): super().__init__(ctx) + def clear_tooltip(self): + if self.ctx.current_tooltip: + App.get_running_app().root.remove_widget(self.ctx.current_tooltip) + + self.ctx.current_tooltip = None + def build(self): container = super().build() @@ -256,7 +266,7 @@ class SC2Context(CommonContext): def build_mission_table(self, dt): if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or - not self.refresh_from_launching)) or self.first_check: + not self.refresh_from_launching)) or self.first_check: self.refresh_from_launching = True self.mission_panel.clear_widgets() @@ -267,12 +277,7 @@ class SC2Context(CommonContext): self.mission_id_to_button = {} categories = {} - available_missions = [] - unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations, - self.ctx.mission_req_table, - self.ctx, available_missions=available_missions, - unfinished_locations=unfinished_locations) + available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) # separate missions into categories for mission in self.ctx.mission_req_table: @@ -283,7 +288,8 @@ class SC2Context(CommonContext): for category in categories: category_panel = MissionCategory() - category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1)) + category_panel.add_widget( + Label(text=category, size_hint_y=None, height=50, outline_width=1)) # Map is completed for mission in categories[category]: @@ -295,7 +301,9 @@ class SC2Context(CommonContext): text = f"[color=6495ED]{text}[/color]" tooltip = f"Uncollected locations:\n" - tooltip += "\n".join(location for location in unfinished_locations[mission]) + tooltip += "\n".join([self.ctx.location_names[loc] for loc in + self.ctx.locations_for_mission(mission) + if loc in self.ctx.missing_locations]) elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met @@ -303,7 +311,7 @@ class SC2Context(CommonContext): text = f"[color=a9a9a9]{text}[/color]" tooltip = f"Requires: " if len(self.ctx.mission_req_table[mission].required_world) > 0: - tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for + tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for req_mission in self.ctx.mission_req_table[mission].required_world) @@ -325,13 +333,17 @@ class SC2Context(CommonContext): self.refresh_from_launching = False self.mission_panel.clear_widgets() - self.mission_panel.add_widget(Label(text="Launching Mission")) + self.mission_panel.add_widget(Label(text="Launching Mission: " + + lookup_id_to_mission[self.launching])) + if self.ctx.ui: + self.ctx.ui.clear_tooltip() def mission_callback(self, button): if not self.launching: - self.ctx.play_mission(list(self.mission_id_to_button.keys()) - [list(self.mission_id_to_button.values()).index(button)]) - self.launching = True + mission_id: int = list(self.mission_id_to_button.values()).index(button) + self.ctx.play_mission(list(self.mission_id_to_button) + [mission_id]) + self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) def finish_launching(self, dt): @@ -349,7 +361,7 @@ class SC2Context(CommonContext): def play_mission(self, mission_id): if self.missions_unlocked or \ - is_mission_available(mission_id, self.checked_locations, self.mission_req_table): + is_mission_available(self, mission_id): if self.sc2_run_task: if not self.sc2_run_task.done(): sc2_logger.warning("Starcraft 2 Client is still running!") @@ -358,12 +370,29 @@ class SC2Context(CommonContext): sc2_logger.warning("Launching Mission without Archipelago authentication, " "checks will not be registered to server.") self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), - name="Starcraft 2 Launch") + name="Starcraft 2 Launch") else: sc2_logger.info( f"{lookup_id_to_mission[mission_id]} is not currently unlocked. " f"Use /unfinished or /available to see what is available.") + def build_location_to_mission_mapping(self): + mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { + mission_info.id: set() for mission_info in self.mission_req_table.values() + } + + for loc in self.server_locations: + mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo) + mission_id_to_location_ids[mission_id].add(objective) + self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in + mission_id_to_location_ids.items()} + + def locations_for_mission(self, mission: str): + mission_id: int = self.mission_req_table[mission].id + objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id] + for objective in objectives: + yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective + async def main(): multiprocessing.freeze_support() @@ -459,11 +488,7 @@ def calc_difficulty(difficulty): return 'X' -async def starcraft_launch(ctx: SC2Context, mission_id): - ctx.rec_announce_pos = len(ctx.items_rec_to_announce) - ctx.sent_announce_pos = len(ctx.items_sent_to_announce) - ctx.announcements_pos = len(ctx.announcements) - +async def starcraft_launch(ctx: SC2Context, mission_id: int): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") with DllDirectory(None): @@ -472,32 +497,29 @@ async def starcraft_launch(ctx: SC2Context, mission_id): class ArchipelagoBot(sc2.bot_ai.BotAI): - game_running = False - mission_completed = False - first_bonus = False - second_bonus = False - third_bonus = False - fourth_bonus = False - fifth_bonus = False - sixth_bonus = False - seventh_bonus = False - eight_bonus = False - ctx: SC2Context = None - mission_id = 0 + game_running: bool = False + mission_completed: bool = False + boni: typing.List[bool] + setup_done: bool + ctx: SC2Context + mission_id: int can_read_game = False - last_received_update = 0 + last_received_update: int = 0 def __init__(self, ctx: SC2Context, mission_id): + self.setup_done = False self.ctx = ctx self.mission_id = mission_id + self.boni = [False for _ in range(max_bonus)] super(ArchipelagoBot, self).__init__() async def on_step(self, iteration: int): game_state = 0 - if iteration == 0: + if not self.setup_done: + self.setup_done = True start_items = calculate_items(self.ctx.items_received) if self.ctx.difficulty_override >= 0: difficulty = calc_difficulty(self.ctx.difficulty_override) @@ -511,36 +533,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): self.last_received_update = len(self.ctx.items_received) else: - if self.ctx.announcement_pos < len(self.ctx.announcements): - index = 0 - message = "" - while index < len(self.ctx.announcements[self.ctx.announcement_pos]): - message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"] - index += 1 - - index = 0 - start_rem_pos = -1 - # Remove unneeded [Color] tags - while index < len(message): - if message[index] == '[': - start_rem_pos = index - index += 1 - elif message[index] == ']' and start_rem_pos > -1: - temp_msg = "" - - if start_rem_pos > 0: - temp_msg = message[:start_rem_pos] - if index < len(message) - 1: - temp_msg += message[index + 1:] - - message = temp_msg - index += start_rem_pos - index - start_rem_pos = -1 - else: - index += 1 - + if not self.ctx.announcements.empty(): + message = self.ctx.announcements.get(timeout=1) await self.chat_send("SendMessage " + message) - self.ctx.announcement_pos += 1 + self.ctx.announcements.task_done() # Archipelago reads the health for unit in self.all_own_units(): @@ -568,169 +564,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): if game_state & (1 << 1) and not self.mission_completed: if self.mission_id != 29: print("Mission Completed") - await self.ctx.send_msgs([ - {"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}]) + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}]) self.mission_completed = True else: print("Game Complete") await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) self.mission_completed = True - if game_state & (1 << 2) and not self.first_bonus: - print("1st Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}]) - self.first_bonus = True - - if not self.second_bonus and game_state & (1 << 3): - print("2nd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}]) - self.second_bonus = True - - if not self.third_bonus and game_state & (1 << 4): - print("3rd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}]) - self.third_bonus = True - - if not self.fourth_bonus and game_state & (1 << 5): - print("4th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}]) - self.fourth_bonus = True - - if not self.fifth_bonus and game_state & (1 << 6): - print("5th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}]) - self.fifth_bonus = True - - if not self.sixth_bonus and game_state & (1 << 7): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}]) - self.sixth_bonus = True - - if not self.seventh_bonus and game_state & (1 << 8): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}]) - self.seventh_bonus = True - - if not self.eight_bonus and game_state & (1 << 9): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}]) - self.eight_bonus = True + for x, completed in enumerate(self.boni): + if not completed and game_state & (1 << (x + 2)): + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}]) + self.boni[x] = True else: await self.chat_send("LostConnection - Lost connection to game.") -def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx): - objectives_complete = 0 - - if missions_info[mission].extra_locations > 0: - for i in range(missions_info[mission].extra_locations): - if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done: - objectives_complete += 1 - else: - unfinished_locations[mission].append(ctx.location_names[ - missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i]) - - return objectives_complete - - else: - return -1 - - -def request_unfinished_missions(locations_done, location_table, ui, ctx): - if location_table: +def request_unfinished_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Unfinished Missions: " - unlocks = initialize_blank_mission_dict(location_table) - unfinished_locations = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks, - unfinished_locations=unfinished_locations) + _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) - message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " + + message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " + mark_up_objectives( - f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]", + f"[{len(unfinished_missions[mission])}/" + f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", ctx, unfinished_locations, mission) for mission in unfinished_missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None, - available_missions=[]): +def calc_unfinished_missions(ctx: SC2Context, unlocks=None): unfinished_missions = [] locations_completed = [] if not unlocks: - unlocks = initialize_blank_mission_dict(locations) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - if not unfinished_locations: - unfinished_locations = initialize_blank_mission_dict(locations) - - if len(available_missions) > 0: - available_missions = [] - - available_missions.extend(calc_available_missions(locations_done, locations, unlocks)) + available_missions = calc_available_missions(ctx, unlocks) for name in available_missions: - if not locations[name].extra_locations == -1: - objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx) - - if objectives_completed < locations[name].extra_locations: + objectives = set(ctx.locations_for_mission(name)) + if objectives: + objectives_completed = ctx.checked_locations & objectives + if len(objectives_completed) < len(objectives): unfinished_missions.append(name) locations_completed.append(objectives_completed) - else: + else: # infer that this is the final mission as it has no objectives unfinished_missions.append(name) locations_completed.append(-1) - return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))} + return available_missions, dict(zip(unfinished_missions, locations_completed)) -def is_mission_available(mission_id_to_check, locations_done, locations): - unfinished_missions = calc_available_missions(locations_done, locations) +def is_mission_available(ctx: SC2Context, mission_id_to_check): + unfinished_missions = calc_available_missions(ctx) - return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions) + return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions) -def mark_up_mission_name(mission, location_table, ui, unlock_table): +def mark_up_mission_name(ctx: SC2Context, mission, unlock_table): """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" - if location_table[mission].completion_critical: - if ui: + if ctx.mission_req_table[mission].completion_critical: + if ctx.ui: message = "[color=AF99EF]" + mission + "[/color]" else: message = "*" + mission + "*" else: message = mission - if ui: + if ctx.ui: unlocks = unlock_table[mission] if len(unlocks) > 0: - pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: " - pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks) + pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: " + pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks) pre_message += f"]" message = pre_message + message + "[/ref]" @@ -743,7 +667,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): if ctx.ui: locations = unfinished_locations[mission] - pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|" + pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|" pre_message += "
".join(location for location in locations) pre_message += f"]" formatted_message = pre_message + message + "[/ref]" @@ -751,90 +675,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): return formatted_message -def request_available_missions(locations_done, location_table, ui): - if location_table: +def request_available_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Available Missions: " # Initialize mission unlock table - unlocks = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - missions = calc_available_missions(locations_done, location_table, unlocks) + missions = calc_available_missions(ctx, unlocks) message += \ - ", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]" + ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" + f"[{ctx.mission_req_table[mission].id}]" for mission in missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_available_missions(locations_done, locations, unlocks=None): +def calc_available_missions(ctx: SC2Context, unlocks=None): available_missions = [] missions_complete = 0 # Get number of missions completed - for loc in locations_done: - if loc % 100 == 0: + for loc in ctx.checked_locations: + if loc % victory_modulo == 0: missions_complete += 1 - for name in locations: + for name in ctx.mission_req_table: # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips if unlocks: - for unlock in locations[name].required_world: - unlocks[list(locations)[unlock-1]].append(name) + for unlock in ctx.mission_req_table[name].required_world: + unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name) - if mission_reqs_completed(name, missions_complete, locations_done, locations): + if mission_reqs_completed(ctx, name, missions_complete): available_missions.append(name) return available_missions -def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations): +def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete): """Returns a bool signifying if the mission has all requirements complete and can be done - Keyword arguments: + Arguments: + ctx -- instance of SC2Context locations_to_check -- the mission string name to check missions_complete -- an int of how many missions have been completed - locations_done -- a list of the location ids that have been complete - locations -- a dict of MissionInfo for mission requirements for this world""" - if len(locations[location_to_check].required_world) >= 1: +""" + if len(ctx.mission_req_table[mission_name].required_world) >= 1: # A check for when the requirements are being or'd or_success = False # Loop through required missions - for req_mission in locations[location_to_check].required_world: + for req_mission in ctx.mission_req_table[mission_name].required_world: req_success = True # Check if required mission has been completed - if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done: - if not locations[location_to_check].or_requirements: + if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id * + victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations: + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # Recursively check required mission to see if it's requirements are met, in case !collect has been done - if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done, - locations): - if not locations[location_to_check].or_requirements: + if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # If requirement check succeeded mark or as satisfied - if locations[location_to_check].or_requirements and req_success: + if ctx.mission_req_table[mission_name].or_requirements and req_success: or_success = True - if locations[location_to_check].or_requirements: + if ctx.mission_req_table[mission_name].or_requirements: # Return false if or requirements not met if not or_success: return False # Check number of missions - if missions_complete >= locations[location_to_check].number: + if missions_complete >= ctx.mission_req_table[mission_name].number: return True else: return False @@ -929,7 +854,7 @@ class DllDirectory: self.set(self._old) @staticmethod - def get() -> str: + def get() -> typing.Optional[str]: if sys.platform == "win32": n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) buf = ctypes.create_unicode_buffer(n) diff --git a/Utils.py b/Utils.py index 4b2300a870..c362131d75 100644 --- a/Utils.py +++ b/Utils.py @@ -35,7 +35,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.4" +__version__ = "0.3.5" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index ecd1da4922..4f1b1157ec 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -69,8 +69,8 @@ vanilla_mission_req_table = { "Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True), "Evacuation": MissionInfo(4, 4, [3], "Colonist"), "Outbreak": MissionInfo(5, 3, [4], "Colonist"), - "Safe Haven": MissionInfo(6, 1, [5], "Colonist", number=7), - "Haven's Fall": MissionInfo(7, 1, [5], "Colonist", number=7), + "Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7), + "Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7), "Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True), "The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True), "The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True), diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index cf3175bd6e..4f9b33609f 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -43,6 +43,7 @@ class SC2WoLWorld(World): locked_locations: typing.List[str] location_cache: typing.List[Location] mission_req_table = {} + required_client_version = 0, 3, 5 def __init__(self, world: MultiWorld, player: int): super(SC2WoLWorld, self).__init__(world, player) From 0444fdc379aab5d41e4052686e55b896f96f3de6 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:20:30 -0400 Subject: [PATCH 42/62] SM: wasteland ap (#983) --- .../variaRandomizer/graph/vanilla/graph_access.py | 13 ++++++++++++- .../graph/vanilla/graph_locations.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index eebff84c52..b74b69026e 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -294,7 +294,18 @@ accessPoints = [ sm.canGetBackFromRidleyZone(), sm.canPassWastelandDessgeegas(), sm.canPassRedKiHunters())), - 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])) + 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])), + 'Wasteland': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas())) + }, internal=True), + AccessPoint('Wasteland', 'LowerNorfair', { + # no transition to firefleas to exlude pb of shame location when starting at firefleas top + 'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.traverse('WastelandLeft'), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas(), + sm.canPassNinjaPirates())) }, internal=True), AccessPoint('Three Muskateers Room Left', 'LowerNorfair', { 'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index b8a1d3f44e..671368e831 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -797,10 +797,10 @@ locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = ( lambda sm: SMBool(True) ) locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = { - 'Ridley Zone': lambda sm: sm.canUsePowerBombs() + 'Wasteland': lambda sm: sm.canUsePowerBombs() } locationsDict["Power Bomb (Power Bombs of shame)"].Available = ( - lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']) + lambda sm: SMBool(True) ) locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = { 'Firefleas': lambda sm: SMBool(True) From b115bdafe78e34261b8fd8c83fdde54db469e50d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:30:28 +0200 Subject: [PATCH 43/62] CI/Doc: Use pytest subtests (#986) * CI/Doc: use pytest-subtests * CI: clean up pip installs a bit * make lint and unittests install the same stuff * make sure to install wheel, which is a recommended (not required) dependency for everything pip --- .github/workflows/lint.yml | 4 ++-- .github/workflows/unittests.yml | 4 ++-- docs/running from source.md | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d7cc3c7439..28adb50026 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,8 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 1c8ab10c70..4d0ceaec87 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -32,8 +32,8 @@ jobs: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" - name: Unittests run: | diff --git a/docs/running from source.md b/docs/running from source.md index 4360b28c16..39addd0a28 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in host.yaml at your SNI folder. + + +## Running tests + +Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. From 03f66a922d42f492ff751fede981d94e14dd4ee4 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 1 Sep 2022 21:21:53 +0200 Subject: [PATCH 44/62] sm64ex: Fix a Location (#979) --- worlds/sm64ex/Locations.py | 4 ++-- worlds/sm64ex/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index 1995abf425..9a3db29c26 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -245,7 +245,7 @@ locBitFS_table = { locWMotR_table = { "Wing Mario Over the Rainbow Red Coins": 3626154, - "Wing Mario Over the Rainbow 1Up Block": 3626242 + "Wing Mario Over the Rainbow 1Up Block": 3626243 } locBitS_table = { @@ -268,4 +268,4 @@ location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table, **locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \ **loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \ **locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \ - **locSS_table} \ No newline at end of file + **locSS_table} diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 447a09d431..cf8a8e875d 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -34,7 +34,7 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 7 + data_version = 8 required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] From e413619c26c30322110f963e36fef044b64073b2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 1 Sep 2022 21:25:06 +0200 Subject: [PATCH 45/62] Tests: verify that a world doesn't use the same ID multiple times (#985) --- test/general/TestIDs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/general/TestIDs.py b/test/general/TestIDs.py index f91775c8de..db1c9461b9 100644 --- a/test/general/TestIDs.py +++ b/test/general/TestIDs.py @@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase): else: for location_id in world_type.location_id_to_name: self.assertGreater(location_id, 0) + + def testDuplicateItemIDs(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id)) + + def testDuplicateLocationIDs(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) From 8d2333006a43407e2fa7fb5ad88fc00b08ef124a Mon Sep 17 00:00:00 2001 From: skrawpie <21212370+skrawpie@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:26:04 -0400 Subject: [PATCH 46/62] Minecraft: Added shuffled recipe list to en_Minecraft.md (#980) Co-authored-by: KonoTyran --- worlds/minecraft/docs/en_Minecraft.md | 87 ++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md index b67107a8fc..2d4f063b79 100644 --- a/worlds/minecraft/docs/en_Minecraft.md +++ b/worlds/minecraft/docs/en_Minecraft.md @@ -7,9 +7,9 @@ config file. ## What does randomization do to this game? -Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which +Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item -checks, and occasionally when completing your own achievements. +checks, and occasionally when completing your own achievements. See below for which recipes are shuffled. ## What is considered a location check in minecraft? @@ -25,3 +25,86 @@ inventory directly. Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits sequence either by skipping it or watching hit play out. + +## Which recipes are locked? + +* Archery + * Bow + * Arrow + * Crossbow +* Brewing + * Blaze Powder + * Brewing Stand +* Enchanting + * Enchanting Table + * Bookshelf +* Bucket +* Flint & Steel +* All Beds +* Bottles +* Shield +* Fishing Rod + * Fishing Rod + * Carrot on a Stick + * Warped Fungus on a Stick +* Campfire + * Campfire + * Soul Campfire +* Spyglass +* Lead +* Progressive Weapons + * Tier I + * Stone Sword + * Stone Axe + * Tier II + * Iron Sword + * Iron Axe + * Tier III + * Diamond Sword + * Diamond Axe +* Progessive Tools + * Tier I + * Stone Shovel + * Stone Hoe + * Tier II + * Iron Shovel + * Iron Hoe + * Tier III + * Diamond Shovel + * Diamond Hoe + * Netherite Ingot +* Progressive Armor + * Tier I + * Iron Helmet + * Iron Chestplate + * Iron Leggings + * Iron Boots + * Tier II + * Diamond Helmet + * Diamond Chestplate + * Diamond Leggings + * Diamond Boots +* Progressive Resource Crafting + * Tier I + * Iron Ingot from Nuggets + * Iron Nugget + * Gold Ingot from Nuggets + * Gold Nugget + * Furnace + * Blast Furnace + * Tier II + * Redstone + * Redstone Block + * Glowstone + * Iron Ingot from Iron Block + * Iron Block + * Gold Ingot from Gold Block + * Gold Block + * Diamond + * Diamond Block + * Netherite Block + * Netherite Ingot from Netherite Block + * Anvil + * Emerald + * Emerald Block + * Copper Block From b14d694e1e22cdd901d317837bc2bf533fa2e444 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 1 Sep 2022 13:45:14 -0500 Subject: [PATCH 47/62] templates: fix bug report label --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index dff9a56651..d4c8702da0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,7 +2,7 @@ name: Bug Report description: File a bug report. title: "Bug: " labels: - - bug + - bug / fix body: - type: markdown attributes: @@ -32,4 +32,4 @@ body: - Local generation - While playing validations: - required: true \ No newline at end of file + required: true From f7d107fc0c89a4d34f6ae6a4c4b563a14b50c650 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 2 Sep 2022 03:35:41 +0200 Subject: [PATCH 48/62] Subnautica: add some more missed aggressive creatures --- worlds/subnautica/Creatures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 687c3732a9..cb34f261f5 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -55,7 +55,6 @@ all_creatures: Dict[str, int] = { "Sea Emperor Juvenile": 1700, } -# be nice and make these require Stasis Rifle aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", @@ -75,6 +74,8 @@ aggressive: Set[str] = { "Lava Lizard", "Sea Dragon Leviathan", "River Prowler", + "Ghost Leviathan Juvenile", + "Ghost Leviathan" } containment: Set[str] = { # creatures that have to be raised from eggs From b45d8bf22164d266c1d0cf2079df45af26cff96e Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:37:37 -0400 Subject: [PATCH 49/62] Patch: Save patch file extension in archipelago.json (#968) --- Patch.py | 3 ++- WebHostLib/downloads.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Patch.py b/Patch.py index f90e376656..aaa4fc2404 100644 --- a/Patch.py +++ b/Patch.py @@ -17,7 +17,7 @@ ModuleUpdate.update() import Utils -current_patch_version = 4 +current_patch_version = 5 class AutoPatchRegister(type): @@ -128,6 +128,7 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): manifest = super(APDeltaPatch, self).get_manifest() manifest["base_checksum"] = self.hash manifest["result_file_ending"] = self.result_file_ending + manifest["patch_file_ending"] = self.patch_file_ending return manifest @classmethod diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 528cbe5ec0..c3a373c2e9 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -32,9 +32,12 @@ def download_patch(room_id, patch_id): new_zip.writestr("archipelago.json", json.dumps(manifest)) else: new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9) - + if "patch_file_ending" in manifest: + patch_file_ending = manifest["patch_file_ending"] + else: + patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \ - f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}" + f"{patch_file_ending}" new_file.seek(0) return send_file(new_file, as_attachment=True, download_name=fname) else: From 4b6d46fd743d9246f400d743786505a90cd3d7af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 2 Sep 2022 11:04:23 +0200 Subject: [PATCH 50/62] Core: update modules --- WebHostLib/requirements.txt | 6 +++--- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 52d0316b2a..a4dd710e83 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.1.3 +flask>=2.2.2 pony>=0.7.16 -waitress>=2.1.1 +waitress>=2.1.2 Flask-Caching>=2.0.1 Flask-Compress>=1.12 -Flask-Limiter>=2.5.0 +Flask-Limiter>=2.6.2 bokeh>=2.4.3 diff --git a/requirements.txt b/requirements.txt index 661209e072..6c9e3b9d2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ websockets>=10.3 PyYAML>=6.0 jellyfish>=0.9.0 jinja2>=3.1.2 -schema>=0.7.4 +schema>=0.7.5 kivy>=2.1.0 bsdiff4>=1.2.2 \ No newline at end of file From b7cfcc9272efdc39f6cab4e8c4e5ad9466463c2b Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Sat, 3 Sep 2022 12:25:04 -0700 Subject: [PATCH 51/62] New features and fixes for Raft (#984) * Add DeathLink, small logic changes * Fix generation, rules, use bool for slotData * Add more island options * Update Shovel-related logic * Update docs --- worlds/raft/Options.py | 40 ++++++++++++----------- worlds/raft/Rules.py | 61 ++++++++++++++++++++-------------- worlds/raft/__init__.py | 55 ++++++++++++++++++++++++------- worlds/raft/docs/en_Raft.md | 2 +- worlds/raft/docs/setup_en.md | 63 ++++++++++++++++++------------------ worlds/raft/regions.json | 16 ++++----- 6 files changed, 143 insertions(+), 94 deletions(-) diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index 482f1d343a..5f09db07c0 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,8 +1,4 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice - -class UseResourcePacks(DefaultOnToggle): - """Uses Resource Packs to fill out the item pool from Raft. Resource Packs have basic earlygame items such as planks, plastic, or food.""" - display_name = "Use resource packs" +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -19,23 +15,30 @@ class MaximumResourcePackAmount(Range): default = 5 class DuplicateItems(Choice): - """Adds duplicates of items to the item pool. These will be selected alongside - Resource Packs (if configured). Note that there are not many progression items, - and selecting Progression may produce many of the same duplicate item.""" + """Adds duplicates of items to the item pool (if configured in Filler items). These will be selected alongside Resource Packs (if configured). Note that there are not many progression items, and selecting Progression may produce many of the same duplicate item.""" display_name = "Duplicate items" - option_disabled = 0 - option_progression = 1 - option_non_progression = 2 - option_any = 3 + option_progression = 0 + option_non_progression = 1 + option_any = 2 + default = 2 + +class FillerItemTypes(Choice): + """Determines whether to use Resource Packs, Duplicate Items (as configured), or both.""" + display_name = "Filler items" + option_resource_packs = 0 + option_duplicates = 1 + option_both = 2 class IslandFrequencyLocations(Choice): """Sets where frequencies for story islands are located.""" display_name = "Frequency locations" option_vanilla = 0 option_random_on_island = 1 - option_progressive = 2 - option_anywhere = 3 - default = 1 + option_random_island_order = 2 + option_random_on_island_random_order = 3 + option_progressive = 4 + option_anywhere = 5 + default = 2 class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -56,7 +59,7 @@ class ProgressiveItems(DefaultOnToggle): display_name = "Progressive items" class BigIslandEarlyCrafting(Toggle): - """Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter.""" + """Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter. Big islands are available from the start of the game, however it can take a long time to find them.""" display_name = "Early recipes behind big islands" class PaddleboardMode(Toggle): @@ -64,14 +67,15 @@ class PaddleboardMode(Toggle): display_name = "Paddleboard Mode" raft_options = { - "use_resource_packs": UseResourcePacks, "minimum_resource_pack_amount": MinimumResourcePackAmount, "maximum_resource_pack_amount": MaximumResourcePackAmount, "duplicate_items": DuplicateItems, + "filler_item_types": FillerItemTypes, "island_frequency_locations": IslandFrequencyLocations, "island_generation_distance": IslandGenerationDistance, "expensive_research": ExpensiveResearch, "progressive_items": ProgressiveItems, "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode + "paddleboard_mode": PaddleboardMode, + "death_link": DeathLink } diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index 3ca565d9d6..0432c2806f 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -12,9 +12,6 @@ class RaftLogic(LogicMixin): def raft_can_smelt_items(self, player): return self.has("Smelter", player) - - def raft_can_find_titanium(self, player): - return self.has("Metal detector", player) def raft_can_craft_bolt(self, player): return self.raft_can_smelt_items(player) and self.has("Bolt", player) @@ -27,12 +24,19 @@ class RaftLogic(LogicMixin): def raft_can_craft_circuitBoard(self, player): return self.raft_can_smelt_items(player) and self.has("Circuit board", player) + + def raft_can_craft_shovel(self, player): + return self.raft_can_smelt_items(player) and self.has("Shovel", player) and self.raft_can_craft_bolt(player) def raft_can_craft_reciever(self, player): return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_hinge(player) and self.has("Receiver", player) def raft_can_craft_antenna(self, player): return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_bolt(player) and self.has("Antenna", player) + + def raft_can_find_titanium(self, player): + return (self.has("Metal detector", player) and self.raft_can_craft_battery(player) + and self.raft_can_craft_shovel(player)) def raft_can_craft_plasticBottle(self, player): return self.raft_can_smelt_items(player) and self.has("Empty bottle", player) @@ -60,7 +64,7 @@ class RaftLogic(LogicMixin): return self.raft_can_craft_hinge(player) and self.raft_can_craft_bolt(player) and self.has("Zipline tool", player) def raft_can_get_dirt(self, player): - return self.raft_can_smelt_items(player) and self.raft_can_craft_bolt(player) and self.has("Shovel", player) + return self.raft_can_craft_shovel(player) and self.raft_big_islands_available(player) def raft_can_craft_grassPlot(self, player): return self.raft_can_get_dirt(player) and self.has("Grass plot", player) @@ -88,60 +92,69 @@ class RaftLogic(LogicMixin): return self.raft_can_access_radio_tower(player) def raft_can_access_vasagatan(self, player): - return self.raft_can_complete_radio_tower(player) and self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player) + return self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player) def raft_can_complete_vasagatan(self, player): return self.raft_can_access_vasagatan(player) def raft_can_access_balboa_island(self, player): - return (self.raft_can_complete_vasagatan(player) - and self.raft_can_drive(player) - and self.has("Balboa Island Frequency", player)) + return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player) def raft_can_complete_balboa_island(self, player): return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) def raft_can_access_caravan_island(self, player): - return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) + return self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) def raft_can_complete_caravan_island(self, player): return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_tangaroa(self, player): - return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) + return self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) def raft_can_complete_tangaroa(self, player): - return self.raft_can_access_tangaroa(player) + return self.raft_can_access_tangaroa(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_varuna_point(self, player): - return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) + return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) def raft_can_complete_varuna_point(self, player): - return self.raft_can_access_varuna_point(player) + return self.raft_can_access_varuna_point(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_temperance(self, player): - return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player) + return self.raft_can_drive(player) and self.has("Temperance Frequency", player) def raft_can_complete_temperance(self, player): - return self.raft_can_access_temperance(player) + return self.raft_can_access_temperance(player) # No zipline required on Temperance def raft_can_access_utopia(self, player): - return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player) + return (self.raft_can_drive(player) + # Access checks are to prevent frequencies for other + # islands from appearing in Utopia + and self.raft_can_access_radio_tower(player) + and self.raft_can_access_vasagatan(player) + and self.raft_can_access_balboa_island(player) + and self.raft_can_access_caravan_island(player) + and self.raft_can_access_tangaroa(player) + and self.raft_can_access_varuna_point(player) + and self.raft_can_access_temperance(player) + and self.has("Utopia Frequency", player) + and self.raft_can_craft_shovel(player)) # Shovels are available but we don't want to softlock players def raft_can_complete_utopia(self, player): - return self.raft_can_access_utopia(player) + return self.raft_can_access_utopia(player) and self.raft_can_craft_ziplineTool(player) def set_rules(world, player): regionChecks = { "Raft": lambda state: True, "ResearchTable": lambda state: True, "RadioTower": lambda state: state.raft_can_access_radio_tower(player), # All can_access functions have state as implicit parameter for function - "Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player), - "BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player), - "CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player), - "Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player), - "Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player), - "Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player), + "Vasagatan": lambda state: state.raft_can_access_vasagatan(player), + "BalboaIsland": lambda state: state.raft_can_access_balboa_island(player), + "CaravanIsland": lambda state: state.raft_can_access_caravan_island(player), + "Tangaroa": lambda state: state.raft_can_access_tangaroa(player), + "Varuna Point": lambda state: state.raft_can_access_varuna_point(player), + "Temperance": lambda state: state.raft_can_access_temperance(player), "Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player) } itemChecks = { @@ -183,7 +196,7 @@ def set_rules(world, player): if region != "Menu": for exitRegion in world.get_region(region, player).exits: set_rule(world.get_entrance(exitRegion.name, player), regionChecks[region]) - + # Location access rules for location in location_table: locFromWorld = world.get_location(location["name"], player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index da4b58f24f..860ba9aab5 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -56,21 +56,21 @@ class RaftWorld(World): extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.world.use_resource_packs[self.player].value): + if (self.world.filler_item_types[self.player].value != 1): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.world.duplicate_items[self.player].value != 0: + if self.world.filler_item_types[self.player].value != 0: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.world.island_frequency_locations[self.player].value != 3: # Not completely random locations + if self.world.island_frequency_locations[self.player].value != 5: # Not completely random locations dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.world.duplicate_items[self.player].value == 1): # Progression only + if (self.world.duplicate_items[self.player].value == 0): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.world.duplicate_items[self.player].value == 2): # Non-progression only + elif (self.world.duplicate_items[self.player].value == 1): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -91,19 +91,15 @@ class RaftWorld(World): def create_regions(self): create_regions(self.world, self.player) - - def fill_slot_data(self): - slot_data = {} - return slot_data def get_pre_fill_items(self): - if self.world.island_frequency_locations[self.player] in [0, 1]: + if self.world.island_frequency_locations[self.player] in [0, 1, 2, 3]: return [loc.item for loc in self.world.get_filled_locations()] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2) + shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 4) or (not isFrequency and self.world.progressive_items[self.player].value)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] @@ -148,6 +144,40 @@ class RaftWorld(World): self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") self.setLocationItemFromRegion("Temperance", "Utopia Frequency") + elif self.world.island_frequency_locations[self.player] in [2, 3]: + locationToFrequencyItemMap = { + "Vasagatan": "Vasagatan Frequency", + "BalboaIsland": "Balboa Island Frequency", + "CaravanIsland": "Caravan Island Frequency", + "Tangaroa": "Tangaroa Frequency", + "Varuna Point": "Varuna Point Frequency", + "Temperance": "Temperance Frequency", + "Utopia": "Utopia Frequency" + } + locationToVanillaFrequencyLocationMap = { + "RadioTower": "Radio Tower Frequency to Vasagatan", + "Vasagatan": "Vasagatan Frequency to Balboa", + "BalboaIsland": "Relay Station quest", + "CaravanIsland": "Caravan Island Frequency to Tangaroa", + "Tangaroa": "Tangaroa Frequency to Varuna Point", + "Varuna Point": "Varuna Point Frequency to Temperance", + "Temperance": "Temperance Frequency to Utopia" + } + # Utopia is never chosen until the end, otherwise these are chosen randomly + availableLocationList = ["Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"] + previousLocation = "RadioTower" + while (len(availableLocationList) > 0): + if (len(availableLocationList) > 1): + currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)] + else: + currentLocation = availableLocationList[0] # Utopia (only one left in list) + availableLocationList.remove(currentLocation) + if self.world.island_frequency_locations[self.player] == 2: + self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) + elif self.world.island_frequency_locations[self.player] == 3: + self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) + previousLocation = currentLocation + # Victory item self.world.get_location("Utopia Complete", self.player).place_locked_item( RaftItem("Victory", ItemClassification.progression, None, player=self.player)) @@ -166,7 +196,8 @@ class RaftWorld(World): def fill_slot_data(self): return { "IslandGenerationDistance": self.world.island_generation_distance[self.player].value, - "ExpensiveResearch": self.world.expensive_research[self.player].value + "ExpensiveResearch": bool(self.world.expensive_research[self.player].value), + "DeathLink": bool(self.world.death_link[self.player].value) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): diff --git a/worlds/raft/docs/en_Raft.md b/worlds/raft/docs/en_Raft.md index adcf5ea6cb..385377d456 100644 --- a/worlds/raft/docs/en_Raft.md +++ b/worlds/raft/docs/en_Raft.md @@ -22,7 +22,7 @@ Decoration Packages are unchanged. Researches and pickups remain visually unchanged, regardless of what the unlock is. ## When the player receives an item, what happens? -A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Machete) will NOT give it to you, but must instead be crafted. +A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Zipline) will NOT give it to you, but must instead be crafted. ## Are there any limitations compared to vanilla Raft? - Mods that add new researchable technologies, modify story islands, or give items like blueprints are likely incompatible with Raftipelago. diff --git a/worlds/raft/docs/setup_en.md b/worlds/raft/docs/setup_en.md index ad69eacdf8..4d84ef0f65 100644 --- a/worlds/raft/docs/setup_en.md +++ b/worlds/raft/docs/setup_en.md @@ -4,23 +4,50 @@ - [Raft](https://store.steampowered.com/app/648800/Raft/) - [Raft Mod Loader](https://www.raftmodding.com/loader) ("*RML*") +- [ModUtils mod](https://www.raftmodding.com/mods/modutils) - [Raftipelago mod](https://www.raftmodding.com/mods/raftipelago) ## Installation Procedures -1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds. +1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. Any minor version (such as 1.08) should be compatible. 2. Install RML. -3. Install the Raftipelago mod from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually. +3. Install the Raftipelago and ModUtils mods from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually. -4. Open RML and click Play. If you've already installed it, the shortcut in the Start Menu is called "RMLLauncher.exe". Raft should start. +4. Open RML and click Play. If you've already installed it, the executable that was used to install RML ("RMLLauncher.exe" unless renamed) should be used to run RML. Raft should start after clicking Play. 5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it. 6. Navigate to the "Mod manager" tab in the left-hand menu. -7. Click on the plug icon for Raftipelago to load the mod. +7. Click on the plug icon for ModUtils to load the mod. You can also click on the (i) next to the plug icon, then check the "Load this mod at startup" button. This will make the mod always load at startup. + +8. Click on the plug icon for Raftipelago to load the mod. While it's possible to also make this mod load at startup, it's recommended *not* to do so; if this mod loads before ModUtils, the mod will fail to load properly. + +## Joining a MultiWorld Game + +1. Ensure you're on the Main Menu with Raftipelago loaded. + +2. Open the Debug Console by pressing F10. + +3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter. + - Example: */connect archipelago.gg:12345 SunnyBat* + - If there is no password, the password argument may be omitted (as is the case in the above example). + - serverAddress must not contain spaces. + - If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine. + - If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*). + +4. Start a new game or load an existing one. It's recommended to avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible. + +5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter. + +## Multiplayer Raft + +You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes: +- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host. +- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat. +- Ingame chat will only work when the host is connected to the Archipelago server. ## Installation Troubleshooting @@ -45,38 +72,12 @@ If this happens, then RML is configured to only start a new instance of Raft, th You can either: - Close the existing instance of Raft then click Play - Check the box next to the "Disable Automatic Game Start" setting in the Settings menu then click Play. - -## Joining a MultiWorld Game - -1. Ensure you're on the Main Menu with Raftipelago loaded. - -2. Open the Debug Console by pressing F10. - -3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter. - - Example: */connect archipelago.gg:12345 SunnyBat* - - serverAddress must not contain spaces. - - If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine. - - If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*). - -4. Start a new game or load an existing one. - - Raftipelago save games are marked as *incompatible* with vanilla Raft. This means when Raftipelago is not loaded, saves made with Raftipelago will show as corrupt/unselectable. - - Avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible. - -5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter. - -## Multiplayer Raft - -You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes: -- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host. -- Resource Packs are only received by the host and any other players connected to the Raft world when the resource pack is received. -- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat. -- Ingame chat will only work when the host is connected to the Archipelago server. ## Game Troubleshooting ### The "Load game" button is disabled for my world / my world is corrupt -Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to reload all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully). +Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to refresh all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully). ### I'm certain I'm doing things correctly, but the world is still not loadable diff --git a/worlds/raft/regions.json b/worlds/raft/regions.json index b2737a1298..e593a0ab61 100644 --- a/worlds/raft/regions.json +++ b/worlds/raft/regions.json @@ -1,12 +1,12 @@ { - "Raft": ["RadioTower", "ResearchTable"], + "Raft": ["ResearchTable", "RadioTower", "Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"], "ResearchTable": [], - "RadioTower": ["Vasagatan"], - "Vasagatan": ["BalboaIsland"], - "BalboaIsland": ["CaravanIsland"], - "CaravanIsland": ["Tangaroa"], - "Tangaroa": ["Varuna Point"], - "Varuna Point": ["Temperance"], - "Temperance": ["Utopia"], + "RadioTower": [], + "Vasagatan": [], + "BalboaIsland": [], + "CaravanIsland": [], + "Tangaroa": [], + "Varuna Point": [], + "Temperance": [], "Utopia": [] } \ No newline at end of file From f9e28004a078f4925f8ba8694d7785043162a4bb Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 3 Sep 2022 15:25:55 -0400 Subject: [PATCH 52/62] SMZ3: item link gt fill fix (#995) --- worlds/smz3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 15ac85c7c3..b9aab50e22 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -526,9 +526,11 @@ class SMZ3World(World): def JunkFillGT(self, factor): poolLength = len(self.world.itempool) + playerGroups = self.world.get_player_groups(self.player) + playerGroups.add(self.player) junkPoolIdx = [i for i in range(0, poolLength) if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and - self.world.itempool[i].player == self.player] + self.world.itempool[i].player in playerGroups] toRemove = [] for loc in self.locations.values(): # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT From 539d2e80f1eff44cdbfb8263d179d316141b05c4 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 3 Sep 2022 10:02:20 -0500 Subject: [PATCH 53/62] OoT: prevent glitched + mq dungeons this combo is not allowed on main ootr, so we won't have it here either --- worlds/oot/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index a9b7d5a1b6..b65882c874 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -190,7 +190,11 @@ class OOTWorld(World): # Determine which dungeons are MQ # Possible future plan: allow user to pick which dungeons are MQ - mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons) + if self.logic_rules == 'glitchless': + mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons) + else: + self.mq_dungeons = 0 + mq_dungeons = [] self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table} # Determine tricks in logic From 0cbb3c283917094ec5dad42628fe7d93a75025c0 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 3 Sep 2022 17:52:09 -0400 Subject: [PATCH 54/62] SMZ3: data package fix (#996) --- SNIClient.py | 3 ++- worlds/smz3/__init__.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 3d90fafc17..ccac3998a1 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1271,7 +1271,8 @@ async def game_watcher(ctx: Context): snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from worlds.smz3.TotalSMZ3.Location import locations_start_id - location_id = locations_start_id + itemIndex + from worlds.smz3 import convertLocSMZ3IDToAPID + location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex) ctx.locations_checked.add(location_id) location = ctx.location_names[location_id] diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b9aab50e22..b796c2a43c 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -25,6 +25,10 @@ from Options import Accessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") +# Location IDs in the range 256+196 to 256+202 shifted +34 between 11.2 and 11.3 +# this is required to keep backward compatibility +def convertLocSMZ3IDToAPID(value): + return (value - 34) if value >= 256+230 and value <= 256+236 else value class SMZ3CollectionState(metaclass=AutoLogicRegister): def init_mixin(self, parent: MultiWorld): @@ -61,12 +65,13 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 2 + data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id - location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} + location_name_to_id: Dict[str, int] = {key : locations_start_id + convertLocSMZ3IDToAPID(value.Id) + for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} web = SMZ3Web() remote_items: bool = False From 2acc129381135dedea93d58cf1411124e78cca5b Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sun, 4 Sep 2022 08:45:45 -0400 Subject: [PATCH 55/62] SA2B: Fix typo in doc string (#997) --- worlds/sa2b/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 84a38f221c..e9b75bbeb8 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -46,7 +46,7 @@ def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range class SA2BWorld(World): """ - Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth. + Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth. """ game: str = "Sonic Adventure 2 Battle" option_definitions = sa2b_options From 5e8ac74b2a168568a4d26c7cddef0878372af5fc Mon Sep 17 00:00:00 2001 From: wildham0 <64616385+wildham0@users.noreply.github.com> Date: Mon, 5 Sep 2022 03:21:00 -0400 Subject: [PATCH 56/62] FFR: fix NoOverworld mode (#999) * Add Sigil/Mark to item list --- data/lua/FF1/ff1_connector.lua | 12 ++++++++++-- worlds/ff1/Items.py | 2 +- worlds/ff1/__init__.py | 2 +- worlds/ff1/data/items.json | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/data/lua/FF1/ff1_connector.lua b/data/lua/FF1/ff1_connector.lua index ed3ee1ca60..6b2eec269a 100644 --- a/data/lua/FF1/ff1_connector.lua +++ b/data/lua/FF1/ff1_connector.lua @@ -97,6 +97,11 @@ local extensionConsumableLookup = { [443] = 0x3F } +local noOverworldItemsLookup = { + [499] = 0x2B, + [500] = 0x12, +} + local itemMessages = {} local consumableStacks = nil local prevstate = "" @@ -341,7 +346,7 @@ function processBlock(block) -- This is a key item memoryLocation = memoryLocation - 0x0E0 wU8(memoryLocation, 0x01) - elseif v >= 0x1E0 then + elseif v >= 0x1E0 and v <= 0x1F2 then -- This is a movement item -- Minus Offset (0x100) - movement offset (0xE0) memoryLocation = memoryLocation - 0x1E0 @@ -351,7 +356,10 @@ function processBlock(block) else wU8(memoryLocation, 0x01) end - + elseif v >= 0x1F3 and v <= 0x1F4 then + -- NoOverworld special items + memoryLocation = noOverworldItemsLookup[v] + wU8(memoryLocation, 0x01) elseif v >= 0x16C and v <= 0x1AF then -- This is a gold item amountToAdd = goldLookup[v] diff --git a/worlds/ff1/Items.py b/worlds/ff1/Items.py index e045fbaf18..469cf6f051 100644 --- a/worlds/ff1/Items.py +++ b/worlds/ff1/Items.py @@ -20,7 +20,7 @@ FF1_STARTER_ITEMS = [ FF1_PROGRESSION_LIST = [ "Rod", "Cube", "Lute", "Key", "Chime", "Oxyale", - "Ship", "Canoe", "Floater", "Canal", + "Ship", "Canoe", "Floater", "Mark", "Sigil", "Canal", "Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle", "Shard", "EarthOrb", "FireOrb", "WaterOrb", "AirOrb" diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 0d731ace4b..b9f90a2ea1 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -31,7 +31,7 @@ class FF1World(World): game = "Final Fantasy" topology_present = False remote_items = True - data_version = 1 + data_version = 2 remote_start_inventory = True ff1_items = FF1Items() diff --git a/worlds/ff1/data/items.json b/worlds/ff1/data/items.json index 99611837fb..333080973d 100644 --- a/worlds/ff1/data/items.json +++ b/worlds/ff1/data/items.json @@ -190,5 +190,7 @@ "Ship": 480, "Bridge": 488, "Canal": 492, - "Canoe": 498 + "Canoe": 498, + "Sigil": 499, + "Mark": 500 } From 1792b66b3a8355e98304a690bced48363cff49d0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 4 Sep 2022 23:43:03 +0200 Subject: [PATCH 57/62] CI: fix automated builds, update SNI and Enemizer * Launcher.py always running ModuleUpdate breaks setup.py build --yes * Use env variables in github workflows * Update SNI and Enemizer versions in github workflows * Minor cleanup in workflows * Silence pycharm warning in Launcher.py --- .github/workflows/build.yml | 17 ++++++++++++----- .github/workflows/release.yml | 14 ++++++++++---- Launcher.py | 6 ++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4138f93f04..be053bdc2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,11 @@ name: Build on: workflow_dispatch +env: + SNI_VERSION: v0.0.84 + ENEMIZER_VERSION: 7.1 + APPIMAGETOOL_VERSION: 13 + jobs: # build-release-macos: # LF volunteer @@ -17,9 +22,9 @@ jobs: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip + Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip Expand-Archive -Path sni.zip -DestinationPath SNI -Force - Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip + Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - name: Build run: | @@ -43,6 +48,7 @@ jobs: build-ubuntu1804: runs-on: ubuntu-18.04 steps: + # - copy code below to release.yml - - uses: actions/checkout@v2 - name: Install base dependencies run: | @@ -56,18 +62,18 @@ jobs: - name: Install build-time dependencies run: | echo "PYTHON=python3.9" >> $GITHUB_ENV - wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage + wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI - wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z + wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build run: | @@ -84,6 +90,7 @@ jobs: (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV + # - copy code above to release.yml - - name: Store AppImage uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa82883ff1..23f018caf2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,11 @@ on: tags: - '*.*.*' +env: + SNI_VERSION: v0.0.84 + ENEMIZER_VERSION: 7.1 + APPIMAGETOOL_VERSION: 13 + jobs: create-release: runs-on: ubuntu-latest @@ -44,22 +49,23 @@ jobs: - name: Install build-time dependencies run: | echo "PYTHON=python3.9" >> $GITHUB_ENV - wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage + wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI - wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z + wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build run: | - "${{ env.PYTHON }}" -m pip install --upgrade pip setuptools virtualenv PyGObject # pygobject should probably move to requirements + # pygobject is an optional dependency for kivy that's not in requirements + "${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools "${{ env.PYTHON }}" -m venv venv source venv/bin/activate pip install -r requirements.txt diff --git a/Launcher.py b/Launcher.py index 92f43cd26c..8a3d53f866 100644 --- a/Launcher.py +++ b/Launcher.py @@ -19,8 +19,9 @@ from os.path import isfile from shutil import which from typing import Iterable, Sequence, Callable, Union, Optional -import ModuleUpdate -ModuleUpdate.update() +if __name__ == "__main__": + import ModuleUpdate + ModuleUpdate.update() from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ is_windows, is_macos, is_linux @@ -69,6 +70,7 @@ def browse_files(): webbrowser.open(file) +# noinspection PyArgumentList class Type(Enum): TOOL = auto() FUNC = auto() # not a real component From 8aad75ed239d851c8b3dce5c3a868102eeb597f2 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 5 Sep 2022 01:02:40 -0700 Subject: [PATCH 58/62] Tests: Check for Holes in the Item Pool (#992) * test for holes in the item pool * Update test/general/TestItems.py Co-authored-by: alwaysintreble * Update test/general/TestItems.py Co-authored-by: alwaysintreble * Update test/general/TestItems.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update test/general/TestItems.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: alwaysintreble Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/general/TestItems.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/general/TestItems.py b/test/general/TestItems.py index bd1bd50665..3dd589995e 100644 --- a/test/general/TestItems.py +++ b/test/general/TestItems.py @@ -1,5 +1,6 @@ import unittest from worlds.AutoWorld import AutoWorldRegister +from . import setup_default_world class TestBase(unittest.TestCase): @@ -29,3 +30,17 @@ class TestBase(unittest.TestCase): with self.subTest(group_name, group_name=group_name): for item in items: self.assertIn(item, world_type.item_name_to_id) + + def testItemCountGreaterEqualLocations(self): + for game_name, world_type in AutoWorldRegister.world_types.items(): + + if game_name in {"Final Fantasy"}: + continue + with self.subTest("Game", game=game_name): + world = setup_default_world(world_type) + location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations()) + self.assertGreaterEqual( + len(world.itempool), + location_count, + f"{game_name} Item count MUST meet or exceede the number of locations", + ) From baf51e59591c7c4f051345c8f5cafc22e27c4442 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 5 Sep 2022 21:09:03 +0200 Subject: [PATCH 59/62] SC2: fix Launching Mission: text pulling the unshuffled ID. (#1001) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Starcraft2Client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index b8f6086914..ce4d9b046c 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -340,9 +340,8 @@ class SC2Context(CommonContext): def mission_callback(self, button): if not self.launching: - mission_id: int = list(self.mission_id_to_button.values()).index(button) - self.ctx.play_mission(list(self.mission_id_to_button) - [mission_id]) + mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button) + self.ctx.play_mission(mission_id) self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) @@ -359,7 +358,7 @@ class SC2Context(CommonContext): if self.sc2_run_task: self.sc2_run_task.cancel() - def play_mission(self, mission_id): + def play_mission(self, mission_id: int): if self.missions_unlocked or \ is_mission_available(self, mission_id): if self.sc2_run_task: From 7c04e7e06fc724639ef43a317d1c1de96cd047a7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 5 Sep 2022 10:07:10 +0200 Subject: [PATCH 60/62] MultiServer: save goal completion flag --- MultiServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MultiServer.py b/MultiServer.py index 6354f8e7a9..fc6e17dd20 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -593,6 +593,7 @@ class Context: forfeit_player(self, client.team, client.slot) elif self.forced_auto_forfeits[self.games[client.slot]]: forfeit_player(self, client.team, client.slot) + self.save() # save goal completion flag def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): From ade82e3d60cbb5786d9e62efb19f6605b12441ea Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:56:23 -0400 Subject: [PATCH 61/62] SM: varia tracker fix (#1006) --- .../multiworld-basepatch.ips | Bin 18193 -> 18193 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 704 +++++++++--------- .../sm-basepatch-symbols.json | 70 +- 3 files changed, 387 insertions(+), 387 deletions(-) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index 7ac3ea018475a0db495d6d540c9dda5f66f1391f..f8fba9b0cfba91ed4adfc2c8d8032796683271d7 100644 GIT binary patch delta 200 zcmbQ($2hT%ae}d2%MONy?+h$4H(GZvY-VO)Q<6Ec6cnspt@OTm zGQX<2c5^Gk0#SyQ3P!|W18tMiDEj#uzJlOGpdq2~I9jlbOTPA0y8?d%yE0wfto~vFW4*=dmO!oi) delta 200 zcmbQ($2hT%ae}d2;|_*~?+h$4H=1@ZY-VO)Q<6Ec6cnspt@OTO zGQX<2c0((}0#SyQ3YP!|W18tMiDjXU-;JlOGpdq2~I9jlbO8z*O|8?ZKJE0r{Eo~vFW4* Date: Wed, 7 Sep 2022 11:16:32 -0700 Subject: [PATCH 62/62] ALttP: remove link_palettes option (#1004) * ALttP: remove link_palettes option It doesn't work anyway so better to have it not visible. --- LttPAdjuster.py | 6 +++--- worlds/alttp/Options.py | 6 +++--- worlds/alttp/__init__.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index f516a20ec0..469e8920b3 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -83,9 +83,9 @@ def main(): parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', 'sick']) - parser.add_argument('--link_palettes', default='default', - choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', - 'sick']) + # parser.add_argument('--link_palettes', default='default', + # choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', + # 'sick']) parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', 'sick']) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 183f3eda91..b40becbdbb 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -282,8 +282,8 @@ class ShieldPalette(Palette): display_name = "Shield Palette" -class LinkPalette(Palette): - display_name = "Link Palette" +# class LinkPalette(Palette): +# display_name = "Link Palette" class HeartBeep(Choice): @@ -387,7 +387,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "hud_palettes": HUDPalette, "sword_palettes": SwordPalette, "shield_palettes": ShieldPalette, - "link_palettes": LinkPalette, + # "link_palettes": LinkPalette, "heartbeep": HeartBeep, "heartcolor": HeartColor, "quickswap": QuickSwap, diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3e32584352..abb1f0a9e6 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -378,7 +378,7 @@ class ALTTPWorld(World): 'hud': world.hud_palettes[player], 'sword': world.sword_palettes[player], 'shield': world.shield_palettes[player], - 'link': world.link_palettes[player] + # 'link': world.link_palettes[player] } palettes_options = {key: option.current_key for key, option in palettes_options.items()}