Compare commits

..

60 Commits
0.1.1 ... 0.1.2

Author SHA1 Message Date
Fabian Dill
4b5ac3f926 Update VC Redist 2021-06-07 11:53:33 +02:00
Fabian Dill
72e5acfb86 Factorio recipe time: adjust triangular mode 2021-06-07 11:32:39 +02:00
Fabian Dill
4b283242fe FactorioClient: remove duplicate log 2021-06-06 23:59:15 +02:00
Fabian Dill
353ea0fbbe encode correct color 2021-06-06 23:44:04 +02:00
Fabian Dill
fc941f55ef FactorioClientGUI.py: disable multitouch emulation on mouse 2021-06-06 23:23:06 +02:00
Fabian Dill
12600a8cbd FactorioClientGUI.py: fix frozen logging 2021-06-06 23:13:19 +02:00
Fabian Dill
33fa9542e0 move FactorioJSONtoTextParser 2021-06-06 22:49:37 +02:00
Fabian Dill
d872ea32af Update various links 2021-06-06 22:14:13 +02:00
Fabian Dill
46bb2d1367 Factorio: add chaos recipe time and use random.triangular distribution 2021-06-06 21:38:53 +02:00
Fabian Dill
403ddd603f Factorio: implement random recipe times 2021-06-06 21:11:58 +02:00
Fabian Dill
7907838c24 Factorio: Revamp Tech Tree Layouts 2021-06-06 20:26:40 +02:00
Fabian Dill
15bd79186a remove player_name feature in MultiMystery
MultiMystery is slated to be integrated into Mystery and the auto-launch feature is not maintainable for a growing games list
2021-06-06 18:12:19 +02:00
Fabian Dill
4555b77204 FactorioClient.py formatting 2021-06-06 17:50:48 +02:00
Fabian Dill
dd3c612dec Factorio: Colored ingame text relay for AP texts 2021-06-06 17:41:06 +02:00
Fabian Dill
09b6698de8 revamp some spoiler log conditions 2021-06-06 17:13:34 +02:00
Fabian Dill
27ee156706 tiny cleanup 2021-06-06 17:10:49 +02:00
espeon65536
48c3d1fa4a Added campfire for Sticky Situation, by popular demand 2021-06-06 15:10:45 +00:00
espeon65536
286254c5cd require end crystals for Free the End, since it's possible to kill the dragon with beds and not receive the advancement 2021-06-06 15:10:45 +00:00
espeon65536
82cd51f5f4 structure plando for Minecraft 2021-06-06 15:10:45 +00:00
espeon65536
08bf993146 only write Medallions section to spoiler log if there is an ALttP world 2021-06-06 15:10:45 +00:00
espeon65536
a55bcae3ec Minecraft logic improvements
- Very Very Frightening now properly accounts for getting a villager into the overworld by curing a zombie villager
- Hot Tourist Destinations no longer requires striders, since no one was using them anyway
- Saddles are now also obtainable from raids by killing a ravager (100% drop rate)
2021-06-06 15:10:45 +00:00
Fabian Dill
607a14e921 FactorioClient: log kivy exceptions 2021-06-06 16:09:00 +02:00
Fabian Dill
c71387ad00 Factorio: fix single-player static node placement 2021-06-06 16:08:17 +02:00
Fabian Dill
c095c28618 Split requirements into world types, automatically discover and resolve them. 2021-06-06 15:30:20 +02:00
Fabian Dill
cae1188ff8 Allow ModuleUpdate to use multiple requirements files, no longer need to care about naming, and use conventional requirement parsing. Also add WebHost to it. 2021-06-06 15:11:17 +02:00
CaitSith2
7e599c51f8 Make defaults for missing options in host.yaml consistent. 2021-06-05 21:15:54 -07:00
CaitSith2
6ccb9d2dc2 Fix adjuster reference 2021-06-05 13:58:59 -07:00
Fabian Dill
1d00ed463e fix updated name aliases for tracker 2021-06-05 03:54:16 +02:00
Fabian Dill
c99054e479 add /build_factorio to gitignore 2021-06-04 01:00:03 +02:00
Fabian Dill
85a9e0d0bc write Factorio options to spoiler 2021-06-04 00:29:59 +02:00
Fabian Dill
8b4ea3c80c fix max progressive item icon in per-player tracker 2021-06-03 01:02:31 +02:00
Fabian Dill
30dec34b72 update websockets 2021-06-02 04:40:43 +02:00
Fabian Dill
a3d2df7c45 Merge branch 'factorio_gui_client' into Archipelago_Main 2021-06-02 04:31:39 +02:00
Fabian Dill
034f338f45 set default hint cost to 10 2021-06-01 04:28:15 +02:00
Fabian Dill
1d84346705 Factorio: Don't trigger bridge file on receiving a technology from server 2021-05-29 20:02:36 +02:00
Fabian Dill
6e916ebd45 bake correct minimum version for Factorio into multidata 2021-05-29 06:23:35 +02:00
Fabian Dill
a993bed8dc move factorio_client_setup.py into setup.py 2021-05-27 12:26:08 +02:00
Fabian Dill
aa6f65ee1f Prevent logical lockout from Pedestal/Pyramid Fairy in ice rod hunt 2021-05-27 12:14:20 +02:00
Fabian Dill
573931930c remove debugging helper 2021-05-25 01:06:15 +02:00
Fabian Dill
252bb69808 FactorioClient: Read Bridge file after a server log indicates that the file was written 2021-05-25 01:03:04 +02:00
Fabian Dill
0175c8ab8a move FactorioClient log to logs folder 2021-05-24 16:09:10 +02:00
Fabian Dill
f78bb2078d make sure Factorio subprocess is terminated properly 2021-05-24 13:51:27 +02:00
Fabian Dill
bc028a63cd first version of a Factorio Graphical Client 2021-05-24 12:49:01 +02:00
Fabian Dill
4b04f2b918 update icons 2021-05-24 12:48:18 +02:00
Fabian Dill
887a3b0922 update flask and jinja 2021-05-24 05:03:45 +02:00
Fabian Dill
3df78fa387 Factorio add ap_unimportant.png 2021-05-23 20:13:19 +02:00
Fabian Dill
c36ac5baba consider the ability to craft a rocket-silo for factorio completion 2021-05-22 21:13:53 +02:00
Fabian Dill
d8e33fe596 Factorio: Differentiate advancement items. 2021-05-22 10:46:27 +02:00
Fabian Dill
80b7e2e188 Factorio: Build logic for rocket launch, allow beatable only to work correctly
Convert Science requirements to Event of "automate <pack>"
2021-05-22 10:06:21 +02:00
Fabian Dill
14b430a168 Factorio: simplify resulting data-final-fixes.lua after templating a bit. 2021-05-22 08:08:37 +02:00
Fabian Dill
22aa4cbb9f Factorio: Fix Rocket Launch event getting encoded into mod 2021-05-22 07:54:12 +02:00
Fabian Dill
71bb5b850e set correct player ID for Factorio Victory 2021-05-22 07:06:09 +02:00
Fabian Dill
066c830a43 Fix LttP progressive starting Items not writing to ROM 2021-05-22 06:27:22 +02:00
Fabian Dill
760107becf remove no longer needed imports 2021-05-22 03:00:24 +02:00
Fabian Dill
8dad49e385 assign generic tracker's checked locations to correct player 2021-05-20 01:22:18 +02:00
Fabian Dill
518e5db55b use item_name filter for generic tracker 2021-05-19 21:57:10 +02:00
Fabian Dill
31a3c1cf33 Add a generic fallback tracker for all games 2021-05-19 21:55:18 +02:00
Fabian Dill
e1b4975a11 Add Crafting Machine awareness to Factorio logic
(should have no effect on vanilla, mostly for modded gameplay)
2021-05-19 06:52:53 +02:00
Fabian Dill
f8a5e8bfc7 add Factorio Victory Event 2021-05-19 05:33:44 +02:00
Fabian Dill
a656ad5cd2 potential fix for rcon timing issue 2021-05-18 20:45:56 +02:00
59 changed files with 1130 additions and 394 deletions

3
.gitignore vendored
View File

@@ -16,6 +16,7 @@
*.apsave *.apsave
build build
/build_factorio/
bundle/components.wxs bundle/components.wxs
dist dist
README.html README.html
@@ -142,4 +143,4 @@ dmypy.json
.pytype/ .pytype/
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/

View File

