Merge branch 'main' into feature/ds3_use_slotdata

This commit is contained in:
Marechal-l
2022-10-29 15:43:32 +02:00
16 changed files with 348 additions and 244 deletions

View File

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

16
Main.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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") %}

View File

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

View File

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

View File

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