From 0c54c4702362cdacfbdf627aea60b41a2d552f73 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 28 Oct 2022 13:24:08 -0400 Subject: [PATCH 1/6] Docs: Add ArchipelagoRS to the Network Protocol docs (#1153) --- docs/network protocol.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index 84587ab237..c0a0764881 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -25,6 +25,7 @@ There are also a number of community-supported libraries available that implemen | | [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) | | +| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | | ## 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 ec0389eefb490004b0dbc2cec587a8eb3b5d631c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:13:09 +0200 Subject: [PATCH 2/6] CI: use build_exe in automated build --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be053bdc2d..60dbd15b9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools pip install -r requirements.txt - python setup.py build --yes + python setup.py build_exe --yes $NAME="$(ls build)".Split('.',2)[1] $ZIP_NAME="Archipelago_$NAME.7z" echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV @@ -82,7 +82,7 @@ jobs: "${{ env.PYTHON }}" -m venv venv source venv/bin/activate pip install -r requirements.txt - python setup.py build --yes bdist_appimage --yes + python setup.py build_exe --yes bdist_appimage --yes echo -e "setup.py build output:\n `ls build`" echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. From 53974d568b0d2723e7f1bce9c17f2d2fc5f89f06 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 28 Oct 2022 21:00:06 +0200 Subject: [PATCH 3/6] Factorio: revamped location system (#1147) --- Utils.py | 2 +- worlds/factorio/Locations.py | 32 +++ worlds/factorio/Mod.py | 43 ++-- worlds/factorio/Options.py | 41 ++-- worlds/factorio/Shapes.py | 101 ++++----- worlds/factorio/Technologies.py | 4 +- worlds/factorio/__init__.py | 201 +++++++++++------- .../data/mod_template/data-final-fixes.lua | 81 ++----- .../data/mod_template/locale/en/locale.cfg | 20 +- worlds/factorio/data/mod_template/macros.lua | 2 +- 10 files changed, 294 insertions(+), 233 deletions(-) create mode 100644 worlds/factorio/Locations.py diff --git a/Utils.py b/Utils.py index e0c86ddb39..447d39d26c 100644 --- a/Utils.py +++ b/Utils.py @@ -37,7 +37,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.5" +__version__ = "0.3.6" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py new file mode 100644 index 0000000000..1903e589be --- /dev/null +++ b/worlds/factorio/Locations.py @@ -0,0 +1,32 @@ +from typing import Dict, List + +from .Technologies import factorio_base_id, factorio_id +from .Options import MaxSciencePack + +boundary: int = 0xff +total_locations: int = 0xff + +assert total_locations <= boundary +assert factorio_base_id != factorio_id + + +def make_pools() -> Dict[str, List[str]]: + pools: Dict[str, List[str]] = {} + for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): + max_needed: int = sum(divmod(boundary, i)) + scale: float = boundary / max_needed + prefix: str = f"AP-{i}-" + pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + return pools + + +location_pools: Dict[str, List[str]] = make_pools() + +location_table: Dict[str, int] = {} +end_id: int = factorio_id +for pool in location_pools.values(): + location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) + end_id += len(pool) + +assert end_id - len(location_table) == factorio_id +del pool diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 018816d90a..33e3b75a7b 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -1,23 +1,23 @@ """Outputs a Factorio Mod to facilitate integration with Archipelago""" -import os -import zipfile -from typing import Optional -import threading import json +import os +import shutil +import threading +import zipfile +from typing import Optional, TYPE_CHECKING import jinja2 -import shutil import Utils -import Patch -import worlds.AutoWorld import worlds.Files from . import Options - from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \ base_tech_table, tech_to_progressive_lookup, fluids +if TYPE_CHECKING: + from . import Factorio + template_env: Optional[jinja2.Environment] = None data_template: Optional[jinja2.Template] = None @@ -75,7 +75,7 @@ class FactorioModFile(worlds.Files.APContainer): super(FactorioModFile, self).write_contents(opened_zipfile) -def generate_mod(world, output_directory: str): +def generate_mod(world: "Factorio", output_directory: str): player = world.player multiworld = world.world global data_final_template, locale_template, control_template, data_template, settings_template @@ -95,18 +95,10 @@ def generate_mod(world, output_directory: str): control_template = template_env.get_template("control.lua") settings_template = template_env.get_template("settings.lua") # get data for templates - locations = [] - for location in multiworld.get_filled_locations(player): - if location.address: - locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) + locations = [(location, location.item) + for location in world.locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" - tech_cost_scale = {0: 0.1, - 1: 0.25, - 2: 0.5, - 3: 1, - 4: 2, - 5: 5, - 6: 10}[multiworld.tech_cost[player].value] + random = multiworld.slot_seeds[player] def flop_random(low, high, base=None): @@ -120,18 +112,19 @@ def generate_mod(world, output_directory: str): return random.uniform(low, high) template_data = { - "locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table, - "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, + "locations": locations, + "player_names": multiworld.player_name, + "tech_table": tech_table, + "base_tech_table": base_tech_table, + "tech_to_progressive_lookup": tech_to_progressive_lookup, "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), - "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player, "starting_items": multiworld.starting_items[player], "recipes": recipes, "random": random, "flop_random": flop_random, - "static_nodes": multiworld.worlds[player].static_nodes, "recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None), "recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None), "free_sample_blacklist": {item: 1 for item in free_sample_exclusions}, @@ -141,7 +134,7 @@ def generate_mod(world, output_directory: str): "max_science_pack": multiworld.max_science_pack[player].value, "liquids": fluids, "goal": multiworld.goal[player].value, - "energy_link": multiworld.energy_link[player].value + "energy_link": multiworld.energy_link[player].value, } for factorio_option in Options.factorio_options: diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 64e03c4338..a3580f8ff2 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -41,17 +41,30 @@ class Goal(Choice): default = 0 -class TechCost(Choice): - """How expensive are the technologies.""" - display_name = "Technology Cost Scale" - option_very_easy = 0 - option_easy = 1 - option_kind = 2 - option_normal = 3 - option_hard = 4 - option_very_hard = 5 - option_insane = 6 - default = 3 +class TechCost(Range): + range_start = 1 + range_end = 10000 + default = 5 + + +class MinTechCost(TechCost): + """The cheapest a Technology can be in Science Packs.""" + display_name = "Minimum Science Pack Cost" + default = 5 + + +class MaxTechCost(TechCost): + """The most expensive a Technology can be in Science Packs.""" + display_name = "Maximum Science Pack Cost" + default = 500 + + +class TechCostMix(Range): + """Percent chance that a preceding Science Pack is also required. + Chance is rolled per preceding pack.""" + display_name = "Science Pack Cost Mix" + range_end = 100 + default = 70 class Silo(Choice): @@ -168,7 +181,7 @@ class FactorioFreeSampleWhitelist(OptionSet): class TrapCount(Range): - range_end = 4 + range_end = 25 class AttackTrapCount(TrapCount): @@ -343,7 +356,9 @@ factorio_options: typing.Dict[str, type(Option)] = { "max_science_pack": MaxSciencePack, "goal": Goal, "tech_tree_layout": TechTreeLayout, - "tech_cost": TechCost, + "min_tech_cost": MinTechCost, + "max_tech_cost": MaxTechCost, + "tech_cost_mix": TechCostMix, "silo": Silo, "satellite": Satellite, "free_samples": FreeSamples, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index f42da4d20c..4f093c3a86 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -1,8 +1,11 @@ -from typing import Dict, List, Set +from typing import Dict, List, Set, TYPE_CHECKING from collections import deque from .Options import TechTreeLayout +if TYPE_CHECKING: + from . import Factorio, FactorioScienceLocation + funnel_layers = {TechTreeLayout.option_small_funnels: 3, TechTreeLayout.option_medium_funnels: 4, TechTreeLayout.option_large_funnels: 5} @@ -12,24 +15,26 @@ funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6, TechTreeLayout.option_large_funnels: 15} -def get_shapes(factorio_world) -> Dict[str, List[str]]: +def _sorter(location: "FactorioScienceLocation"): + return location.complexity, location.rel_cost + + +def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]]: world = factorio_world.world player = factorio_world.player - prerequisites: Dict[str, Set[str]] = {} + prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} layout = world.tech_tree_layout[player].value - custom_technologies = factorio_world.custom_technologies - tech_names: List[str] = list(set(custom_technologies) - world.worlds[player].static_nodes) - tech_names.sort() - world.random.shuffle(tech_names) + locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name) + world.random.shuffle(locations) if layout == TechTreeLayout.option_single: pass elif layout == TechTreeLayout.option_small_diamonds: slice_size = 4 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) diamond_0, diamond_1, diamond_2, diamond_3 = slice # 0 | @@ -40,10 +45,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_medium_diamonds: slice_size = 9 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -65,10 +70,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_large_diamonds: slice_size = 16 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -101,10 +106,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_small_pyramids: slice_size = 6 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -119,10 +124,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_medium_pyramids: slice_size = 10 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -144,10 +149,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout == TechTreeLayout.option_large_pyramids: slice_size = 15 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) # 0 | # 1 2 | @@ -176,17 +181,17 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: elif layout in funnel_layers: slice_size = funnel_slice_sizes[layout] - world.random.shuffle(tech_names) + world.random.shuffle(locations) - while len(tech_names) > slice_size: - tech_names = tech_names[slice_size:] - current_tech_names = tech_names[:slice_size] + while len(locations) > slice_size: + locations = locations[slice_size:] + current_locations = locations[:slice_size] layer_size = funnel_layers[layout] previous_slice = [] - current_tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + current_locations.sort(key=_sorter) for layer in range(funnel_layers[layout]): - slice = current_tech_names[:layer_size] - current_tech_names = current_tech_names[layer_size:] + slice = current_locations[:layer_size] + current_locations = current_locations[layer_size:] if previous_slice: for i, tech_name in enumerate(slice): prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2]) @@ -202,10 +207,10 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: # 15 | # 16 | slice_size = 17 - while len(tech_names) > slice_size: - slice = tech_names[:slice_size] - tech_names = tech_names[slice_size:] - slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) + while len(locations) > slice_size: + slice = locations[:slice_size] + locations = locations[slice_size:] + slice.sort(key=_sorter) prerequisites[slice[1]] = {slice[0]} prerequisites[slice[2]] = {slice[0]} @@ -229,13 +234,13 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]: prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]} prerequisites[slice[16]] = {slice[15]} elif layout == TechTreeLayout.option_choices: - tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies())) - current_choices = deque([tech_names[0]]) - tech_names = tech_names[1:] - while len(tech_names) > 1: + locations.sort(key=_sorter) + current_choices = deque([locations[0]]) + locations = locations[1:] + while len(locations) > 1: source = current_choices.pop() - choices = tech_names[:2] - tech_names = tech_names[2:] + choices = locations[:2] + locations = locations[2:] for choice in choices: prerequisites[choice] = {source} current_choices.extendleft(choices) diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index b88cc9b1ad..09f9fc93f7 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -36,7 +36,7 @@ technology_table: Dict[str, Technology] = {} always = lambda state: True -class FactorioElement(): +class FactorioElement: name: str def __repr__(self): @@ -98,7 +98,7 @@ class CustomTechnology(Technology): and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"}) or origin.name == "rocket-silo") self.player = player - if origin.name not in world.worlds[player].static_nodes: + if origin.name not in world.worlds[player].special_nodes: if military_allowed: ingredients.add("military-science-pack") ingredients = list(ingredients) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index a01abac748..f1bdce1755 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -1,19 +1,20 @@ +from __future__ import annotations + import collections +import logging import typing -from worlds.AutoWorld import World, WebWorld - from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +from .Mod import generate_mod +from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal +from .Shapes import get_shapes from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ - fluids, stacking_items, valid_ingredients -from .Shapes import get_shapes -from .Mod import generate_mod -from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal - -import logging + fluids, stacking_items, valid_ingredients, progressive_rows +from .Locations import location_pools, location_table class FactorioWeb(WebWorld): @@ -43,89 +44,75 @@ class Factorio(World): research new technologies, and become more efficient in your quest to build a rocket and return home. """ game: str = "Factorio" - static_nodes = {"automation", "logistics", "rocket-silo"} + special_nodes = {"automation", "logistics", "rocket-silo"} custom_recipes: typing.Dict[str, Recipe] + location_pool: typing.List[FactorioScienceLocation] advancement_technologies: typing.Set[str] web = FactorioWeb() item_name_to_id = all_items - location_name_to_id = base_tech_table + # TODO: remove base_tech_table ~ 0.3.7 + location_name_to_id = {**base_tech_table, **location_table} item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 5 - required_client_version = (0, 3, 0) + data_version = 6 + required_client_version = (0, 3, 6) + + ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() + tech_mix: int = 0 + skip_silo: bool = False def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) self.advancement_technologies = set() self.custom_recipes = {} - - def generate_basic(self): - player = self.player - want_progressives = collections.defaultdict(lambda: self.world.progressive[player]. - want_progressives(self.world.random)) - skip_silo = self.world.silo[player].value == Silo.option_spawn - evolution_traps_wanted = self.world.evolution_traps[player].value - attack_traps_wanted = self.world.attack_traps[player].value - traps_wanted = ["Evolution Trap"] * evolution_traps_wanted + ["Attack Trap"] * attack_traps_wanted - self.world.random.shuffle(traps_wanted) - - for tech_name in base_tech_table: - if traps_wanted and tech_name in useless_technologies: - self.world.itempool.append(self.create_item(traps_wanted.pop())) - elif skip_silo and tech_name == "rocket-silo": - pass - else: - progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) - want_progressive = want_progressives[progressive_item_name] - item_name = progressive_item_name if want_progressive else tech_name - tech_item = self.create_item(item_name) - if tech_name in self.static_nodes: - self.world.get_location(tech_name, player).place_locked_item(tech_item) - else: - self.world.itempool.append(tech_item) - - map_basic_settings = self.world.world_gen[player].value["basic"] - if map_basic_settings.get("seed", None) is None: # allow seed 0 - map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint - - # used to be called "sending_visible" - if self.world.tech_tree_information[player] == TechTreeInformation.option_full: - # mark all locations as pre-hinted - self.world.start_location_hints[self.player].value.update(base_tech_table) + self.locations = [] generate_output = generate_mod + def generate_early(self) -> None: + self.world.max_tech_cost[self.player] = max(self.world.max_tech_cost[self.player], + self.world.min_tech_cost[self.player]) + self.tech_mix = self.world.tech_cost_mix[self.player] + self.skip_silo = self.world.silo[self.player].value == Silo.option_spawn + def create_regions(self): player = self.player + random = self.world.random menu = Region("Menu", RegionType.Generic, "Menu", player, self.world) crash = Entrance(player, "Crash Land", menu) menu.exits.append(crash) nauvis = Region("Nauvis", RegionType.Generic, "Nauvis", player, self.world) - skip_silo = self.world.silo[self.player].value == Silo.option_spawn - for tech_name, tech_id in base_tech_table.items(): - if skip_silo and tech_name == "rocket-silo": - continue - tech = Location(player, tech_name, tech_id, nauvis) - nauvis.locations.append(tech) - tech.game = "Factorio" - location = Location(player, "Rocket Launch", None, nauvis) + location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ + self.world.evolution_traps[player].value + self.world.attack_traps[player].value + + location_pool = [] + + for pack in self.world.max_science_pack[self.player].get_allowed_packs(): + location_pool.extend(location_pools[pack]) + location_names = self.world.random.sample(location_pool, location_count) + self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) + for loc_name in location_names] + rand_values = sorted(random.randint(self.world.min_tech_cost[self.player], + self.world.max_tech_cost[self.player]) for _ in self.locations) + for i, location in enumerate(sorted(self.locations, key=lambda loc: loc.rel_cost)): + location.count = rand_values[i] + del rand_values + nauvis.locations.extend(self.locations) + location = FactorioLocation(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) - location.game = "Factorio" event = FactorioItem("Victory", ItemClassification.progression, None, player) - event.game = "Factorio" - self.world.push_item(location, event, False) - location.event = location.locked = True + location.place_locked_item(event) + for ingredient in self.world.max_science_pack[self.player].get_allowed_packs(): - location = Location(player, f"Automate {ingredient}", None, nauvis) - location.game = "Factorio" + location = FactorioLocation(player, f"Automate {ingredient}", None, nauvis) nauvis.locations.append(location) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) - self.world.push_item(location, event, False) - location.event = location.locked = True + location.place_locked_item(event) + crash.connect(nauvis) self.world.regions += [menu, nauvis] @@ -151,17 +138,13 @@ class Factorio(World): location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) - skip_silo = self.world.silo[self.player].value == Silo.option_spawn - for tech_name, technology in self.custom_technologies.items(): - if skip_silo and tech_name == "rocket-silo": - continue - location = world.get_location(tech_name, player) - Rules.set_rule(location, technology.build_rule(player)) - prequisites = shapes.get(tech_name) - if prequisites: - locations = {world.get_location(requisite, player) for requisite in prequisites} - Rules.add_rule(location, lambda state, - locations=locations: all(state.can_reach(loc) for loc in locations)) + for location in self.locations: + Rules.set_rule(location, lambda state, ingredients=location.ingredients: + all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) + prerequisites = shapes.get(location) + if prerequisites: + Rules.add_rule(location, lambda state, locations= + prerequisites: all(state.can_reach(loc) for loc in locations)) silo_recipe = None if self.world.silo[self.player] == Silo.option_spawn: @@ -179,6 +162,48 @@ class Factorio(World): world.completion_condition[player] = lambda state: state.has('Victory', player) + def generate_basic(self): + player = self.player + want_progressives = collections.defaultdict(lambda: self.world.progressive[player]. + want_progressives(self.world.random)) + self.world.itempool.extend(self.create_item("Evolution Trap") for _ in + range(self.world.evolution_traps[player].value)) + self.world.itempool.extend(self.create_item("Attack Trap") for _ in + range(self.world.attack_traps[player].value)) + + cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) + special_index = {"automation": 0, + "logistics": 1, + "rocket-silo": -1} + loc: FactorioScienceLocation + if self.skip_silo: + removed = useless_technologies | {"rocket-silo"} + else: + removed = useless_technologies + for tech_name in base_tech_table: + if tech_name not in removed: + progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name) + want_progressive = want_progressives[progressive_item_name] + item_name = progressive_item_name if want_progressive else tech_name + tech_item = self.create_item(item_name) + index = special_index.get(tech_name, None) + if index is None: + self.world.itempool.append(tech_item) + else: + loc = cost_sorted_locations[index] + loc.place_locked_item(tech_item) + loc.revealed = True + + map_basic_settings = self.world.world_gen[player].value["basic"] + if map_basic_settings.get("seed", None) is None: # allow seed 0 + map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint + + if self.world.tech_tree_information[player] == TechTreeInformation.option_full: + # mark all locations as pre-hinted + self.world.start_location_hints[self.player].value.update(base_tech_table) + for loc in self.locations: + loc.revealed = True + def collect_item(self, state, item, remove=False): if item.advancement and item.name in progressive_technology_table: prog_table = progressive_technology_table[item.name].progressive @@ -400,3 +425,33 @@ class Factorio(World): ItemClassification.trap if "Trap" in name else ItemClassification.filler, all_items[name], self.player) return item + + +class FactorioLocation(Location): + game: str = Factorio.game + + +class FactorioScienceLocation(FactorioLocation): + complexity: int + revealed: bool = False + + # Factorio technology properties: + ingredients: typing.Dict[str, int] + count: int + + def __init__(self, player: int, name: str, address: int, parent: Region): + super(FactorioScienceLocation, self).__init__(player, name, address, parent) + # "AP-{Complexity}-{Cost}" + self.complexity = int(self.name[3]) - 1 + self.rel_cost = int(self.name[5:], 16) + + self.ingredients = {Factorio.ordered_science_packs[self.complexity]: 1} + for complexity in range(self.complexity): + if parent.world.tech_cost_mix[self.player] > parent.world.random.randint(0, 99): + self.ingredients[Factorio.ordered_science_packs[complexity]] = 1 + self.count = parent.world.random.randint(parent.world.min_tech_cost[self.player], + parent.world.max_tech_cost[self.player]) + + @property + def factorio_ingredients(self) -> typing.List[typing.Tuple[str, int]]: + return [(name, count) for name, count in self.ingredients.items()] diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index 70bc1eac0a..3021fd5dad 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -1,4 +1,4 @@ -{% from "macros.lua" import dict_to_recipe %} +{% from "macros.lua" import dict_to_recipe, variable_to_lua %} -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template require('lib') data.raw["rocket-silo"]["rocket-silo"].fluid_boxes = { @@ -50,16 +50,8 @@ data.raw["recipe"]["{{recipe_name}}"].ingredients = {{ dict_to_recipe(recipe.ing {%- endfor %} local technologies = data.raw["technology"] -local original_tech local new_tree_copy -allowed_ingredients = {} -{%- for tech_name, technology in custom_technologies.items() %} -allowed_ingredients["{{ tech_name }}"] = { -{%- for ingredient in technology.ingredients %} -["{{ingredient}}"] = 1, -{%- endfor %} -} -{% endfor %} + local template_tech = table.deepcopy(technologies["automation"]) {#- ensure the copy unlocks nothing #} template_tech.unlocks = {} @@ -87,39 +79,6 @@ template_tech.prerequisites = {} data.raw["recipe"]["rocket-silo"].enabled = true {% endif %} -function prep_copy(new_copy, old_tech) - old_tech.hidden = true - local ingredient_filter = allowed_ingredients[old_tech.name] - if ingredient_filter ~= nil then - if mods["science-not-invited"] then - local weights = { - ["automation-science-pack"] = 0, -- Red science - ["logistic-science-pack"] = 0, -- Green science - ["military-science-pack"] = 0, -- Black science - ["chemical-science-pack"] = 0, -- Blue science - ["production-science-pack"] = 0, -- Purple science - ["utility-science-pack"] = 0, -- Yellow science - ["space-science-pack"] = 0 -- Space science - } - for key, value in pairs(ingredient_filter) do - weights[key] = value - end - SNI.setWeights(weights) - -- Just in case an ingredient is being added to an existing tech. Found the root cause of the 9.223e+18 problem. - -- Turns out science-not-invited was ultimately dividing by zero, due to it being unaware of there being added ingredients. - old_tech.unit.ingredients = add_ingredients(old_tech.unit.ingredients, ingredient_filter) - SNI.sendInvite(old_tech) - -- SCIENCE-not-invited could potentially make tech cost 9.223e+18. - old_tech.unit.count = math.min(100000, old_tech.unit.count) - end - new_copy.unit = table.deepcopy(old_tech.unit) - new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter) - new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter) - else - new_copy.unit = table.deepcopy(old_tech.unit) - end -end - function set_ap_icon(tech) tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png" tech.icons = nil @@ -198,38 +157,40 @@ end data.raw["ammo"]["artillery-shell"].stack_size = 10 {# each randomized tech gets set to be invisible, with new nodes added that trigger those #} -{%- for original_tech_name, item_name, receiving_player, advancement in locations %} -original_tech = technologies["{{original_tech_name}}"] +{%- for original_tech_name in base_tech_table -%} +technologies["{{ original_tech_name }}"].hidden = true +{% endfor %} +{%- for location, item in locations %} {#- the tech researched by the local player #} new_tree_copy = table.deepcopy(template_tech) -new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} -prep_copy(new_tree_copy, original_tech) -{% if tech_cost_scale != 1 %} -new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }})) -{% endif %} -{%- if (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in base_tech_table -%} -{#- copy Factorio Technology Icon -#} -copy_factorio_icon(new_tree_copy, "{{ item_name }}") -{%- if original_tech_name == "rocket-silo" and original_tech_name in static_nodes %} +new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #} +new_tree_copy.unit.count = {{ location.count }} +new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }} + +{%- if location.revealed and item.name in base_tech_table -%} +{#- copy Factorio Technology Icon #} +copy_factorio_icon(new_tree_copy, "{{ item.name }}") +{%- if item.name == "rocket-silo" and item.player == location.player %} {%- for ingredient in custom_recipes["rocket-part"].ingredients %} table.insert(new_tree_copy.effects, {type = "nothing", effect_description = "Ingredient {{ loop.index }}: {{ ingredient }}"}) {% endfor -%} {% endif -%} -{%- elif (tech_tree_information == 2 or original_tech_name in static_nodes) and item_name in progressive_technology_table -%} -copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0] }}") +{%- elif location.revealed and item.name in progressive_technology_table -%} +copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item.name][0] }}") {%- else -%} {#- use default AP icon if no Factorio graphics exist -#} -{% if advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} +{% if item.advancement or not tech_tree_information %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %} {%- endif -%} {#- connect Technology #} -{%- if original_tech_name in tech_tree_layout_prerequisites %} -{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %} -table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-") +{%- if location in tech_tree_layout_prerequisites %} +{%- for prerequisite in tech_tree_layout_prerequisites[location] %} +table.insert(new_tree_copy.prerequisites, "ap-{{ prerequisite.address }}-") {% endfor %} {% endif -%} {#- add new Technology to game #} data:extend{new_tree_copy} {% endfor %} +{#- Recipe Rando #} {% if recipe_time_scale %} {%- for recipe_name, recipe in recipes.items() %} {%- if recipe.category not in ("basic-solid", "basic-fluid") %} diff --git a/worlds/factorio/data/mod_template/locale/en/locale.cfg b/worlds/factorio/data/mod_template/locale/en/locale.cfg index e970dbfa8c..59dcffcd63 100644 --- a/worlds/factorio/data/mod_template/locale/en/locale.cfg +++ b/worlds/factorio/data/mod_template/locale/en/locale.cfg @@ -5,22 +5,22 @@ archipelago=Archipelago archipelago=World preset created by the Archipelago Randomizer. World may or may not contain actual archipelagos. [technology-name] -{% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} -ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} +{% for location, item in locations %} +{%- if location.revealed %} +ap-{{ location.address }}-={{ player_names[item.player] }}'s {{ item.name }} ({{ location.name }}) {%- else %} -ap-{{ tech_table[original_tech_name] }}-=An Archipelago Sendable +ap-{{ location.address }}-= {{location.name}} {%- endif -%} {% endfor %} [technology-description] -{% for original_tech_name, item_name, receiving_player, advancement in locations %} -{%- if tech_tree_information == 2 or original_tech_name in static_nodes %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}{% if advancement %}, which is considered a logical advancement{% endif %}. -{%- elif tech_tree_information == 1 and advancement %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone, which is considered a logical advancement. For purposes of hints, this location is called "{{ original_tech_name }}". +{% for location, item in locations %} +{%- if location.revealed %} +ap-{{ location.address }}-=Researching this technology sends {{ item.name }} to {{ player_names[item.player] }}{% if item.advancement %}, which is considered a logical advancement{% elif item.useful %}, which is considered useful{% elif item.trap %}, which is considered fun{% endif %}. +{%- elif tech_tree_information == 1 and item.advancement %} +ap-{{ location.address }}-=Researching this technology sends something to someone, which is considered a logical advancement. {%- else %} -ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}". +ap-{{ location.address }}-=Researching this technology sends something to someone. {%- endif -%} {% endfor %} diff --git a/worlds/factorio/data/mod_template/macros.lua b/worlds/factorio/data/mod_template/macros.lua index c81ddc5fcc..1b271031a3 100644 --- a/worlds/factorio/data/mod_template/macros.lua +++ b/worlds/factorio/data/mod_template/macros.lua @@ -4,7 +4,7 @@ ["{{ key }}"] = {{ variable_to_lua(value) }}{% if not loop.last %},{% endif %} {% endfor -%} } -{%- endmacro %} +{% endmacro %} {% macro list_to_lua(list) -%} { {%- for key in list -%} From f298b8d6e71e010572735717ed57fed487aa2705 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Fri, 28 Oct 2022 12:56:50 -0700 Subject: [PATCH 4/6] Zillion: validate rescue item links (#1140) --- Main.py | 16 +++++++++------- worlds/AutoWorld.py | 2 ++ worlds/zillion/__init__.py | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Main.py b/Main.py index 38100bd050..d4df1b18ca 100644 --- a/Main.py +++ b/Main.py @@ -8,9 +8,9 @@ import concurrent.futures import pickle import tempfile import zipfile -from typing import Dict, Tuple, Optional, Set +from typing import Dict, List, Tuple, Optional, Set -from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location +from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from worlds.alttp.Items import item_name_groups from worlds.alttp.Regions import is_main_entrance from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned @@ -154,8 +154,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # temporary home for item links, should be moved out of Main for group_id, group in world.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]): - classifications = collections.defaultdict(int) + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) counters = {player: {name: 0 for name in shared_pool} for player in players} for item in world.itempool: if item.player in counters and item.name in shared_pool: @@ -165,7 +167,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in players.copy(): if all([counters[player][item] == 0 for item in shared_pool]): players.remove(player) - del(counters[player]) + del (counters[player]) if not players: return None, None @@ -177,14 +179,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No counters[player][item] = count else: for player in players: - del(counters[player][item]) + del (counters[player][item]) return counters, classifications common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) if not common_item_count: continue - new_itempool = [] + new_itempool: List[Item] = [] for item_name, item_count in next(iter(common_item_count.values())).items(): for _ in range(item_count): new_item = group["world"].create_item(item_name) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 8d3fab64cf..37cf5300a2 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -90,6 +90,8 @@ def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None: f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" " f"of player \"{world.player_name[player]}\". Please make a copy instead.") + # TODO: investigate: Iterating through a set is not a deterministic order. + # If any random is used, this could make unreproducible seed. for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index d982782840..918b91ec9b 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -11,7 +11,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \ from Options import AssembleOptions from .logic import cs_to_zz_locs from .region import ZillionLocation, ZillionRegion -from .options import zillion_options, validate +from .options import ZillionStartChar, zillion_options, validate from .id_maps import item_name_to_id as _item_name_to_id, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \ zz_reg_name_to_reg_name, base_id @@ -242,6 +242,42 @@ class ZillionWorld(World): self.world.completion_condition[self.player] = \ lambda state: state.has("Win", self.player) + @staticmethod + def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: + # item link pools are about to be created in main + # JJ can't be an item link unless all the players share the same start_char + # (The reason for this is that the JJ ZillionItem will have a different ZzItem depending + # on whether the start char is Apple or Champ, and the logic depends on that ZzItem.) + for group in multiworld.groups.values(): + # TODO: remove asserts on group when we can specify which members of TypedDict are optional + assert "game" in group + if group["game"] == "Zillion": + assert "item_pool" in group + item_pool = group["item_pool"] + to_stay = "JJ" + if "JJ" in item_pool: + assert "players" in group + group_players = group["players"] + start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char")) + players_start_chars = [ + (player, start_chars[player].get_current_option_name()) + for player in group_players + ] + start_char_counts = Counter(sc for _, sc in players_start_chars) + # majority rules + if start_char_counts["Apple"] > start_char_counts["Champ"]: + to_stay = "Apple" + elif start_char_counts["Champ"] > start_char_counts["Apple"]: + to_stay = "Champ" + else: # equal + to_stay = multiworld.random.choice(("Apple", "Champ")) + + for p, sc in players_start_chars: + if sc != to_stay: + group_players.remove(p) + assert "world" in group + cast(ZillionWorld, group["world"])._make_item_maps(to_stay) + def post_fill(self) -> None: """Optional Method that is called after regular fill. Can be used to do adjustments before output generation. This happens before progression balancing, so the items may not be in their final locations yet.""" From 3dfbbc5057afe24546462d2849657720eb12b412 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 28 Oct 2022 23:02:23 +0200 Subject: [PATCH 5/6] Doc: Clarify annotations in style guide (#1149) * Doc: Clarify annotations in style guide * Fix typo * Update docs/style.md Co-authored-by: Doug Hoskisson Co-authored-by: Doug Hoskisson --- docs/style.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/style.md b/docs/style.md index a9f55caa7c..745b51ed6e 100644 --- a/docs/style.md +++ b/docs/style.md @@ -15,7 +15,9 @@ * Strings in worlds should use double quotes as well, but imported code may differ. * Prefer [format string literals](https://peps.python.org/pep-0498/) over string concatenation, use single quotes inside them: `f"Like {dct['key']}"` -* Use type annotation where possible. +* Use type annotations where possible for function signatures and class members. +* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the + type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls. ## Markdown From c711264d1a983aafc975ef64115a9185e440c03c Mon Sep 17 00:00:00 2001 From: Ludovic Marechal Date: Sat, 29 Oct 2022 13:35:33 +0200 Subject: [PATCH 6/6] DS3: Added a few new items and locations (#1059) * Update items_data.py added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear. * Update locations_data.py Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll` * Update __init__.py Add `Karla's Ashes` requirements * Update items_data.py Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes` * Update locations_data.py Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes` * Update items_data.py removed "hollows ashes" * Update locations_data.py remove "hollows ashes" * DS3: Increment data_version * DS3: Fix item name in rule * DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility * DS3: Resolve Python 3.8 compatibility * DS3: Removed useless region for locations IDs consistency * DS3: Changed i in loop Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com> --- worlds/dark_souls_3/Items.py | 17 ++++++++++ worlds/dark_souls_3/Locations.py | 17 ++++++++++ worlds/dark_souls_3/__init__.py | 36 +++++++++------------- worlds/dark_souls_3/data/items_data.py | 17 ++++++++-- worlds/dark_souls_3/data/locations_data.py | 28 ++++++++++++----- 5 files changed, 85 insertions(+), 30 deletions(-) create mode 100644 worlds/dark_souls_3/Items.py create mode 100644 worlds/dark_souls_3/Locations.py diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py new file mode 100644 index 0000000000..e7ba2ecf00 --- /dev/null +++ b/worlds/dark_souls_3/Items.py @@ -0,0 +1,17 @@ +from BaseClasses import Item +from worlds.dark_souls_3.data.items_data import item_tables + + +class DarkSouls3Item(Item): + game: str = "Dark Souls III" + + @staticmethod + def get_name_to_id() -> dict: + base_id = 100000 + table_offset = 100 + + output = {} + for i, table in enumerate(item_tables): + output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) + + return output diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py new file mode 100644 index 0000000000..0ba84e365b --- /dev/null +++ b/worlds/dark_souls_3/Locations.py @@ -0,0 +1,17 @@ +from BaseClasses import Location +from worlds.dark_souls_3.data.locations_data import location_tables + + +class DarkSouls3Location(Location): + game: str = "Dark Souls III" + + @staticmethod + def get_name_to_id() -> dict: + base_id = 100000 + table_offset = 100 + + output = {} + for i, table in enumerate(location_tables): + output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))}) + + return output diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 1ded4203c5..df56306cc5 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -2,9 +2,11 @@ import json import os +from .Items import DarkSouls3Item +from .Locations import DarkSouls3Location from .Options import dark_souls_options -from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list -from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \ +from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list +from .data.locations_data import location_dictionary, fire_link_shrine_table, \ high_wall_of_lothric, \ undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \ farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \ @@ -51,10 +53,11 @@ class DarkSouls3World(World): remote_items: bool = False remote_start_inventory: bool = False web = DarkSouls3Web() - data_version = 2 + data_version = 3 base_id = 100000 - item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)} - location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)} + required_client_version = (0, 3, 6) + item_name_to_id = DarkSouls3Item.get_name_to_id() + location_name_to_id = DarkSouls3Location.get_name_to_id() def __init__(self, world: MultiWorld, player: int): super().__init__(world, player) @@ -79,7 +82,6 @@ class DarkSouls3World(World): self.world.regions.append(menu_region) # Create all Vanilla regions of Dark Souls III - cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table) firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table) firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower", firelink_shrine_bell_tower_table) @@ -104,9 +106,7 @@ class DarkSouls3World(World): # Create the entrance to connect those regions menu_region.exits.append(Entrance(self.player, "New Game", menu_region)) - self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region) - cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region)) - self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region) + self.world.get_entrance("New Game", self.player).connect(firelink_shrine_region) firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric", firelink_shrine_region)) firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame", @@ -209,6 +209,8 @@ class DarkSouls3World(World): lambda state: state.has("Jailer's Key Ring", self.player)) set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player), lambda state: state.has("Old Cell Key", self.player)) + set_rule(self.world.get_location("ID: Karla's Ashes", self.player), + lambda state: state.has("Jailer's Key Ring", self.player)) black_hand_gotthard_corpse_rule = lambda state: \ (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) @@ -237,18 +239,18 @@ class DarkSouls3World(World): def generate_output(self, output_directory: str): # Depending on the specified option, modify items hexadecimal value to add an upgrade level - item_dictionary = item_dictionary_table.copy() + item_dictionary_copy = item_dictionary.copy() if self.world.randomize_weapons_level[self.player]: # Randomize some weapons upgrades for name in weapons_upgrade_5_table.keys(): if self.world.random.randint(0, 100) < 33: value = self.world.random.randint(1, 5) - item_dictionary[name] += value + item_dictionary_copy[name] += value for name in weapons_upgrade_10_table.keys(): if self.world.random.randint(0, 100) < 33: value = self.world.random.randint(1, 10) - item_dictionary[name] += value + item_dictionary_copy[name] += value # Create the mandatory lists to generate the player's output file items_id = [] @@ -262,7 +264,7 @@ class DarkSouls3World(World): items_address.append(item_dictionary[location.item.name]) if location.player == self.player: - locations_address.append(location_dictionary_table[location.name]) + locations_address.append(location_dictionary[location.name]) locations_id.append(location.address) if location.item.player == self.player: locations_target.append(item_dictionary[location.item.name]) @@ -289,11 +291,3 @@ class DarkSouls3World(World): filename = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}.json" with open(os.path.join(output_directory, filename), 'w') as outfile: json.dump(data, outfile) - - -class DarkSouls3Location(Location): - game: str = "Dark Souls III" - - -class DarkSouls3Item(Item): - game: str = "Dark Souls III" diff --git a/worlds/dark_souls_3/data/items_data.py b/worlds/dark_souls_3/data/items_data.py index 9add18820b..e951b674fe 100644 --- a/worlds/dark_souls_3/data/items_data.py +++ b/worlds/dark_souls_3/data/items_data.py @@ -52,6 +52,7 @@ weapons_upgrade_5_table = { "Storm Curved Sword": 0x003E4180, "Dragonslayer Swordspear": 0x008BC540, "Sage's Crystal Staff": 0x00C8CE40, + "Irithyll Rapier": 0x002E8A10 } weapons_upgrade_10_table = { @@ -131,7 +132,7 @@ shields_table = { "Golden Wing Crest Shield": 0x0143CAA0, "Ancient Dragon Greatshield": 0x013599D0, "Spirit Tree Crest Shield": 0x014466E0, - + "Red and White Round Shield": 0x01343A40, } goods_table = { @@ -244,6 +245,10 @@ armor_table = { "Shadow Garb": 0x14D3FA28, "Shadow Gauntlets": 0x14D3FE10, "Shadow Leggings": 0x14D401F8, + "Outrider Knight Helm": 0x1328B740, + "Outrider Knight Armor": 0x1328BB28, + "Outrider Knight Gauntlets": 0x1328BF10, + "Outrider Knight Leggings": 0x1328C2F8, } rings_table = { @@ -292,6 +297,7 @@ rings_table = { "Red Tearstone Ring": 0x20004ECA, "Dragonscale Ring": 0x2000515E, "Knight Slayer's Ring": 0x20005000, + "Magic Stoneplate Ring": 0x20004E66, } spells_table = { @@ -313,6 +319,7 @@ spells_table = { "Divine Pillars of Light": 0x4038C340, "Great Magic Barrier": 0x40365628, "Great Magic Shield": 0x40144F38, + "Crystal Scroll": 0x40000856, } misc_items_table = { @@ -359,6 +366,10 @@ misc_items_table = { "Dragon Chaser's Ashes": 0x40000867, "Twinkling Dragon Torso Stone": 0x40000184, "Braille Divine Tome of Lothric": 0x40000848, + "Irina's Ashes": 0x40000843, + "Karla's Ashes": 0x40000842, + "Cornyx's Ashes": 0x40000841, + "Orbeck's Ashes": 0x40000840 } key_items_list = { @@ -380,4 +391,6 @@ key_items_list = { "Jailer's Key Ring", } -item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} +item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, armor_table, rings_table, spells_table, misc_items_table, goods_table] + +item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table} diff --git a/worlds/dark_souls_3/data/locations_data.py b/worlds/dark_souls_3/data/locations_data.py index 384da049ac..855b4a9ab3 100644 --- a/worlds/dark_souls_3/data/locations_data.py +++ b/worlds/dark_souls_3/data/locations_data.py @@ -5,9 +5,6 @@ Regular expression parser https://regex101.com/r/XdtiLR/2 List of locations https://darksouls3.wiki.fextralife.com/Locations """ -cemetery_of_ash_table = { -} - fire_link_shrine_table = { # "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless "FS: Broken Straight Sword": 0x001EF9B0, @@ -92,6 +89,9 @@ undead_settlement_table = { "US: Soul of the Rotted Greatwood": 0x400002D7, "US: Hawk Ring": 0x20004F92, "US: Warrior of Sunlight Covenant": 0x20002738, + "US: Red and White Round Shield": 0x01343A40, + "US: Irina's Ashes": 0x40000843, + "US: Cornyx's Ashes": 0x40000841 } road_of_sacrifice_table = { @@ -149,6 +149,7 @@ road_of_sacrifice_table = { "RS: Grass Crest Shield": 0x01437C80, "RS: Soul of a Crystal Sage": 0x400002CB, "RS: Great Swamp Ring": 0x20004F10, + "RS: Orbeck's Ashes": 0x40000840 } cathedral_of_the_deep_table = { @@ -315,6 +316,7 @@ irithyll_dungeon_table = { "ID: Jailer's Key Ring": 0x400007D8, "ID: Dusk Crown Ring": 0x20004F4C, "ID: Dark Clutch Ring": 0x20005028, + "ID: Karla's Ashes": 0x40000842 } profaned_capital_table = { @@ -359,6 +361,7 @@ lothric_castle_table = { "LC: Caitha's Chime": 0x00CA06C0, "LC: Braille Divine Tome of Lothric": 0x40000848, "LC: Knight's Ring": 0x20004FEC, + "LC: Irithyll Rapier": 0x002E8A10, "LC: Sunlight Straight Sword": 0x00203230, "LC: Soul of Dragonslayer Armour": 0x400002D1, @@ -375,6 +378,7 @@ consumed_king_garden_table = { "CKG: Shadow Leggings": 0x14D401F8, "CKG: Claw": 0x00A7D8C0, "CKG: Soul of Consumed Oceiros": 0x400002CE, + "CKG: Magic Stoneplate Ring": 0x20004E66, # "CKG: Path of the Dragon Gesture": 0x40002346, I can't technically randomize it as it is a gesture and not an item } @@ -393,6 +397,11 @@ grand_archives_table = { "GA: Cinders of a Lord - Lothric Prince": 0x4000084E, "GA: Soul of the Twin Princes": 0x400002DB, "GA: Sage's Crystal Staff": 0x00C8CE40, + "GA: Outrider Knight Helm": 0x1328B740, + "GA: Outrider Knight Armor": 0x1328BB28, + "GA: Outrider Knight Gauntlets": 0x1328BF10, + "GA: Outrider Knight Leggings": 0x1328C2F8, + "GA: Crystal Scroll": 0x40000856, } untended_graves_table = { @@ -428,7 +437,12 @@ archdragon_peak_table = { "AP: Havel's Greatshield": 0x013376F0, } -location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, - **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, - **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, - **grand_archives_table, **untended_graves_table, **archdragon_peak_table} +location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table, + cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, + irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table, + grand_archives_table, untended_graves_table, archdragon_peak_table] + +location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table, + **cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table, + **irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table, + **grand_archives_table, **untended_graves_table, **archdragon_peak_table}