@@ -1193,6 +1193,14 @@ class Location():
return True return True
return False return False
def place_locked_item(self, item: Item):
if self.item:
raise Exception(f"Location {self} already filled.")
self.item = item
self.event = item.advancement
self.item.world = self.parent_region.world
self.locked = True
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -1221,7 +1229,7 @@ class Item():
zora_credit_text = None zora_credit_text = None
fluteboy_credit_text = None fluteboy_credit_text = None
def __init__(self, name: str, advancement: bool, code: int, player: int): def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
self.name = name self.name = name
self.advancement = advancement self.advancement = advancement
self.player = player self.player = player
@@ -1467,6 +1475,7 @@ class Spoiler(object):
return json.dumps(out) return json.dumps(out)
def to_file(self, filename): def to_file(self, filename):
import Options
self.parse_data() self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str: def bool_to_text(variable: Union[bool, str]) -> str:
@@ -1490,16 +1499,21 @@ class Spoiler(object):
'Yes' if self.metadata['progression_balancing'][player] else 'No')) 'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
if player in self.world.hk_player_ids: if player in self.world.hk_player_ids:
import Options
for hk_option in Options.hollow_knight_options: for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player] res = getattr(self.world, hk_option)[player]
outfile.write(f'{hk_option+":":33}{res}\n') outfile.write(f'{hk_option+":":33}{res}\n')
if player in self.world.minecraft_player_ids:
import Options elif player in self.world.factorio_player_ids:
for f_option in Options.factorio_options:
res = getattr(self.world, f_option)[player]
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
elif player in self.world.minecraft_player_ids:
for mc_option in Options.minecraft_options: for mc_option in Options.minecraft_options:
res = getattr(self.world, mc_option)[player] res = getattr(self.world, mc_option)[player]
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
if player in self.world.alttp_player_ids:
elif player in self.world.alttp_player_ids:
for team in range(self.world.teams): for team in range(self.world.teams):
outfile.write('%s%s\n' % ( outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
@@ -1575,17 +1589,24 @@ class Spoiler(object):
'<=>' if entry['direction'] == 'both' else '<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>', '<=' if entry['direction'] == 'exit' else '=>',
entry['exit']) for entry in self.entrances.values()])) entry['exit']) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items(): if self.medallions:
outfile.write(f'\n{dungeon}: {medallion}') outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.startinventory: if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n') outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory)) outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n') outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()])) outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops)) if self.shops:
for player in range(1, self.world.players + 1): outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
for player in self.world.alttp_player_ids:
if self.world.boss_shuffle[player] != 'none': if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n') outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n')
@@ -1595,19 +1616,20 @@ class Spoiler(object):
if self.unreachables: if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n') outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables])) outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n')
path_listings = [] if self.paths:
for location, path in sorted(self.paths.items()): outfile.write('\n\nPaths:\n\n')
path_lines = [] path_listings = []
for region, exit in path: for location, path in sorted(self.paths.items()):
if exit is not None: path_lines = []
path_lines.append("{} -> {}".format(region, exit)) for region, exit in path:
else: if exit is not None:
path_lines.append(region) path_lines.append("{} -> {}".format(region, exit))
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines))) else:
path_lines.append(region)
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings)) outfile.write('\n'.join(path_listings))
from worlds.alttp.Items import item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.generic import PlandoItem, PlandoConnection from worlds.generic import PlandoItem, PlandoConnection

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
import os import os
import logging import logging
import json import json
import string import string
import copy import copy
import sys
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import colorama import colorama
@@ -13,7 +15,7 @@ from MultiServer import mark_raw
import Utils import Utils
import random import random
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio.Technologies import lookup_id_to_name
@@ -21,7 +23,6 @@ rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32)) rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago" save_name = "Archipelago"
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
options = Utils.get_options() options = Utils.get_options()
executable = options["factorio_options"]["executable"] executable = options["factorio_options"]["executable"]
@@ -34,13 +35,14 @@ if not os.path.exists(executable):
else: else:
raise FileNotFoundError(executable) raise FileNotFoundError(executable)
import sys
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:]) server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
threadpool = ThreadPoolExecutor(10) thread_pool = ThreadPoolExecutor(10)
class FactorioCommandProcessor(ClientCommandProcessor): class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
@mark_raw @mark_raw
def _cmd_factorio(self, text: str) -> bool: def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server.""" """Send the following command to the bound Factorio Server."""
@@ -65,7 +67,9 @@ class FactorioContext(CommonContext):
super(FactorioContext, self).__init__(*args, **kwargs) super(FactorioContext, self).__init__(*args, **kwargs)
self.send_index = 0 self.send_index = 0
self.rcon_client = None self.rcon_client = None
self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self) self.raw_json_text_parser = RawJSONtoTextParser(self)
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested): async def server_auth(self, password_requested):
if password_requested and not self.password: if password_requested and not self.password:
@@ -81,43 +85,50 @@ class FactorioContext(CommonContext):
logger.info(args["text"]) logger.info(args["text"])
if self.rcon_client: if self.rcon_client:
cleaned_text = args['text'].replace('"', '') cleaned_text = args['text'].replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
def on_print_json(self, args: dict): def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]: if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups. pass # don't want info on other player's local pickups.
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
logger.info(self.jsontotextparser(args["data"])) logger.info(text)
if self.rcon_client: if self.rcon_client:
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '') text = self.factorio_json_text_parser(args["data"])
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")") cleaned_text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
async def game_watcher(ctx: FactorioContext, bridge_file: str): async def game_watcher(ctx: FactorioContext, bridge_file: str):
bridge_logger = logging.getLogger("FactorioWatcher") bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0 bridge_counter = 0
try: try:
while 1: while not ctx.exit_event.is_set():
if os.path.exists(bridge_file): if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.") bridge_logger.info("Found Factorio Bridge file.")
while 1: while not ctx.exit_event.is_set():
with open(bridge_file) as f: if ctx.awaiting_bridge:
data = json.load(f) ctx.awaiting_bridge = False
research_data = data["research_done"] with open(bridge_file) as f:
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data} data = json.load(f)
victory = data["victory"] research_data = data["research_done"]
ctx.auth = data["slot_name"] research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
ctx.seed_name = data["seed_name"] victory = data["victory"]
ctx.auth = data["slot_name"]
ctx.seed_name = data["seed_name"]
if not ctx.finished_game and victory: if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True ctx.finished_game = True
if ctx.locations_checked != research_data: if ctx.locations_checked != research_data:
bridge_logger.info(f"New researches done: " bridge_logger.info(
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}") f"New researches done: "
ctx.locations_checked = research_data f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}]) ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1) await asyncio.sleep(1)
else: else:
bridge_counter += 1 bridge_counter += 1
@@ -160,12 +171,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
stream_factorio_output(factorio_process.stdout, factorio_queue) stream_factorio_output(factorio_process.stdout, factorio_queue)
stream_factorio_output(factorio_process.stderr, factorio_queue) stream_factorio_output(factorio_process.stderr, factorio_queue)
script_folder = None script_folder = None
progression_watcher = None
try: try:
while 1: while not ctx.exit_event.is_set():
while not factorio_queue.empty(): while not factorio_queue.empty():
msg = factorio_queue.get() msg = factorio_queue.get()
factorio_server_logger.info(msg) factorio_server_logger.info(msg)
if not ctx.rcon_client and "Hosting game at IP ADDR:" in msg: if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# trigger lua interface confirmation # trigger lua interface confirmation
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
@@ -177,7 +189,10 @@ async def factorio_server_watcher(ctx: FactorioContext):
if os.path.exists(bridge_file): if os.path.exists(bridge_file):
os.remove(bridge_file) os.remove(bridge_file)
logging.info(f"Bridge File Path: {bridge_file}") logging.info(f"Bridge File Path: {bridge_file}")
asyncio.create_task(game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher") progression_watcher = asyncio.create_task(
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
ctx.awaiting_bridge = True
if ctx.rcon_client: if ctx.rcon_client:
while ctx.send_index < len(ctx.items_received): while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index] transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
@@ -196,6 +211,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
logging.exception(e) logging.exception(e)
logging.error("Aborted Factorio Server Bridge") logging.error("Aborted Factorio Server Bridge")
finally:
factorio_process.terminate()
if progression_watcher:
await progression_watcher
async def main(): async def main():
ctx = FactorioContext(None, None, True) ctx = FactorioContext(None, None, True)
@@ -226,6 +246,20 @@ async def main():
await input_task await input_task
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
"brown", "cyan", "acid"}:
node["text"] = f"[color={color}]{node['text']}[/color]"
return self._handle_text(node)
elif color == "magenta":
node["text"] = f"[color=pink]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__': if __name__ == '__main__':
colorama.init() colorama.init()
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

167
FactorioClientGUI.py Normal file
View File

@@ -0,0 +1,167 @@
import os
import logging
import sys
os.makedirs("logs", exist_ok=True)
if getattr(sys, "frozen", False):
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
import asyncio
from CommonClient import server_loop, logger
from FactorioClient import FactorioContext, factorio_server_watcher
async def main():
ctx = FactorioContext(None, None, True)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
ui_app = FactorioManager(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await ctx.exit_event.wait() # wait for signal to exit application
ui_app.stop()
ctx.server_address = None
ctx.snes_reconnect_address = None
# allow tasks to quit
await ui_task
await factorio_server_task
await ctx.server_task
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
while ctx.input_requests > 0: # clear queue for shutdown
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
from kivy.app import App
from kivy.uix.label import Label
from kivy.base import ExceptionHandler, ExceptionManager, Config
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.lang import Builder
class FactorioManager(App):
def __init__(self, ctx):
super(FactorioManager, self).__init__()
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = "data/icon.png"
def build(self):
self.grid = GridLayout()
self.grid.cols = 1
self.tabs = TabbedPanel()
self.tabs.default_tab_text = "All"
self.title = "Archipelago Factorio Client"
pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge File Log"),
]
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
for logger_name, display_name in pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
panel.content = UILog(bridge_logger)
self.tabs.add_widget(panel)
self.grid.add_widget(self.tabs)
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
textinput.bind(on_text_validate=self.on_message)
self.grid.add_widget(textinput)
self.commandprocessor("/help")
return self.grid
def on_stop(self):
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
elif input_text:
self.commandprocessor(input_text)
except Exception as e:
logger.exception(e)
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
self.on_log = on_log
def handle(self, record: logging.LogRecord) -> None:
self.on_log(record)
class UILog(RecycleView):
cols = 1
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
self.data = []
for logger in loggers_to_handle:
logger.addHandler(LogtoUI(self.on_log))
def on_log(self, record: logging.LogRecord) -> None:
self.data.append({"text": record.getMessage()})
class E(ExceptionHandler):
def handle_exception(self, inst):
logger.exception(inst)
return ExceptionManager.RAISE
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Builder.load_string('''
<TabbedPanel>
tab_width: 200
<Row@Label>:
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
Rectangle:
size: self.size
pos: self.pos
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: dp(20)
<UILog>:
viewclass: 'Row'
scroll_y: 0
effect_cls: "ScrollEffect"
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
''')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

View File

@@ -5,10 +5,8 @@ import tkinter as tk
from Utils import local_path from Utils import local_path
def set_icon(window): def set_icon(window):
er16 = tk.PhotoImage(file=local_path('data', 'ER16.gif')) logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
er32 = tk.PhotoImage(file=local_path('data', 'ER32.gif')) window.tk.call('wm', 'iconphoto', window._w, logo)
er48 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
# Although tkinter is intended to be thread safe, there are many reports of issues # Although tkinter is intended to be thread safe, there are many reports of issues
# some which may be platform specific, or depend on if the TCL library was compiled without # some which may be platform specific, or depend on if the TCL library was compiled without

View File

@@ -153,8 +153,8 @@ def adjust(args):
def adjustGUI(): def adjustGUI():
from tkinter import Checkbutton, OptionMenu, Toplevel, LabelFrame, PhotoImage, Tk, LEFT, RIGHT, BOTTOM, TOP, \ from tkinter import Tk, LEFT, BOTTOM, TOP, \
StringVar, IntVar, Frame, Label, W, E, X, BOTH, Entry, Spinbox, Button, filedialog, messagebox, ttk StringVar, Frame, Label, X, Entry, Button, filedialog, messagebox, ttk
from Gui import get_rom_options_frame, get_rom_frame from Gui import get_rom_options_frame, get_rom_frame
from GuiUtils import set_icon from GuiUtils import set_icon
from argparse import Namespace from argparse import Namespace

View File

@@ -1,18 +1,13 @@
import argparse import argparse
import atexit import atexit
import time import time
import functools
import webbrowser
import multiprocessing import multiprocessing
import socket
import os import os
import subprocess import subprocess
import base64 import base64
import shutil import shutil
from json import loads, dumps from json import loads, dumps
from random import randrange
from Utils import get_item_name_from_id from Utils import get_item_name_from_id
exit_func = atexit.register(input, "Press enter to close.") exit_func = atexit.register(input, "Press enter to close.")

14
Main.py
View File

@@ -171,14 +171,15 @@ def main(args, seed=None):
world.player_names[player].append(name) world.player_names[player].append(name)
logger.info('') logger.info('')
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
for item_name in args.startinventory[player]: for item_name in args.startinventory[player]:
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player) item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
item.game = world.game[player]
world.push_precollected(item) world.push_precollected(item)
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
# enforce pre-defined local items. # enforce pre-defined local items.
@@ -414,7 +415,7 @@ def main(args, seed=None):
return player, team, bytes(rom.name) return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor() pool = concurrent.futures.ThreadPoolExecutor()
multidata_task = None
check_accessibility_task = pool.submit(world.fulfills_accessibility) check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom_futures = [] rom_futures = []
@@ -501,7 +502,10 @@ def main(args, seed=None):
minimum_versions = {"server": (0, 1, 1), "clients": client_versions} minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {} games = {}
for slot in world.player_ids: for slot in world.player_ids:
client_versions[slot] = (0, 0, 3) if world.game[slot] == "Factorio":
client_versions[slot] = (0, 1, 2)
else:
client_versions[slot] = (0, 0, 3)
games[slot] = world.game[slot] games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names} slot, team, rom_name in rom_names}

View File

@@ -1,55 +1,48 @@
import os import os
import sys import sys
import subprocess import subprocess
import importlib import pkg_resources
requirements_files = {'requirements.txt'}
if sys.version_info < (3, 8, 6): if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
update_ran = hasattr(sys, "frozen") and getattr(sys, "frozen") # don't run update if environment is frozen/compiled update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
if not update_ran:
for entry in os.scandir("worlds"):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
def update_command(): def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade']) for file in requirements_files:
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
naming_specialties = {"PyYAML": "yaml", # PyYAML is imported as the name yaml
"maseya-z3pr": "maseya",
"factorio-rcon-py": "factorio_rcon"}
def update(): def update():
global update_ran global update_ran
if not update_ran: if not update_ran:
update_ran = True update_ran = True
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt') for req_file in requirements_files:
if not os.path.exists(path): path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
path = os.path.join(os.path.dirname(__file__), 'requirements.txt') if not os.path.exists(path):
with open(path) as requirementsfile: path = os.path.join(os.path.dirname(__file__), req_file)
for line in requirementsfile.readlines(): with open(path) as requirementsfile:
module, remote_version = line.split(">=") requirements = pkg_resources.parse_requirements(requirementsfile)
module = naming_specialties.get(module, module) for requirement in requirements:
try: requirement = str(requirement)
module = importlib.import_module(module) try:
except: pkg_resources.require(requirement)
import traceback except pkg_resources.ResolutionError:
traceback.print_exc() import traceback
input(f'Required python module {module} not found, press enter to install it') traceback.print_exc()
update_command() input(f'Requirement {requirement} is not satisfied, press enter to install it')
return update_command()
else: return
if hasattr(module, "__version__"):
module_version = module.__version__
module = module.__name__ # also unloads the module to make it writable
if type(module_version) == str:
module_version = tuple(int(part.strip()) for part in module_version.split("."))
remote_version = tuple(int(part.strip()) for part in remote_version.split("."))
if module_version < remote_version:
input(f'Required python module {module} is outdated ({module_version}<{remote_version}),'
' press enter to upgrade it')
update_command()
return
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -45,7 +45,6 @@ if __name__ == "__main__":
zip_multidata = multi_mystery_options["zip_multidata"] zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"] zip_format = multi_mystery_options["zip_format"]
# zip_password = multi_mystery_options["zip_password"] not at this time # zip_password = multi_mystery_options["zip_password"] not at this time
player_name = multi_mystery_options["player_name"]
meta_file_path = multi_mystery_options["meta_file_path"] meta_file_path = multi_mystery_options["meta_file_path"]
weights_file_path = multi_mystery_options["weights_file_path"] weights_file_path = multi_mystery_options["weights_file_path"]
pre_roll = multi_mystery_options["pre_roll"] pre_roll = multi_mystery_options["pre_roll"]
@@ -124,15 +123,6 @@ if __name__ == "__main__":
spoilername = f"AP_{seed_name}_Spoiler.txt" spoilername = f"AP_{seed_name}_Spoiler.txt"
romfilename = "" romfilename = ""
if player_name:
for file in os.listdir(output_path):
if player_name in file:
import MultiClient
import asyncio
asyncio.run(MultiClient.run_game(os.path.join(output_path, file)))
break
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)): if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
import zipfile import zipfile
@@ -167,7 +157,7 @@ if __name__ == "__main__":
def _handle_sfc_file(file: str): def _handle_sfc_file(file: str):
if zip_roms: if zip_roms:
pack_file(file) pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower(): if zip_roms == 2:
remove_zipped_file(file) remove_zipped_file(file)

View File

@@ -564,6 +564,17 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
setattr(ret, option_name, option.from_any(get_choice(option_name, weights))) setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else: else:
setattr(ret, option_name, option(option.default)) setattr(ret, option_name, option(option.default))
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
else: else:
raise Exception(f"Unsupported game {ret.game}") raise Exception(f"Unsupported game {ret.game}")
return ret return ret

View File

@@ -139,6 +139,8 @@ class OptionDict(Option):
else: else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self):
return str(self.value)
class Logic(Choice): class Logic(Choice):
option_no_glitches = 0 option_no_glitches = 0
@@ -309,8 +311,16 @@ class TechTreeLayout(Choice):
option_single = 0 option_single = 0
option_small_diamonds = 1 option_small_diamonds = 1
option_medium_diamonds = 2 option_medium_diamonds = 2
option_pyramid = 3 option_large_diamonds = 3
option_funnel = 4 option_small_pyramids = 4
option_medium_pyramids = 5
option_large_pyramids = 6
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_funnels = 4
alias_pyramid = 6
alias_funnel = 9
default = 0 default = 0
@@ -319,6 +329,12 @@ class Visibility(Choice):
option_sending = 1 option_sending = 1
default = 1 default = 1
class RecipeTime(Choice):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
option_chaos = 5
class FactorioStartItems(OptionDict): class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19} default = {"burner-mining-drill": 19, "stone-furnace": 19}
@@ -330,7 +346,8 @@ factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxScien
"free_samples": FreeSamples, "free_samples": FreeSamples,
"visibility": Visibility, "visibility": Visibility,
"random_tech_ingredients": Toggle, "random_tech_ingredients": Toggle,
"starting_items": FactorioStartItems} "starting_items": FactorioStartItems,
"recipe_time": RecipeTime}
class AdvancementGoal(Choice): class AdvancementGoal(Choice):

View File

@@ -12,7 +12,7 @@ class Version(typing.NamedTuple):
minor: int minor: int
build: int build: int
__version__ = "0.1.1" __version__ = "0.1.2"
_version_tuple = tuplize_version(__version__) _version_tuple = tuplize_version(__version__)
import builtins import builtins
@@ -188,7 +188,7 @@ def get_default_options() -> dict:
"server_password": None, "server_password": None,
"disable_item_cheat": False, "disable_item_cheat": False,
"location_check_points": 1, "location_check_points": 1,
"hint_cost": 1000, "hint_cost": 10,
"forfeit_mode": "goal", "forfeit_mode": "goal",
"remaining_mode": "goal", "remaining_mode": "goal",
"auto_shutdown": 0, "auto_shutdown": 0,
@@ -203,10 +203,10 @@ def get_default_options() -> dict:
"weights_file_path": "weights.yaml", "weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml", "meta_file_path": "meta.yaml",
"pre_roll": False, "pre_roll": False,
"player_name": "",
"create_spoiler": 1, "create_spoiler": 1,
"zip_roms": 0, "zip_roms": 0,
"zip_diffs": 2, "zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0, "zip_spoiler": 0,
"zip_multidata": 1, "zip_multidata": 1,
"zip_format": 1, "zip_format": 1,
@@ -345,12 +345,12 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
f"Enter yes, no or never: ") f"Enter yes, no or never: ")
if adjust_wanted and adjust_wanted.startswith("y"): if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"): if hasattr(adjuster_settings, "sprite_pool"):
from Adjuster import AdjusterWorld from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool")) adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
adjusted = True adjusted = True
import Adjuster import LttPAdjuster
_, romfile = Adjuster.adjust(adjuster_settings) _, romfile = LttPAdjuster.adjust(adjuster_settings)
if hasattr(adjuster_settings, "world"): if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world") delattr(adjuster_settings, "world")

View File

@@ -2,6 +2,10 @@ import os
import multiprocessing import multiprocessing
import logging import logging
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update()
from WebHostLib import app as raw_app from WebHostLib import app as raw_app
from waitress import serve from waitress import serve

View File

@@ -1,4 +1,4 @@
flask>=2.0.0 flask>=2.0.1
pony>=0.7.14 pony>=0.7.14
waitress>=2.0.0 waitress>=2.0.0
flask-caching>=1.10.1 flask-caching>=1.10.1

View File

@@ -1,7 +1,7 @@
# A Link to the Past Randomizer Setup Guide # A Link to the Past Randomizer Setup Guide
## Benötigte Software ## Benötigte Software
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien - Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
- Ein Emulator, der lua-scripts abspielen kann - Ein Emulator, der lua-scripts abspielen kann
@@ -15,7 +15,7 @@
### Windows ### Windows
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die 1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die
aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**. aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**.
Für normale Multiworld-Spiele lädst du die `Setup.BerserkerMultiworld.exe` herunter. Für normale Multiworld-Spiele lädst du die `Setup.Archipelago.exe` herunter.
- Für den Doorrandomizer muss die alternative doors-Variante geladen werden. - Für den Doorrandomizer muss die alternative doors-Variante geladen werden.
- Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software - Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software
bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt. bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Required Software ## Required Software
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware or software capable of loading and playing SNES ROM files - Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of running Lua scripts - An emulator capable of running Lua scripts
@@ -21,7 +21,7 @@
### Windows Setup ### Windows Setup
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version. 1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal **The file is located in the assets section at the bottom of the version information**. If you intend to play normal
multiworld games, you want `Setup.BerserkerMultiWorld.exe` multiworld games, you want `Setup.Archipelago.exe`
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file. - If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have - During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
installed this software before and are simply upgrading now, you will not be prompted to locate your installed this software before and are simply upgrading now, you will not be prompted to locate your
@@ -144,7 +144,7 @@ on successfully joining a multiworld game!
## Hosting a MultiWorld game ## Hosting a MultiWorld game
The recommended way to host a game is to use the hosting service provided on The recommended way to host a game is to use the hosting service provided on
[the website](https://berserkermulti.world/generate). The process is relatively simple: [the website](/generate). The process is relatively simple:
1. Collect YAML files from your players. 1. Collect YAML files from your players.
2. Create a zip file containing your players' YAML files. 2. Create a zip file containing your players' YAML files.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Software requerido ## Software requerido
- [MultiWorld Utilities](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES - Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
- Un emulador capaz de ejecutar scripts Lua - Un emulador capaz de ejecutar scripts Lua
@@ -20,7 +20,7 @@
### Instalación en Windows ### Instalación en Windows
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente. 1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.BerserkerMultiWorld.exe` **El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe' - Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe'
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez. - Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez.
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación. - Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
@@ -136,7 +136,7 @@ por unirte satisfactoriamente a una partida de multiworld!
## Hospedando una partida de multiworld ## Hospedando una partida de multiworld
La manera recomendad para hospedar una partida es usar el servicio proveído en La manera recomendad para hospedar una partida es usar el servicio proveído en
[el sitio web](https://berserkermulti.world/generate). El proceso es relativamente sencillo: [el sitio web](/generate). El proceso es relativamente sencillo:
1. Recolecta los ficheros YAML de todos los jugadores que participen. 1. Recolecta los ficheros YAML de todos los jugadores que participen.
2. Crea un fichero ZIP conteniendo esos ficheros. 2. Crea un fichero ZIP conteniendo esos ficheros.

View File

@@ -7,7 +7,7 @@
</div> </div>
## Logiciels requis ## Logiciels requis
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents) - [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES - Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
- Un émulateur capable d'éxécuter des scripts Lua - Un émulateur capable d'éxécuter des scripts Lua

View File

@@ -2,7 +2,7 @@
## Configuration ## Configuration
1. Plando features have to be enabled first, before they can be used (opt-in). 1. Plando features have to be enabled first, before they can be used (opt-in).
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\BerserkerMultiWorld`), 2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`),
then open the host.yaml file with a text editor. then open the host.yaml file with a text editor.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the 3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
value to value to
@@ -13,7 +13,7 @@
### Bosses ### Bosses
- This module is enabled by default and available to be used on - This module is enabled by default and available to be used on
[https://archipelago.gg/generate](https://archipelago.gg/generate) [https://archipelago.gg/generate](/generate)
- Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted. - Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted.
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end, - Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
it defaults to vanilla it defaults to vanilla

View File

@@ -31,7 +31,7 @@
<p> <p>
After generation is complete, you will have the option to download a patch file. After generation is complete, you will have the option to download a patch file.
This patch file can be opened with the This patch file can be opened with the
<a href="https://github.com/Berserker66/MultiWorld-Utilities/releases">client</a>, which can be <a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
used to to create a rom file. In-browser patching is planned for the future. used to to create a rom file. In-browser patching is planned for the future.
</p> </p>
<div id="generate-game-form-wrapper"> <div id="generate-game-form-wrapper">

View File

@@ -0,0 +1,63 @@
{% extends 'tablepage.html' %}
{% block head %}
{{ super() }}
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/dirtHeader.html' %}
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
<div id="tracker-header-bar">
<input placeholder="Search" id="search"/>
<span class="info">This tracker will automatically update itself periodically.</span>
</div>
<div class="table-wrapper">
<table class="table non-unique-item-table">
<thead>
<tr>
<th>Item</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{% for name, count in inventory.items() %}
<tr>
<td>{{ name | item_name }}</td>
<td>{{ count }}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
<div class="table-wrapper">
<table class="table non-unique-item-table">
<thead>
<tr>
<th>Location</th>
<th>Checked</th>
</tr>
</thead>
<tbody>
{% for name in checked_locations %}
<tr>
<td>{{ name | location_name}}</td>
<td></td>
</tr>
{%- endfor -%}
{% for name in not_checked_locations %}
<tr>
<td>{{ name | location_name}}</td>
<td></td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -2,13 +2,13 @@
<footer id="island-footer"> <footer id="island-footer">
<div id="copyright-notice">Copyright 2021 Archipelago</div> <div id="copyright-notice">Copyright 2021 Archipelago</div>
<div id="links"> <div id="links">
<a href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a> <a href="https://github.com/ArchipelagoMW/Archipelago">Source Code</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/wiki">Wiki</a> <a href="https://github.com/ArchipelagoMW/Archipelago/wiki">Wiki</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">Contributors</a> <a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">Contributors</a>
- -
<a href="https://github.com/Berserker66/MultiWorld-Utilities/issues">Bug Report</a> <a href="https://github.com/ArchipelagoMW/Archipelago/issues">Bug Report</a>
</div> </div>
</footer> </footer>
{% endblock %} {% endblock %}

View File

@@ -43,9 +43,9 @@
trackers are provided for games hosted here.</p> trackers are provided for games hosted here.</p>
<p> <p>
This project is the cumulative effort of many This project is the cumulative effort of many
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">talented people.</a> <a href="https://github.com/ArchipelagoMW/Archipelago/graphs/contributors">talented people.</a>
Together, they have spent countless hours creating a huge repository of Together, they have spent countless hours creating a huge repository of
<a href="https://github.com/Berserker66/MultiWorld-Utilities">source code</a> which has turned <a href="https://github.com/ArchipelagoMW/Archipelago">source code</a> which has turned
our crazy idea into a reality. our crazy idea into a reality.
</p> </p>
<p> <p>

View File

@@ -5,7 +5,7 @@ from werkzeug.exceptions import abort
import datetime import datetime
from uuid import UUID from uuid import UUID
from worlds.alttp import Items, Regions from worlds.alttp import Items
from WebHostLib import app, cache, Room from WebHostLib import app, cache, Room
from Utils import restricted_loads from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
@@ -327,7 +327,8 @@ def get_static_room_data(room: Room):
player_small_key_locations[item_player].add(ids_small_key[item_id]) player_small_key_locations[item_player].add(ids_small_key[item_id])
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
player_big_key_locations, player_small_key_locations, multidata["precollected_items"] player_big_key_locations, player_small_key_locations, multidata["precollected_items"], \
multidata["games"]
_multidata_cache[room.seed.id] = result _multidata_cache[room.seed.id] = result
return result return result
@@ -344,9 +345,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
abort(404) abort(404)
# Collect seed information and pare it down to a single player # Collect seed information and pare it down to a single player
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, player_small_key_locations, precollected_items = get_static_room_data(room) locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
player_big_key_locations, player_small_key_locations, precollected_items, games = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1] player_name = names[tracked_team][tracked_player - 1]
seed_checks_in_area = seed_checks_in_area[tracked_player]
location_to_area = player_location_to_area[tracked_player] location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter() inventory = collections.Counter()
checks_done = {loc_name: 0 for loc_name in default_locations} checks_done = {loc_name: 0 for loc_name in default_locations}
@@ -377,52 +378,58 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
if ms_player == tracked_player: # a check done by the tracked player if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1 checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1 checks_done["Total"] += 1
if games[tracked_player] == "A Link to the Past":
# Note the presence of the triforce item
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
if game_state == 30:
inventory[106] = 1 # Triforce
# Note the presence of the triforce item # Progressive items need special handling for icons and class
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0) progressive_items = {
if game_state == 30: "Progressive Sword": 94,
inventory[106] = 1 # Triforce "Progressive Glove": 97,
"Progressive Bow": 100,
"Progressive Mail": 96,
"Progressive Shield": 95,
}
progressive_names = {
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
"Progressive Bow": [None, "Bow", "Silver Bow"],
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Progressive items need special handling for icons and class # Determine which icon to use
progressive_items = { display_data = {}
"Progressive Sword": 94, for item_name, item_id in progressive_items.items():
"Progressive Glove": 97, level = min(inventory[item_id], len(progressive_names[item_name])-1)
"Progressive Bow": 100, display_name = progressive_names[item_name][level]
"Progressive Mail": 96, acquired = True
"Progressive Shield": 95, if not display_name:
} acquired = False
progressive_names = { display_name = progressive_names[item_name][level+1]
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'], base_name = item_name.split(maxsplit=1)[1].lower()
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'], display_data[base_name+"_acquired"] = acquired
"Progressive Bow": [None, "Bow", "Silver Bow"], display_data[base_name+"_url"] = icons[display_name]
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
}
# Determine which icon to use
display_data = {}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]))
display_name = progressive_names[item_name][level]
acquired = True
if not display_name:
acquired = False
display_name = progressive_names[item_name][level+1]
base_name = item_name.split(maxsplit=1)[1].lower()
display_data[base_name+"_acquired"] = acquired
display_data[base_name+"_url"] = icons[display_name]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should? # The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15] sp_areas = ordered_areas[2:15]
return render_template("playerTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=icons, checks_done=checks_done, player_name=player_name, room=room, icons=icons, checks_done=checks_done,
checks_in_area=seed_checks_in_area, acquired_items={lookup_any_item_id_to_name[id] for id in inventory}, checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas, small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations[tracked_player], key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player], big_key_locations=player_big_key_locations[tracked_player],
**display_data) **display_data)
else:
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
return render_template("genericTracker.html",
inventory=inventory,
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
@app.route('/tracker/<suuid:tracker>') @app.route('/tracker/<suuid:tracker>')
@@ -432,7 +439,7 @@ def getTracker(tracker: UUID):
if not room: if not room:
abort(404) abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \ locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, player_big_key_locations, \
player_small_key_locations, precollected_items = get_static_room_data(room) player_small_key_locations, precollected_items, games = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)} inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)} for teamnumber, team in enumerate(names)}
@@ -447,7 +454,6 @@ def getTracker(tracker: UUID):
else: else:
multisave = {} multisave = {}
if "hints" in multisave: if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items(): for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints) hints[team] |= set(slot_hints)
@@ -486,7 +492,7 @@ def getTracker(tracker: UUID):
for player, name in enumerate(names, 1): for player, name in enumerate(names, 1):
player_names[(team, player)] = name player_names[(team, player)] = name
long_player_names = player_names.copy() long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", []): for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[(team, player)] = alias player_names[(team, player)] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

View File

@@ -0,0 +1 @@
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,10 +1,5 @@
{% macro dict_to_lua(dict) -%} {% from "macros.lua" import dict_to_lua %}
{ -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
{% for key, value in dict.items() %}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor %}
}
{%- endmacro %}
require "lib" require "lib"
require "util" require "util"
@@ -138,16 +133,18 @@ script.on_init(function()
end) end)
-- for testing -- for testing
script.on_event(defines.events.on_tick, function(event) -- script.on_event(defines.events.on_tick, function(event)
if event.tick%600 == 300 then -- if event.tick%3600 == 300 then
dumpInfo(game.forces["player"]) -- dumpInfo(game.forces["player"])
end -- end
end) -- end)
-- hook into researches done -- hook into researches done
script.on_event(defines.events.on_research_finished, function(event) script.on_event(defines.events.on_research_finished, function(event)
local technology = event.research local technology = event.research
dumpInfo(technology.force) if technology.researched and string.find(technology.name, "ap%-") == 1 then
dumpInfo(technology.force) --is sendable
end
if FREE_SAMPLES == 0 then if FREE_SAMPLES == 0 then
return -- Nothing else to do return -- Nothing else to do
end end
@@ -191,6 +188,7 @@ function dumpInfo(force)
end end
end end
game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0) game.write_file("ap_bridge.json", game.table_to_json(data_collection), false, 0)
log("Archipelago Bridge File written for game tick ".. game.tick .. ".")
-- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0) -- game.write_file("research_done.json", game.table_to_json(data_collection), false, 0)
-- game.print("Sent progress to Archipelago.") -- game.print("Sent progress to Archipelago.")
end end

View File

@@ -1,7 +1,8 @@
{% from "macros.lua" import dict_to_recipe %}
-- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template -- this file gets written automatically by the Archipelago Randomizer and is in its raw form a Jinja2 Template
require('lib') require('lib')
data.raw["recipe"]["rocket-part"].ingredients = {{ rocket_recipe | safe }} data.raw["recipe"]["rocket-part"].ingredients = {{ dict_to_recipe(rocket_recipe) }}
local technologies = data.raw["technology"] local technologies = data.raw["technology"]
local original_tech local original_tech
@@ -30,31 +31,51 @@ function prep_copy(new_copy, old_tech)
end end
end end
function set_ap_icon(tech)
tech.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
tech.icons = nil
tech.icon_size = 128
end
function set_ap_unimportant_icon(tech)
tech.icon = "__{{ mod_name }}__/graphics/icons/ap_unimportant.png"
tech.icons = nil
tech.icon_size = 128
end
function copy_factorio_icon(tech, tech_source)
tech.icon = table.deepcopy(technologies[tech_source].icon)
tech.icons = table.deepcopy(technologies[tech_source].icons)
tech.icon_size = table.deepcopy(technologies[tech_source].icon_size)
end
function adjust_energy(recipe_name, factor)
local energy = data.raw.recipe[recipe_name].energy_required
if (energy == nil) then
energy = 1
end
data.raw.recipe[recipe_name].energy_required = energy * factor
end
table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid") table.insert(data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories, "crafting-with-fluid")
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #} {# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
{%- for original_tech_name, item_name, receiving_player in locations %} {%- for original_tech_name, item_name, receiving_player, advancement in locations %}
original_tech = technologies["{{original_tech_name}}"] original_tech = technologies["{{original_tech_name}}"]
{#- the tech researched by the local player #} {#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech) new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #} new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
prep_copy(new_tree_copy, original_tech) prep_copy(new_tree_copy, original_tech)
{% if tech_cost != 1 %} {% if tech_cost != 1 %}
if new_tree_copy.unit.count then new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
end
{% endif %}
{% if item_name in tech_table and visibility %}
{#- copy Factorio Technology Icon #}
new_tree_copy.icon = table.deepcopy(technologies["{{ item_name }}"].icon)
new_tree_copy.icons = table.deepcopy(technologies["{{ item_name }}"].icons)
new_tree_copy.icon_size = table.deepcopy(technologies["{{ item_name }}"].icon_size)
{% else %}
{#- use default AP icon if no Factorio graphics exist #}
new_tree_copy.icon = "__{{ mod_name }}__/graphics/icons/ap.png"
new_tree_copy.icons = nil
new_tree_copy.icon_size = 512
{% endif %} {% endif %}
{%- if item_name in tech_table and visibility -%}
{#- copy Factorio Technology Icon -#}
copy_factorio_icon(new_tree_copy, "{{ item_name }}")
{%- else -%}
{#- use default AP icon if no Factorio graphics exist -#}
{% if advancement %}set_ap_icon(new_tree_copy){% else %}set_ap_unimportant_icon(new_tree_copy){% endif %}
{%- endif -%}
{#- connect Technology #} {#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %} {%- if original_tech_name in tech_tree_layout_prerequisites %}
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %} {%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
@@ -63,5 +84,9 @@ table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
{% endif -%} {% endif -%}
{#- add new Technology to game #} {#- add new Technology to game #}
data:extend{new_tree_copy} data:extend{new_tree_copy}
{% endfor %}
{% endfor %} {% if recipe_time_scale %}
{%- for recipe in recipes %}
adjust_energy("{{ recipe }}", {{ random.triangular(*recipe_time_scale) }})
{%- endfor -%}
{% endif %}

View File

@@ -1,6 +1,6 @@
[technology-name] [technology-name]
{% for original_tech_name, item_name, receiving_player in locations %} {% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if visibility -%} {%- if visibility -%}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }} ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
{% else %} {% else %}
@@ -9,9 +9,9 @@ ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
{% endfor %} {% endfor %}
[technology-description] [technology-description]
{% for original_tech_name, item_name, receiving_player in locations %} {% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if visibility -%} {%- if visibility -%}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends {{ item_name }} to {{ player_names[receiving_player] }}. 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 %}.
{% else %} {% else %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
{%- endif -%} {%- endif -%}

View File

@@ -0,0 +1,14 @@
{% macro dict_to_lua(dict) -%}
{
{%- for key, value in dict.items() -%}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}
{% macro dict_to_recipe(dict) -%}
{
{%- for key, value in dict.items() -%}
{"{{ key }}", {{ value | safe }}}{% if not loop.last %},{% endif %}
{% endfor -%}
}
{%- endmacro %}

BIN
data/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,80 @@
#define sourcepath "build_factorio\exe.win-amd64-3.8\"
#define MyAppName "Archipelago Factorio Client"
#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe"
#define MyAppIcon "icon.ico"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}}
AppName={#MyAppName}
AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes
DefaultGroupName=Archipelago
OutputDir=setups
OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2
SolidCompression=yes
LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[Code]
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
strVersion: string;
begin
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
begin
// Not even an old version installed
Log('VC Redist x64 is not already installed');
Result := True;
end;
end;

View File

@@ -21,7 +21,7 @@ server_options:
location_check_points: 1 location_check_points: 1
# Relative point cost to receive a hint via !hint for players # Relative point cost to receive a hint via !hint for players
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5 # so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 1000 # Set to 0 if you want free hints hint_cost: 10 # Set to 0 if you want free hints
# Forfeit modes # Forfeit modes
# "disabled" -> clients can't forfeit, # "disabled" -> clients can't forfeit,
# "enabled" -> clients can always forfeit # "enabled" -> clients can always forfeit
@@ -63,10 +63,6 @@ multi_mystery_options:
# If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml # If using a pre-rolled yaml fails with "Please fix your yaml.", please file a bug report including both the original yaml
# as well as the generated pre-rolled yaml. # as well as the generated pre-rolled yaml.
pre_roll: false pre_roll: false
# Automatically launches {player_name}.yaml's ROM file using the OS's default program once generation completes. (likely your emulator)
# Does nothing if the name is not found
# Example: player_name = "Berserker"
player_name: "" # The hosts name
# Create a spoiler file # Create a spoiler file
# 0 -> None # 0 -> None
# 1 -> Full spoiler # 1 -> Full spoiler

View File

@@ -82,7 +82,7 @@ begin
begin begin
// Is the installed version at least the packaged one ? // Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion); Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.28.29325') < 0); Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end end
else else
begin begin

View File

@@ -82,7 +82,7 @@ begin
begin begin
// Is the installed version at least the packaged one ? // Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion); Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.28.29325') < 0); Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end end
else else
begin begin

View File

@@ -25,7 +25,6 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1. #{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: game:
A Link to the Past: 1 A Link to the Past: 1
Hollow Knight: 1
Factorio: 1 Factorio: 1
Minecraft: 1 Minecraft: 1
# Shared Options supported by all games: # Shared Options supported by all games:
@@ -49,8 +48,19 @@ tech_tree_layout:
single: 1 single: 1
small_diamonds: 1 small_diamonds: 1
medium_diamonds: 1 medium_diamonds: 1
pyramid: 1 large_diamonds: 1
funnel: 1 small_pyramids: 1
medium_pyramids: 1
large_pyramids: 1
small_funnels: 1
medium_funnels: 1
large_funnels: 1
recipe_time: # randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
vanilla: 1
fast: 0 # 25% to 100% of original time
normal: 0 # 50 % to 200% of original time
slow: 0 # 100% to 400% of original time
chaos: 0 # 25% to 400% of original time
max_science_pack: max_science_pack:
automation_science_pack: 0 automation_science_pack: 0
logistic_science_pack: 0 logistic_science_pack: 0

View File

@@ -1,11 +1,7 @@
colorama>=0.4.4 colorama>=0.4.4
websockets>=9.0.2 websockets>=9.1
PyYAML>=5.4.1 PyYAML>=5.4.1
fuzzywuzzy>=0.18.0 fuzzywuzzy>=0.18.0
bsdiff4>=1.2.1
prompt_toolkit>=3.0.18 prompt_toolkit>=3.0.18
appdirs>=1.4.4 appdirs>=1.4.4
maseya-z3pr>=1.0.0rc1 jinja2>=3.0.1
xxtea>=2.0.0.post0
factorio-rcon-py>=1.2.1
jinja2>=3.0.0

View File

@@ -38,15 +38,15 @@ def _threaded_hash(filepath):
os.makedirs(buildfolder, exist_ok=True) os.makedirs(buildfolder, exist_ok=True)
def manifest_creation(): def manifest_creation(folder):
hashes = {} hashes = {}
manifestpath = os.path.join(buildfolder, "manifest.json") manifestpath = os.path.join(folder, "manifest.json")
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor() pool = ThreadPoolExecutor()
for dirpath, dirnames, filenames in os.walk(buildfolder): for dirpath, dirnames, filenames in os.walk(folder):
for filename in filenames: for filename in filenames:
path = os.path.join(dirpath, filename) path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=buildfolder)] = pool.submit(_threaded_hash, path) hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
import json import json
from Utils import _version_tuple from Utils import _version_tuple
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
@@ -161,4 +161,79 @@ for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore": if file != ".gitignore":
os.remove(alttpr_sprites_folder / file) os.remove(alttpr_sprites_folder / file)
manifest_creation() manifest_creation(buildfolder)
buildfolder = Path("build_factorio", folder)
sbuildfolder = str(buildfolder)
libfolder = Path(buildfolder, "lib")
library = Path(libfolder, "library.zip")
print("Outputting Factorio Client to: " + sbuildfolder)
os.makedirs(buildfolder, exist_ok=True)
scripts = {"FactorioClient.py": "ArchipelagoConsoleFactorioClient"}
exes = []
for script, scriptname in scripts.items():
exes.append(cx_Freeze.Executable(
script=script,
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
))
exes.append(cx_Freeze.Executable(
script="FactorioClientGUI.py",
target_name="ArchipelagoGraphicalFactorioClient" + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
base="Win32GUI"
))
import datetime
buildtime = datetime.datetime.utcnow()
cx_Freeze.setup(
name="Archipelago Factorio Client",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
description="Archipelago Factorio Client",
executables=exes,
options={
"build_exe": {
"packages": ["websockets", "kivy"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["kivy"],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],
"optimize": 2,
"build_exe": buildfolder
},
},
)
extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"]
from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins+glew.dep_bins:
shutil.copytree(folder, buildfolder, dirs_exist_ok=True)
for data in extra_data:
installfile(Path(data))
os.makedirs(buildfolder / "Players", exist_ok=True)
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
if signtool:
for exe in exes:
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(buildfolder, exe.target_name))
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore":
os.remove(alttpr_sprites_folder / file)
manifest_creation(buildfolder)

View File

@@ -201,14 +201,9 @@ class TestAdvancements(TestMinecraft):
["Hot Tourist Destinations", False, [], ['Ingot Crafting']], ["Hot Tourist Destinations", False, [], ['Ingot Crafting']],
["Hot Tourist Destinations", False, [], ['Flint and Steel']], ["Hot Tourist Destinations", False, [], ['Flint and Steel']],
["Hot Tourist Destinations", False, [], ['Progressive Tools']], ["Hot Tourist Destinations", False, [], ['Progressive Tools']],
["Hot Tourist Destinations", False, [], ['Progressive Weapons']],
["Hot Tourist Destinations", False, [], ['Progressive Armor', 'Shield']],
["Hot Tourist Destinations", False, [], ['Fishing Rod']],
["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ["Hot Tourist Destinations", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Bucket', 'Fishing Rod']], ["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']], ["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Bucket', 'Fishing Rod']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Fishing Rod']],
]) ])
def test_42015(self): def test_42015(self):
@@ -979,7 +974,8 @@ class TestAdvancements(TestMinecraft):
["Sticky Situation", False, []], ["Sticky Situation", False, []],
["Sticky Situation", False, [], ['Bottles']], ["Sticky Situation", False, [], ['Bottles']],
["Sticky Situation", False, [], ['Ingot Crafting']], ["Sticky Situation", False, [], ['Ingot Crafting']],
["Sticky Situation", True, ['Bottles', 'Ingot Crafting']], ["Sticky Situation", False, [], ['Campfire']],
["Sticky Situation", True, ['Bottles', 'Ingot Crafting', 'Campfire']],
]) ])
def test_42075(self): def test_42075(self):
@@ -1099,16 +1095,17 @@ class TestAdvancements(TestMinecraft):
self.run_location_tests([ self.run_location_tests([
["When Pigs Fly", False, []], ["When Pigs Fly", False, []],
["When Pigs Fly", False, [], ['Ingot Crafting']], ["When Pigs Fly", False, [], ['Ingot Crafting']],
["When Pigs Fly", False, [], ['Flint and Steel']],
["When Pigs Fly", False, [], ['Progressive Tools']], ["When Pigs Fly", False, [], ['Progressive Tools']],
["When Pigs Fly", False, [], ['Progressive Weapons']], ["When Pigs Fly", False, [], ['Progressive Weapons']],
["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']], ["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']],
["When Pigs Fly", False, [], ['Fishing Rod']], ["When Pigs Fly", False, [], ['Fishing Rod']],
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ["When Pigs Fly", False, ['Progressive Weapons'], ['Flint and Steel', 'Progressive Weapons', 'Progressive Weapons']],
["When Pigs Fly", False, ['Progressive Tools', 'Progressive Tools', 'Progressive Weapons'], ['Bucket', 'Progressive Tools', 'Progressive Weapons', 'Progressive Weapons']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Fishing Rod']], ["When Pigs Fly", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Shield', 'Fishing Rod']],
["When Pigs Fly", True, ['Progressive Weapons', 'Progressive Weapons', 'Progressive Armor', 'Shield', 'Ingot Crafting', 'Progressive Tools', 'Fishing Rod']],
]) ])
def test_42089(self): def test_42089(self):

View File

@@ -241,25 +241,13 @@ def generate_itempool(world, player: int):
else: else:
world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False) world.push_item(world.get_location('Ganon', player), ItemFactory('Triforce', player), False)
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']:
region = world.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
loc.locked = True
if world.goal[player] == 'icerodhunt': if world.goal[player] == 'icerodhunt':
world.progression_balancing[player] = False world.progression_balancing[player] = False
loc = world.get_location('Turtle Rock - Boss', player) loc = world.get_location('Turtle Rock - Boss', player)
world.push_item(loc, ItemFactory('Triforce', player), False) world.push_item(loc, ItemFactory('Triforce Piece', player), False)
world.treasure_hunt_count[player] = 1
if world.boss_shuffle[player] != 'none': if world.boss_shuffle[player] != 'none':
if 'turtle rock-' not in world.boss_shuffle[player]: if 'turtle rock-' not in world.boss_shuffle[player]:
world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}' world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}'
@@ -295,6 +283,19 @@ def generate_itempool(world, player: int):
for item in itempool: for item in itempool:
world.push_precollected(ItemFactory(item, player)) world.push_precollected(ItemFactory(item, player))
if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
region = world.get_region('Light World', player)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory('Triforce', player), False)
loc.event = True
loc.locked = True
world.get_location('Ganon', player).event = True world.get_location('Ganon', player).event = True
world.get_location('Ganon', player).locked = True world.get_location('Ganon', player).locked = True

View File

@@ -2337,9 +2337,13 @@ def write_strings(rom, world, player, team):
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \ tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
f'have beaten Agahnim atop Ganons Tower' f'have beaten Agahnim atop Ganons Tower'
elif world.goal[player] == "icerodhunt": elif world.goal[player] == "icerodhunt":
tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx... Ganon is invincible!' tt['sign_ganon'] = 'Go find the Ice Rod and Kill Trinexx, then talk to Murahdahla... Ganon is invincible!'
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Go kill Trinexx instead.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. " \
"If you bring me the Triforce piece from Turtle Rock, I can reassemble it."
else: else:
if world.crystals_needed_for_ganon[player] == 1: if world.crystals_needed_for_ganon[player] == 1:
tt['sign_ganon'] = 'You need a crystal to beat Ganon.' tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
@@ -2354,7 +2358,7 @@ def write_strings(rom, world, player, team):
tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)] tt['sahasrahla_quest_have_master_sword'] = Sahasrahla2_texts[local_random.randint(0, len(Sahasrahla2_texts) - 1)]
tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)] tt['blind_by_the_light'] = Blind_texts[local_random.randint(0, len(Blind_texts) - 1)]
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: if world.goal[player] in ['triforcehunt', 'localtriforcehunt', 'icerodhunt']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.' tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
if world.goal[player] == 'triforcehunt' and world.players > 1: if world.goal[player] == 'triforcehunt' and world.players > 1:
@@ -2364,12 +2368,12 @@ def write_strings(rom, world, player, team):
if world.treasure_hunt_count[player] > 1: if world.treasure_hunt_count[player] > 1:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d triforce pieces out of %d, I can reassemble it." % \ "hidden in a hollow tree. If you bring\n%d Triforce pieces out of %d, I can reassemble it." % \
(world.treasure_hunt_count[player], world.triforce_pieces_available[player]) (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
else: else:
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \ tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\n" \
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \ "invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
"hidden in a hollow tree. If you bring\n%d triforce piece out of %d, I can reassemble it." % \ "hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
(world.treasure_hunt_count[player], world.triforce_pieces_available[player]) (world.treasure_hunt_count[player], world.triforce_pieces_available[player])
elif world.goal[player] in ['pedestal']: elif world.goal[player] in ['pedestal']:
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.' tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'

View File

@@ -0,0 +1,3 @@
bsdiff4>=1.2.1
maseya-z3pr>=1.0.0rc1
xxtea>=2.0.0.post0

View File

@@ -11,7 +11,9 @@ import Utils
import shutil import shutil
import Options import Options
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from .Technologies import tech_table from .Technologies import tech_table, rocket_recipes, recipes
template_env: Optional[jinja2.Environment] = None
template: Optional[jinja2.Template] = None template: Optional[jinja2.Template] = None
locale_template: Optional[jinja2.Template] = None locale_template: Optional[jinja2.Template] = None
@@ -28,37 +30,35 @@ base_info = {
"factorio_version": "1.1" "factorio_version": "1.1"
} }
# TODO: clean this up, probably as a jinja macro; then add logic for the recipes in completion condition recipe_time_scales = {
rocket_recipes = { # using random.triangular
Options.MaxSciencePack.option_space_science_pack: Options.RecipeTime.option_fast: (0.25, 1),
'{{"rocket-control-unit", 10}, {"low-density-structure", 10}, {"rocket-fuel", 10}}', # 0.5, 2, 0.5 average -> 1.0
Options.MaxSciencePack.option_utility_science_pack: Options.RecipeTime.option_normal: (0.5, 2, 0.5),
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}', Options.RecipeTime.option_slow: (1, 4),
Options.MaxSciencePack.option_production_science_pack: # 0.25, 4, 0.25 average -> 1.5
'{{"speed-module", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}', Options.RecipeTime.option_chaos: (0.25, 4, 0.25),
Options.MaxSciencePack.option_chemical_science_pack: Options.RecipeTime.option_vanilla: None
'{{"advanced-circuit", 10}, {"steel-plate", 10}, {"solid-fuel", 10}}',
Options.MaxSciencePack.option_military_science_pack:
'{{"defender-capsule", 10}, {"stone-wall", 10}, {"coal", 10}}',
Options.MaxSciencePack.option_logistic_science_pack:
'{{"electronic-circuit", 10}, {"stone-brick", 10}, {"coal", 10}}',
Options.MaxSciencePack.option_automation_science_pack:
'{{"copper-cable", 10}, {"iron-plate", 10}, {"wood", 10}}'
} }
def generate_mod(world: MultiWorld, player: int): def generate_mod(world: MultiWorld, player: int):
global template, locale_template, control_template global template, locale_template, control_template
with template_load_lock: with template_load_lock:
if not template: if not template:
mod_template_folder = Utils.local_path("data", "factorio", "mod_template") mod_template_folder = Utils.local_path("data", "factorio", "mod_template")
template = jinja2.Template(open(os.path.join(mod_template_folder, "data-final-fixes.lua")).read()) template_env: Optional[jinja2.Environment] = \
locale_template = jinja2.Template(open(os.path.join(mod_template_folder, "locale", "en", "locale.cfg")).read()) jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
control_template = jinja2.Template(open(os.path.join(mod_template_folder, "control.lua")).read())
template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua")
# get data for templates # get data for templates
player_names = {x: world.player_names[x][0] for x in world.player_ids} player_names = {x: world.player_names[x][0] for x in world.player_ids}
locations = [] locations = []
for location in world.get_filled_locations(player): for location in world.get_filled_locations(player):
locations.append((location.name, location.item.name, location.item.player)) if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}" mod_name = f"AP-{world.seed_name}-P{player}-{world.player_names[player][0]}"
tech_cost = {0: 0.1, tech_cost = {0: 0.1,
1: 0.25, 1: 0.25,
@@ -67,13 +67,15 @@ def generate_mod(world: MultiWorld, player: int):
4: 2, 4: 2,
5: 5, 5: 5,
6: 10}[world.tech_cost[player].value] 6: 10}[world.tech_cost[player].value]
template_data = {"locations": locations, "player_names" : player_names, "tech_table": tech_table, template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
"mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(), "mod_name": mod_name, "allowed_science_packs": world.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost, "custom_data": world.custom_data[player], "tech_cost_scale": tech_cost, "custom_data": world.custom_data[player],
"tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player], "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites[player],
"rocket_recipe" : rocket_recipes[world.max_science_pack[player].value], "rocket_recipe": rocket_recipes[world.max_science_pack[player].value],
"slot_name": world.player_names[player][0], "seed_name": world.seed_name, "slot_name": world.player_names[player][0], "seed_name": world.seed_name,
"starting_items": world.starting_items[player]} "starting_items": world.starting_items[player], "recipes": recipes,
"random": world.random,
"recipe_time_scale": recipe_time_scales[world.recipe_time[player].value]}
for factorio_option in Options.factorio_options: for factorio_option in Options.factorio_options:
template_data[factorio_option] = getattr(world, factorio_option)[player].value template_data[factorio_option] = getattr(world, factorio_option)[player].value
@@ -81,7 +83,7 @@ def generate_mod(world: MultiWorld, player: int):
control_code = control_template.render(**template_data) control_code = control_template.render(**template_data)
data_final_fixes_code = template.render(**template_data) data_final_fixes_code = template.render(**template_data)
mod_dir = Utils.output_path(mod_name)+"_"+Utils.__version__ mod_dir = Utils.output_path(mod_name) + "_" + Utils.__version__
en_locale_dir = os.path.join(mod_dir, "locale", "en") en_locale_dir = os.path.join(mod_dir, "locale", "en")
os.makedirs(en_locale_dir, exist_ok=True) os.makedirs(en_locale_dir, exist_ok=True)
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True) shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True)
@@ -98,12 +100,11 @@ def generate_mod(world: MultiWorld, player: int):
json.dump(info, f, indent=4) json.dump(info, f, indent=4)
# zip the result # zip the result
zf_path = os.path.join(mod_dir+".zip") zf_path = os.path.join(mod_dir + ".zip")
with zipfile.ZipFile(zf_path, compression=zipfile.ZIP_DEFLATED, mode='w') as zf: with zipfile.ZipFile(zf_path, compression=zipfile.ZIP_DEFLATED, mode='w') as zf:
for root, dirs, files in os.walk(mod_dir): for root, dirs, files in os.walk(mod_dir):
for file in files: for file in files:
zf.write(os.path.join(root, file), zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file), os.path.relpath(os.path.join(root, file),
os.path.join(mod_dir, '..'))) os.path.join(mod_dir, '..')))
shutil.rmtree(mod_dir) shutil.rmtree(mod_dir)

View File

@@ -2,21 +2,29 @@ from typing import Dict, List, Set
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from Options import TechTreeLayout from Options import TechTreeLayout
from worlds.factorio.Technologies import technology_table
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
TechTreeLayout.option_medium_funnels: 4,
TechTreeLayout.option_large_funnels: 5}
funnel_slice_sizes = {TechTreeLayout.option_small_funnels: 6,
TechTreeLayout.option_medium_funnels: 10,
TechTreeLayout.option_large_funnels: 15}
def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]: def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
prerequisites: Dict[str, Set[str]] = {} prerequisites: Dict[str, Set[str]] = {}
layout = world.tech_tree_layout[player].value layout = world.tech_tree_layout[player].value
custom_technologies = world.custom_data[player]["custom_technologies"] custom_technologies = world.custom_data[player]["custom_technologies"]
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
if layout == TechTreeLayout.option_small_diamonds: if layout == TechTreeLayout.option_small_diamonds:
slice_size = 4 slice_size = 4
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
while len(tech_names) > slice_size: while len(tech_names) > slice_size:
slice = tech_names[:slice_size] slice = tech_names[:slice_size]
tech_names = tech_names[slice_size:] tech_names = tech_names[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients)) slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
diamond_0, diamond_1, diamond_2, diamond_3 = slice diamond_0, diamond_1, diamond_2, diamond_3 = slice
# 0 | # 0 |
@@ -24,15 +32,13 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
# 3 V # 3 V
prerequisites[diamond_3] = {diamond_1, diamond_2} prerequisites[diamond_3] = {diamond_1, diamond_2}
prerequisites[diamond_2] = prerequisites[diamond_1] = {diamond_0} prerequisites[diamond_2] = prerequisites[diamond_1] = {diamond_0}
elif layout == TechTreeLayout.option_medium_diamonds: elif layout == TechTreeLayout.option_medium_diamonds:
slice_size = 9 slice_size = 9
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
while len(tech_names) > slice_size: while len(tech_names) > slice_size:
slice = tech_names[:slice_size] slice = tech_names[:slice_size]
tech_names = tech_names[slice_size:] tech_names = tech_names[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients)) slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
# 0 | # 0 |
# 1 2 | # 1 2 |
@@ -52,44 +58,136 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
prerequisites[slice[8]] = {slice[6], slice[7]} prerequisites[slice[8]] = {slice[6], slice[7]}
elif layout == TechTreeLayout.option_pyramid: elif layout == TechTreeLayout.option_large_diamonds:
slice_size = 1 slice_size = 16
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes)
tech_names.sort()
world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
previous_slice = []
while len(tech_names) > slice_size: while len(tech_names) > slice_size:
slice = tech_names[:slice_size] slice = tech_names[:slice_size]
world.random.shuffle(slice)
tech_names = tech_names[slice_size:] tech_names = tech_names[slice_size:]
for i, tech_name in enumerate(previous_slice): slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
prerequisites.setdefault(slice[i], set()).add(tech_name)
prerequisites.setdefault(slice[i + 1], set()).add(tech_name)
previous_slice = slice
slice_size += 1
elif layout == TechTreeLayout.option_funnel: # 0 |
# 1 2 |
# 3 4 5 |
# 6 7 8 9 |
# 10 11 12 |
# 13 14 |
# 15 |
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
tech_names: List[str] = list(set(custom_technologies) - world._static_nodes) prerequisites[slice[3]] = {slice[1]}
# find largest inverse pyramid prerequisites[slice[4]] = {slice[1], slice[2]}
# https://www.wolframalpha.com/input/?i=x+=+1/2+(n++++1)+(2++++n)+solve+for+n prerequisites[slice[5]] = {slice[2]}
import math
slice_size = int(0.5*(math.sqrt(8*len(tech_names)+1)-3)) prerequisites[slice[6]] = {slice[3]}
tech_names.sort() prerequisites[slice[7]] = {slice[3], slice[4]}
world.random.shuffle(tech_names) prerequisites[slice[8]] = {slice[4], slice[5]}
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients)) prerequisites[slice[9]] = {slice[5]}
previous_slice = []
while slice_size: prerequisites[slice[10]] = {slice[6], slice[7]}
prerequisites[slice[11]] = {slice[7], slice[8]}
prerequisites[slice[12]] = {slice[8], slice[9]}
prerequisites[slice[13]] = {slice[10], slice[11]}
prerequisites[slice[14]] = {slice[11], slice[12]}
prerequisites[slice[15]] = {slice[13], slice[14]}
elif layout == TechTreeLayout.option_small_pyramids:
slice_size = 6
while len(tech_names) > slice_size:
slice = tech_names[:slice_size] slice = tech_names[:slice_size]
world.random.shuffle(slice)
tech_names = tech_names[slice_size:] tech_names = tech_names[slice_size:]
if previous_slice: slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
for i, tech_name in enumerate(slice):
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2]) # 0 |
previous_slice = slice # 1 2 |
slice_size -= 1 # 3 4 5 |
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
prerequisites[slice[3]] = {slice[1]}
prerequisites[slice[4]] = {slice[1], slice[2]}
prerequisites[slice[5]] = {slice[2]}
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()))
# 0 |
# 1 2 |
# 3 4 5 |
# 6 7 8 9 |
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
prerequisites[slice[3]] = {slice[1]}
prerequisites[slice[4]] = {slice[1], slice[2]}
prerequisites[slice[5]] = {slice[2]}
prerequisites[slice[6]] = {slice[3]}
prerequisites[slice[7]] = {slice[3], slice[4]}
prerequisites[slice[8]] = {slice[4], slice[5]}
prerequisites[slice[9]] = {slice[5]}
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()))
# 0 |
# 1 2 |
# 3 4 5 |
# 6 7 8 9 |
# 10 11 12 13 14 |
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
prerequisites[slice[3]] = {slice[1]}
prerequisites[slice[4]] = {slice[1], slice[2]}
prerequisites[slice[5]] = {slice[2]}
prerequisites[slice[6]] = {slice[3]}
prerequisites[slice[7]] = {slice[3], slice[4]}
prerequisites[slice[8]] = {slice[4], slice[5]}
prerequisites[slice[9]] = {slice[5]}
prerequisites[slice[10]] = {slice[6]}
prerequisites[slice[11]] = {slice[6], slice[7]}
prerequisites[slice[12]] = {slice[7], slice[8]}
prerequisites[slice[13]] = {slice[8], slice[9]}
prerequisites[slice[14]] = {slice[9]}
elif layout in funnel_layers:
slice_size = funnel_slice_sizes[layout]
world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
while len(tech_names) > slice_size:
tech_names = tech_names[slice_size:]
current_tech_names = tech_names[:slice_size]
layer_size = funnel_layers[layout]
previous_slice = []
for layer in range(funnel_layers[layout]):
slice = current_tech_names[:layer_size]
current_tech_names = current_tech_names[layer_size:]
if previous_slice:
for i, tech_name in enumerate(slice):
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
previous_slice = slice
layer_size -= 1
world.tech_tree_layout_prerequisites[player] = prerequisites world.tech_tree_layout_prerequisites[player] = prerequisites
return prerequisites return prerequisites

View File

@@ -1,24 +1,40 @@
from __future__ import annotations from __future__ import annotations
# Factorio technologies are imported from a .json document in /data # Factorio technologies are imported from a .json document in /data
from typing import Dict, Set, FrozenSet from typing import Dict, Set, FrozenSet
import os
import json import json
import Options
import Utils import Utils
import logging import logging
import functools
factorio_id = 2 ** 17 factorio_id = 2 ** 17
source_file = Utils.local_path("data", "factorio", "techs.json") source_folder = Utils.local_path("data", "factorio")
recipe_source_file = Utils.local_path("data", "factorio", "recipes.json")
with open(source_file) as f: with open(os.path.join(source_folder, "techs.json")) as f:
raw = json.load(f) raw = json.load(f)
with open(recipe_source_file) as f: with open(os.path.join(source_folder, "recipes.json")) as f:
raw_recipes = json.load(f) raw_recipes = json.load(f)
with open(os.path.join(source_folder, "machines.json")) as f:
raw_machines = json.load(f)
tech_table: Dict[str, int] = {} tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {} technology_table: Dict[str, Technology] = {}
always = lambda state: True always = lambda state: True
class Technology(): # maybe make subclass of Location? class FactorioElement():
name: str
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
def __hash__(self):
return hash(self.name)
class Technology(FactorioElement): # maybe make subclass of Location?
def __init__(self, name, ingredients, factorio_id): def __init__(self, name, ingredients, factorio_id):
self.name = name self.name = name
self.factorio_id = factorio_id self.factorio_id = factorio_id
@@ -26,35 +42,20 @@ class Technology(): # maybe make subclass of Location?
def build_rule(self, player: int): def build_rule(self, player: int):
logging.debug(f"Building rules for {self.name}") logging.debug(f"Building rules for {self.name}")
ingredient_rules = []
for ingredient in self.ingredients:
logging.debug(f"Building rules for ingredient {ingredient}")
technologies = required_technologies[ingredient] # technologies that unlock the recipes
if technologies:
logging.debug(f"Required Technologies: {technologies}")
ingredient_rules.append(
lambda state, technologies=technologies: all(state.has(technology.name, player)
for technology in technologies))
if ingredient_rules:
ingredient_rules = frozenset(ingredient_rules)
return lambda state: all(rule(state) for rule in ingredient_rules)
return always return lambda state, technologies=technologies: all(state.has(f"Automated {ingredient}", player)
for ingredient in self.ingredients)
def get_prior_technologies(self, allowed_packs) -> Set[Technology]: def get_prior_technologies(self) -> Set[Technology]:
"""Get Technologies that have to precede this one to resolve tree connections.""" """Get Technologies that have to precede this one to resolve tree connections."""
technologies = set() technologies = set()
for ingredient in self.ingredients: for ingredient in self.ingredients:
if ingredient in allowed_packs: technologies |= required_technologies[ingredient] # technologies that unlock the recipes
technologies |= required_technologies[ingredient] # technologies that unlock the recipes
return technologies return technologies
def __hash__(self): def __hash__(self):
return self.factorio_id return self.factorio_id
def __repr__(self):
return f"{self.__class__.__name__}({self.name})"
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology: def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
return CustomTechnology(self, world, allowed_packs, player) return CustomTechnology(self, world, allowed_packs, player)
@@ -67,13 +68,12 @@ class CustomTechnology(Technology):
self.player = player self.player = player
if world.random_tech_ingredients[player]: if world.random_tech_ingredients[player]:
ingredients = list(ingredients) ingredients = list(ingredients)
ingredients.sort() # deterministic sample ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients))) ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id) super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
class Recipe(FactorioElement):
class Recipe():
def __init__(self, name, category, ingredients, products): def __init__(self, name, category, ingredients, products):
self.name = name self.name = name
self.category = category self.category = category
@@ -83,12 +83,22 @@ class Recipe():
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.name})" return f"{self.__class__.__name__}({self.name})"
@property
def crafting_machines(self) -> Set[Machine]:
"""crafting machines able to run this recipe"""
return machines_per_category[self.category]
@property @property
def unlocking_technologies(self) -> Set[Technology]: def unlocking_technologies(self) -> Set[Technology]:
"""Unlocked by any of the returned technologies. Empty set indicates a starting recipe.""" """Unlocked by any of the returned technologies. Empty set indicates a starting recipe."""
return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())} return {technology_table[tech_name] for tech_name in recipe_sources.get(self.name, ())}
class Machine(FactorioElement):
def __init__(self, name, categories):
self.name: str = name
self.categories: set = categories
# recipes and technologies can share names in Factorio # recipes and technologies can share names in Factorio
for technology_name in sorted(raw): for technology_name in sorted(raw):
data = raw[technology_name] data = raw[technology_name]
@@ -107,17 +117,27 @@ for technology, data in raw.items():
del (raw) del (raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
recipes = {}
all_product_sources: Dict[str, Set[Recipe]] = {} all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
for recipe_name, recipe_data in raw_recipes.items(): for recipe_name, recipe_data in raw_recipes.items():
# example: # example:
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"} # "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"])) recipe = Recipe(recipe_name, recipe_data["category"], set(recipe_data["ingredients"]), set(recipe_data["products"]))
if recipe.products != recipe.ingredients and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging recipes[recipe_name] = Recipe
if recipe.products.isdisjoint(recipe.ingredients) and "empty-barrel" not in recipe.products: # prevents loop recipes like uranium centrifuging
for product_name in recipe.products: for product_name in recipe.products:
all_product_sources.setdefault(product_name, set()).add(recipe) all_product_sources.setdefault(product_name, set()).add(recipe)
del (raw_recipes)
machines: Dict[str, Machine] = {}
for name, categories in raw_machines.items():
machine = Machine(name, set(categories))
machines[name] = machine
del (raw_machines)
# build requirements graph for all technology ingredients # build requirements graph for all technology ingredients
@@ -126,7 +146,23 @@ for technology in technology_table.values():
all_ingredient_names |= technology.ingredients all_ingredient_names |= technology.ingredients
def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[Technology]: def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
return current_technologies
def unlock(recipe: Recipe, _done) -> Set[Technology]:
current_technologies = set()
current_technologies |= recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= required_category_technologies[recipe.category]
return current_technologies
def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_func=unlock_just_tech) -> Set[Technology]:
if _done: if _done:
if ingredient_name in _done: if ingredient_name in _done:
return set() return set()
@@ -139,16 +175,76 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[T
return set() return set()
current_technologies = set() current_technologies = set()
for recipe in recipes: for recipe in recipes:
current_technologies |= recipe.unlocking_technologies current_technologies |= unlock_func(recipe, _done)
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
return current_technologies return current_technologies
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in machines:
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
logical_machines = {}
for machine in machines.values():
logically_useful = True
for pot_source_machine in machines.values():
if machine != pot_source_machine \
and machine.categories.issuperset(pot_source_machine.categories) \
and required_machine_technologies[machine.name].issuperset(
required_machine_technologies[pot_source_machine.name]):
logically_useful = False
break
if logically_useful:
logical_machines[machine.name] = machine
del(required_machine_technologies)
machines_per_category: Dict[str: Set[Machine]] = {}
for machine in logical_machines.values():
for category in machine.categories:
machines_per_category.setdefault(category, set()).add(machine)
# required technologies to be able to craft recipes from a certain category
required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
for category_name, cat_machines in machines_per_category.items():
techs = set()
for machine in cat_machines:
techs |= recursively_get_unlocking_technologies(machine.name)
required_category_technologies[category_name] = frozenset(techs)
required_technologies: Dict[str, FrozenSet[Technology]] = {} required_technologies: Dict[str, FrozenSet[Technology]] = {}
for ingredient_name in all_ingredient_names: for ingredient_name in all_ingredient_names:
required_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name)) required_technologies[ingredient_name] = frozenset(
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))
advancement_technologies: Set[str] = set() advancement_technologies: Set[str] = set()
for technologies in required_technologies.values(): for technologies in required_technologies.values():
advancement_technologies |= {technology.name for technology in technologies} advancement_technologies |= {technology.name for technology in technologies}
@functools.lru_cache(10)
def get_rocket_requirements(ingredients: Set[str]) -> Set[str]:
techs = recursively_get_unlocking_technologies("rocket-silo")
for ingredient in ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
return {tech.name for tech in techs}
rocket_recipes = {
Options.MaxSciencePack.option_space_science_pack:
{"rocket-control-unit": 10, "low-density-structure": 10, "rocket-fuel": 10},
Options.MaxSciencePack.option_utility_science_pack:
{"speed-module": 10, "steel-plate": 10, "solid-fuel": 10},
Options.MaxSciencePack.option_production_science_pack:
{"speed-module": 10, "steel-plate": 10, "solid-fuel": 10},
Options.MaxSciencePack.option_chemical_science_pack:
{"advanced-circuit": 10, "steel-plate": 10, "solid-fuel": 10},
Options.MaxSciencePack.option_military_science_pack:
{"defender-capsule": 10, "stone-wall": 10, "coal": 10},
Options.MaxSciencePack.option_logistic_science_pack:
{"electronic-circuit": 10, "stone-brick": 10, "coal": 10},
Options.MaxSciencePack.option_automation_science_pack:
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
}

View File

@@ -1,18 +1,18 @@
from BaseClasses import Region, Entrance, Location, MultiWorld, Item from BaseClasses import Region, Entrance, Location, MultiWorld, Item
from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, required_technologies from .Technologies import tech_table, recipe_sources, technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes
from .Shapes import get_shapes from .Shapes import get_shapes
def gen_factorio(world: MultiWorld, player: int): def gen_factorio(world: MultiWorld, player: int):
static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option? static_nodes = world._static_nodes = {"automation", "logistics"} # turn dynamic/option?
victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value]))
for tech_name, tech_id in tech_table.items(): for tech_name, tech_id in tech_table.items():
tech_item = Item(tech_name, tech_name in advancement_technologies, tech_id, player) tech_item = Item(tech_name, tech_name in advancement_technologies or tech_name in victory_tech_names,
tech_id, player)
tech_item.game = "Factorio" tech_item.game = "Factorio"
if tech_name in static_nodes: if tech_name in static_nodes:
loc = world.get_location(tech_name, player) world.get_location(tech_name, player).place_locked_item(tech_item)
loc.item = tech_item
loc.locked = True
loc.event = tech_item.advancement
else: else:
world.itempool.append(tech_item) world.itempool.append(tech_item)
world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player) world.custom_data[player]["custom_technologies"] = custom_technologies = set_custom_technologies(world, player)
@@ -25,13 +25,26 @@ def factorio_create_regions(world: MultiWorld, player: int):
menu.exits.append(crash) menu.exits.append(crash)
nauvis = Region("Nauvis", None, "Nauvis", player) nauvis = Region("Nauvis", None, "Nauvis", player)
nauvis.world = menu.world = world nauvis.world = menu.world = world
for tech_name, tech_id in tech_table.items(): for tech_name, tech_id in tech_table.items():
tech = Location(player, tech_name, tech_id, nauvis) tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech) nauvis.locations.append(tech)
tech.game = "Factorio" tech.game = "Factorio"
location = Location(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location)
event = Item("Victory", True, None, player)
world.push_item(location, event, False)
location.event = location.locked = True
for ingredient in all_ingredient_names:
location = Location(player, f"Automate {ingredient}", None, nauvis)
nauvis.locations.append(location)
event = Item(f"Automated {ingredient}", True, None, player)
world.push_item(location, event, False)
location.event = location.locked = True
crash.connect(nauvis) crash.connect(nauvis)
world.regions += [menu, nauvis] world.regions += [menu, nauvis]
def set_custom_technologies(world: MultiWorld, player: int): def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies = {} custom_technologies = {}
world_custom = getattr(world, "_custom_technologies", {}) world_custom = getattr(world, "_custom_technologies", {})
@@ -42,11 +55,15 @@ def set_custom_technologies(world: MultiWorld, player: int):
custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player) custom_technologies[technology_name] = technology.get_custom(world, allowed_packs, player)
return custom_technologies return custom_technologies
def set_rules(world: MultiWorld, player: int, custom_technologies): def set_rules(world: MultiWorld, player: int, custom_technologies):
shapes = get_shapes(world, player) shapes = get_shapes(world, player)
if world.logic[player] != 'nologic': if world.logic[player] != 'nologic':
from worlds.generic import Rules from worlds.generic import Rules
for ingredient in all_ingredient_names:
location = world.get_location(f"Automate {ingredient}", player)
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for tech_name, technology in custom_technologies.items(): for tech_name, technology in custom_technologies.items():
location = world.get_location(tech_name, player) location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player)) Rules.set_rule(location, technology.build_rule(player))
@@ -55,7 +72,10 @@ def set_rules(world: MultiWorld, player: int, custom_technologies):
locations = {world.get_location(requisite, player) for requisite in prequisites} locations = {world.get_location(requisite, player) for requisite in prequisites}
Rules.add_rule(location, lambda state, Rules.add_rule(location, lambda state,
locations=locations: all(state.can_reach(loc) for loc in locations)) locations=locations: all(state.can_reach(loc) for loc in locations))
# get all science pack technologies (but not the ability to craft them)
victory_tech_names = get_rocket_requirements(frozenset(rocket_recipes[world.max_science_pack[player].value]))
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names)
# get all science pack technologies (but not the ability to craft them) world.completion_condition[player] = lambda state: state.has('Victory', player)
world.completion_condition[player] = lambda state: all(state.has(technology, player)
for technology in advancement_technologies)

View File

@@ -0,0 +1,2 @@
kivy>=2.0.0
factorio-rcon-py>=1.2.1

View File

@@ -15,7 +15,7 @@ advancement_table = {
"Who is Cutting Onions?": AdvData(42000, 'Overworld'), "Who is Cutting Onions?": AdvData(42000, 'Overworld'),
"Oh Shiny": AdvData(42001, 'Overworld'), "Oh Shiny": AdvData(42001, 'Overworld'),
"Suit Up": AdvData(42002, 'Overworld'), "Suit Up": AdvData(42002, 'Overworld'),
"Very Very Frightening": AdvData(42003, 'Village'), "Very Very Frightening": AdvData(42003, 'Overworld'),
"Hot Stuff": AdvData(42004, 'Overworld'), "Hot Stuff": AdvData(42004, 'Overworld'),
"Free the End": AdvData(42005, 'The End'), "Free the End": AdvData(42005, 'The End'),
"A Furious Cocktail": AdvData(42006, 'Nether Fortress'), "A Furious Cocktail": AdvData(42006, 'Nether Fortress'),

View File

@@ -38,12 +38,25 @@ def link_minecraft_structures(world: MultiWorld, player: int):
return False return False
def set_pair(exit, struct): def set_pair(exit, struct):
try:
assert exit in exits
assert struct in structs
except AssertionError as e:
raise Exception(f"Invalid connection: {exit} => {struct} for player {player}")
pairs[exit] = struct pairs[exit] = struct
exits.remove(exit) exits.remove(exit)
structs.remove(struct) structs.remove(struct)
# Plando stuff. Remove any utilized exits/structs from the lists. # Plando stuff. Remove any utilized exits/structs from the lists.
# Raise error if trying to put Nether Fortress in the End. # Raise error if trying to put Nether Fortress in the End.
if world.plando_connections[player]:
for connection in world.plando_connections[player]:
try:
if connection.entrance == 'The End Structure' and connection.exit == 'Nether Fortress':
raise Exception(f"Cannot place Nether Fortress in the End for player {player}")
set_pair(connection.entrance, connection.exit)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
if world.shuffle_structures[player]: if world.shuffle_structures[player]:
# Can't put Nether Fortress in the End # Can't put Nether Fortress in the End
@@ -51,7 +64,7 @@ def link_minecraft_structures(world: MultiWorld, player: int):
try: try:
end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress']) end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress'])
set_pair('The End Structure', end_struct) set_pair('The End Structure', end_struct)
except IndexError as e: except IndexError as e: # should only happen if structs is emptied by plando
raise Exception(f"Plando forced Nether Fortress in the End for player {player}") from e raise Exception(f"Plando forced Nether Fortress in the End for player {player}") from e
world.random.shuffle(structs) world.random.shuffle(structs)
for exit, struct in zip(exits[:], structs[:]): for exit, struct in zip(exits[:], structs[:]):

View File

@@ -42,9 +42,10 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player)) set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player))
set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player)) set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player))
set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player)) set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player)) set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player) and \
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike
set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player))
set_rule(world.get_location("Free the End", player), lambda state: can_complete(state)) set_rule(world.get_location("Free the End", player), lambda state: can_complete(state) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', 'Region', player))
set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.can_brew_potions(player) and
state.has("Fishing Rod", player) and # Water Breathing state.has("Fishing Rod", player) and # Water Breathing
state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets
@@ -58,8 +59,8 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player)) set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player))
set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state)) set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state))
set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player))
set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player)) set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True)
set_rule(world.get_location("This Boat Has Legs", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player)) set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", player))
set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player)) set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player))
set_rule(world.get_location("Nether", player), lambda state: True) set_rule(world.get_location("Nether", player), lambda state: True)
set_rule(world.get_location("Great View From Up Here", player), lambda state: state.basic_combat(player)) set_rule(world.get_location("Great View From Up Here", player), lambda state: state.basic_combat(player))
@@ -125,7 +126,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything
set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned
set_rule(world.get_location("Minecraft", player), lambda state: True) set_rule(world.get_location("Minecraft", player), lambda state: True)
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has_bottle_mc(player)) set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player))
set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player)) set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player))
set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and
state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and state.has("8 Netherite Scrap", player, 2) and state.has("Ingot Crafting", player) and
@@ -141,7 +142,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails set_rule(world.get_location("On a Rail", player), lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails
set_rule(world.get_location("Time to Strike!", player), lambda state: True) set_rule(world.get_location("Time to Strike!", player), lambda state: True)
set_rule(world.get_location("Cow Tipper", player), lambda state: True) set_rule(world.get_location("Cow Tipper", player), lambda state: True)
set_rule(world.get_location("When Pigs Fly", player), lambda state: state.fortress_loot(player) and state.has("Fishing Rod", player) and state.can_adventure(player)) # saddles in fortress chests set_rule(world.get_location("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", player) and state.can_adventure(player))
set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit set_rule(world.get_location("Overkill", player), lambda state: state.can_brew_potions(player) and (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player)) set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player))