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
build
/build_factorio/
bundle/components.wxs
dist
README.html
@@ -142,4 +143,4 @@ dmypy.json
.pytype/
# Cython debug symbols
cython_debug/
cython_debug/

View File

@@ -1193,6 +1193,14 @@ class Location():
return True
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):
return self.__str__()
@@ -1221,7 +1229,7 @@ class Item():
zora_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.advancement = advancement
self.player = player
@@ -1467,6 +1475,7 @@ class Spoiler(object):
return json.dumps(out)
def to_file(self, filename):
import Options
self.parse_data()
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'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
if player in self.world.hk_player_ids:
import Options
for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player]
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:
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')
if player in self.world.alttp_player_ids:
elif player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
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'] == 'exit' else '=>',
entry['exit']) for entry in self.entrances.values()]))
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.medallions:
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
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\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 range(1, self.world.players + 1):
if self.shops:
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':
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')
@@ -1595,19 +1616,20 @@ class Spoiler(object):
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write('\n\nPaths:\n\n')
path_listings = []
for location, path in sorted(self.paths.items()):
path_lines = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))
else:
path_lines.append(region)
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
if self.paths:
outfile.write('\n\nPaths:\n\n')
path_listings = []
for location, path in sorted(self.paths.items()):
path_lines = []
for region, exit in path:
if exit is not None:
path_lines.append("{} -> {}".format(region, exit))
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.generic import PlandoItem, PlandoConnection

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
import os
import logging
import json
import string
import copy
import sys
from concurrent.futures import ThreadPoolExecutor
import colorama
@@ -13,7 +15,7 @@ from MultiServer import mark_raw
import Utils
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
@@ -21,7 +23,6 @@ rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago"
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
@@ -34,13 +35,14 @@ if not os.path.exists(executable):
else:
raise FileNotFoundError(executable)
import sys
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):
ctx: FactorioContext
@mark_raw
def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server."""
@@ -65,7 +67,9 @@ class FactorioContext(CommonContext):
super(FactorioContext, self).__init__(*args, **kwargs)
self.send_index = 0
self.rcon_client = None
self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self)
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested):
if password_requested and not self.password:
@@ -81,43 +85,50 @@ class FactorioContext(CommonContext):
logger.info(args["text"])
if self.rcon_client:
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):
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.
copy_data = copy.deepcopy(args["data"]) # jsontotextparser is destructive currently
logger.info(self.jsontotextparser(args["data"]))
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
logger.info(text)
if self.rcon_client:
cleaned_text = self.raw_json_text_parser(copy_data).replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"Archipelago: {cleaned_text}\")")
text = self.factorio_json_text_parser(args["data"])
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):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0
try:
while 1:
while not ctx.exit_event.is_set():
if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.")
while 1:
with open(bridge_file) as f:
data = json.load(f)
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
ctx.auth = data["slot_name"]
ctx.seed_name = data["seed_name"]
while not ctx.exit_event.is_set():
if ctx.awaiting_bridge:
ctx.awaiting_bridge = False
with open(bridge_file) as f:
data = json.load(f)
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
ctx.auth = data["slot_name"]
ctx.seed_name = data["seed_name"]
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.info(f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
if ctx.locations_checked != research_data:
bridge_logger.info(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1)
else:
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.stderr, factorio_queue)
script_folder = None
progression_watcher = None
try:
while 1:
while not ctx.exit_event.is_set():
while not factorio_queue.empty():
msg = factorio_queue.get()
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)
# trigger lua interface confirmation
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):
os.remove(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:
while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
@@ -196,6 +211,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
finally:
factorio_process.terminate()
if progression_watcher:
await progression_watcher
async def main():
ctx = FactorioContext(None, None, True)
@@ -226,6 +246,20 @@ async def main():
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__':
colorama.init()
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
def set_icon(window):
er16 = tk.PhotoImage(file=local_path('data', 'ER16.gif'))
er32 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
er48 = tk.PhotoImage(file=local_path('data', 'ER32.gif'))
window.tk.call('wm', 'iconphoto', window._w, er16, er32, er48) # pylint: disable=protected-access
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
window.tk.call('wm', 'iconphoto', window._w, logo)
# 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

View File

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

View File

@@ -1,18 +1,13 @@
import argparse
import atexit
import time
import functools
import webbrowser
import multiprocessing
import socket
import os
import subprocess
import base64
import shutil
from json import loads, dumps
from random import randrange
from Utils import get_item_name_from_id
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)
logger.info('')
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids:
for item_name in args.startinventory[player]:
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
item.game = world.game[player]
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:
# enforce pre-defined local items.
@@ -414,7 +415,7 @@ def main(args, seed=None):
return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor()
multidata_task = None
check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom_futures = []
@@ -501,7 +502,10 @@ def main(args, seed=None):
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {}
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]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}

View File

@@ -1,55 +1,48 @@
import os
import sys
import subprocess
import importlib
import pkg_resources
requirements_files = {'requirements.txt'}
if sys.version_info < (3, 8, 6):
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():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt', '--upgrade'])
naming_specialties = {"PyYAML": "yaml", # PyYAML is imported as the name yaml
"maseya-z3pr": "maseya",
"factorio-rcon-py": "factorio_rcon"}
for file in requirements_files:
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
def update():
global update_ran
if not update_ran:
update_ran = True
path = os.path.join(os.path.dirname(sys.argv[0]), 'requirements.txt')
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), 'requirements.txt')
with open(path) as requirementsfile:
for line in requirementsfile.readlines():
module, remote_version = line.split(">=")
module = naming_specialties.get(module, module)
try:
module = importlib.import_module(module)
except:
import traceback
traceback.print_exc()
input(f'Required python module {module} not found, press enter to install it')
update_command()
return
else:
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
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
requirements = pkg_resources.parse_requirements(requirementsfile)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
if __name__ == "__main__":

View File

@@ -45,7 +45,6 @@ if __name__ == "__main__":
zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"]
# 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"]
weights_file_path = multi_mystery_options["weights_file_path"]
pre_roll = multi_mystery_options["pre_roll"]
@@ -124,15 +123,6 @@ if __name__ == "__main__":
spoilername = f"AP_{seed_name}_Spoiler.txt"
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)):
import zipfile
@@ -167,7 +157,7 @@ if __name__ == "__main__":
def _handle_sfc_file(file: str):
if zip_roms:
pack_file(file)
if zip_roms == 2 and player_name.lower() not in file.lower():
if zip_roms == 2:
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)))
else:
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:
raise Exception(f"Unsupported game {ret.game}")
return ret

View File

@@ -139,6 +139,8 @@ class OptionDict(Option):
else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self):
return str(self.value)
class Logic(Choice):
option_no_glitches = 0
@@ -309,8 +311,16 @@ class TechTreeLayout(Choice):
option_single = 0
option_small_diamonds = 1
option_medium_diamonds = 2
option_pyramid = 3
option_funnel = 4
option_large_diamonds = 3
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
@@ -319,6 +329,12 @@ class Visibility(Choice):
option_sending = 1
default = 1
class RecipeTime(Choice):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
option_chaos = 5
class FactorioStartItems(OptionDict):
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,
"visibility": Visibility,
"random_tech_ingredients": Toggle,
"starting_items": FactorioStartItems}
"starting_items": FactorioStartItems,
"recipe_time": RecipeTime}
class AdvancementGoal(Choice):

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# A Link to the Past Randomizer Setup Guide
## 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)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
- Ein Emulator, der lua-scripts abspielen kann
@@ -15,7 +15,7 @@
### Windows
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!**.
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.
- 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.

View File

@@ -7,7 +7,7 @@
</div>
## 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)
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of running Lua scripts
@@ -21,7 +21,7 @@
### Windows Setup
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
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.
- 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
@@ -144,7 +144,7 @@ on successfully joining a multiworld game!
## Hosting a MultiWorld game
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.
2. Create a zip file containing your players' YAML files.

View File

@@ -7,7 +7,7 @@
</div>
## 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)
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
- Un emulador capaz de ejecutar scripts Lua
@@ -20,7 +20,7 @@
### Instalación en Windows
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'
- 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.
@@ -136,7 +136,7 @@ por unirte satisfactoriamente a una partida de multiworld!
## Hospedando una partida de multiworld
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.
2. Crea un fichero ZIP conteniendo esos ficheros.

View File

@@ -7,7 +7,7 @@
</div>
## 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)
- 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

View File

@@ -2,7 +2,7 @@
## Configuration
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.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
value to
@@ -13,7 +13,7 @@
### Bosses
- 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.
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
it defaults to vanilla

View File

@@ -31,7 +31,7 @@
<p>
After generation is complete, you will have the option to download a patch file.
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.
</p>
<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">
<div id="copyright-notice">Copyright 2021 Archipelago</div>
<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>
</footer>
{% endblock %}

View File

@@ -43,9 +43,9 @@
trackers are provided for games hosted here.</p>
<p>
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
<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.
</p>
<p>

View File

@@ -5,7 +5,7 @@ from werkzeug.exceptions import abort
import datetime
from uuid import UUID
from worlds.alttp import Items, Regions
from worlds.alttp import Items
from WebHostLib import app, cache, Room
from Utils import restricted_loads
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])
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
return result
@@ -344,9 +345,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
abort(404)
# 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]
seed_checks_in_area = seed_checks_in_area[tracked_player]
location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter()
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
checks_done[location_to_area[location]] += 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
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
if game_state == 30:
inventory[106] = 1 # Triforce
# Progressive items need special handling for icons and class
progressive_items = {
"Progressive Sword": 94,
"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
progressive_items = {
"Progressive Sword": 94,
"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"]
}
# 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]
# 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])-1)
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?
sp_areas = ordered_areas[2:15]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15]
return render_template("playerTracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
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},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player],
**display_data)
return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
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,
key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player],
**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>')
@@ -432,7 +439,7 @@ def getTracker(tracker: UUID):
if not room:
abort(404)
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)}
for teamnumber, team in enumerate(names)}
@@ -447,7 +454,6 @@ def getTracker(tracker: UUID):
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
@@ -486,7 +492,7 @@ def getTracker(tracker: UUID):
for player, name in enumerate(names, 1):
player_names[(team, player)] = name
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
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) -%}
{
{% for key, value in dict.items() %}
["{{ key }}"] = {{ value | safe }}{% if not loop.last %},{% endif %}
{% endfor %}
}
{%- endmacro %}
{% 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
require "lib"
require "util"
@@ -138,16 +133,18 @@ script.on_init(function()
end)
-- for testing
script.on_event(defines.events.on_tick, function(event)
if event.tick%600 == 300 then
dumpInfo(game.forces["player"])
end
end)
-- script.on_event(defines.events.on_tick, function(event)
-- if event.tick%3600 == 300 then
-- dumpInfo(game.forces["player"])
-- end
-- end)
-- hook into researches done
script.on_event(defines.events.on_research_finished, function(event)
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
return -- Nothing else to do
end
@@ -191,6 +188,7 @@ function dumpInfo(force)
end
end
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.print("Sent progress to Archipelago.")
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
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 original_tech
@@ -30,31 +31,51 @@ function prep_copy(new_copy, old_tech)
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")
{# 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}}"]
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
prep_copy(new_tree_copy, original_tech)
{% if tech_cost != 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 }}))
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
new_tree_copy.unit.count = math.max(1, math.floor(new_tree_copy.unit.count * {{ tech_cost_scale }}))
{% 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 #}
{%- if original_tech_name in tech_tree_layout_prerequisites %}
{%- 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 -%}
{#- add new Technology to game #}
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]
{% for original_tech_name, item_name, receiving_player in locations %}
{% for original_tech_name, item_name, receiving_player, advancement in locations %}
{%- if visibility -%}
ap-{{ tech_table[original_tech_name] }}-={{ player_names[receiving_player] }}'s {{ item_name }}
{% else %}
@@ -9,9 +9,9 @@ ap-{{ tech_table[original_tech_name] }}-= An Archipelago Sendable
{% endfor %}
[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 -%}
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 %}
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone.
{%- 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
# 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
hint_cost: 1000 # Set to 0 if you want free hints
hint_cost: 10 # Set to 0 if you want free hints
# Forfeit modes
# "disabled" -> clients can't 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
# as well as the generated pre-rolled yaml.
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
# 0 -> None
# 1 -> Full spoiler

View File

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

View File

@@ -82,7 +82,7 @@ begin
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.28.29325') < 0);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
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.
game:
A Link to the Past: 1
Hollow Knight: 1
Factorio: 1
Minecraft: 1
# Shared Options supported by all games:
@@ -49,8 +48,19 @@ tech_tree_layout:
single: 1
small_diamonds: 1
medium_diamonds: 1
pyramid: 1
funnel: 1
large_diamonds: 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:
automation_science_pack: 0
logistic_science_pack: 0

View File

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

View File

@@ -38,15 +38,15 @@ def _threaded_hash(filepath):
os.makedirs(buildfolder, exist_ok=True)
def manifest_creation():
def manifest_creation(folder):
hashes = {}
manifestpath = os.path.join(buildfolder, "manifest.json")
manifestpath = os.path.join(folder, "manifest.json")
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor()
for dirpath, dirnames, filenames in os.walk(buildfolder):
for dirpath, dirnames, filenames in os.walk(folder):
for filename in filenames:
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
from Utils import _version_tuple
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
@@ -161,4 +161,79 @@ for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore":
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, [], ['Flint and Steel']],
["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", True, ['Ingot Crafting', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor', 'Flint and Steel', 'Bucket', 'Fishing Rod']],
["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', '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']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket']],
["Hot Tourist Destinations", True, ['Ingot Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools']],
])
def test_42015(self):
@@ -979,7 +974,8 @@ class TestAdvancements(TestMinecraft):
["Sticky Situation", False, []],
["Sticky Situation", False, [], ['Bottles']],
["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):
@@ -1099,16 +1095,17 @@ class TestAdvancements(TestMinecraft):
self.run_location_tests([
["When Pigs Fly", False, []],
["When Pigs Fly", False, [], ['Ingot Crafting']],
["When Pigs Fly", False, [], ['Flint and Steel']],
["When Pigs Fly", False, [], ['Progressive Tools']],
["When Pigs Fly", False, [], ['Progressive Weapons']],
["When Pigs Fly", False, [], ['Progressive Armor', 'Shield']],
["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', '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', '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):

View File

@@ -241,25 +241,13 @@ def generate_itempool(world, player: int):
else:
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':
world.progression_balancing[player] = False
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 'turtle rock-' not in 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:
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).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 ' \
f'have beaten Agahnim atop Ganons Tower'
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_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:
if world.crystals_needed_for_ganon[player] == 1:
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['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_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
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:
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\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])
else:
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\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])
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.'

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 Options
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
locale_template: Optional[jinja2.Template] = None
@@ -28,37 +30,35 @@ base_info = {
"factorio_version": "1.1"
}
# TODO: clean this up, probably as a jinja macro; then add logic for the recipes in completion condition
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}}'
recipe_time_scales = {
# using random.triangular
Options.RecipeTime.option_fast: (0.25, 1),
# 0.5, 2, 0.5 average -> 1.0
Options.RecipeTime.option_normal: (0.5, 2, 0.5),
Options.RecipeTime.option_slow: (1, 4),
# 0.25, 4, 0.25 average -> 1.5
Options.RecipeTime.option_chaos: (0.25, 4, 0.25),
Options.RecipeTime.option_vanilla: None
}
def generate_mod(world: MultiWorld, player: int):
global template, locale_template, control_template
with template_load_lock:
if not 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())
locale_template = jinja2.Template(open(os.path.join(mod_template_folder, "locale", "en", "locale.cfg")).read())
control_template = jinja2.Template(open(os.path.join(mod_template_folder, "control.lua")).read())
template_env: Optional[jinja2.Environment] = \
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
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
player_names = {x: world.player_names[x][0] for x in world.player_ids}
locations = []
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]}"
tech_cost = {0: 0.1,
1: 0.25,
@@ -67,13 +67,15 @@ def generate_mod(world: MultiWorld, player: int):
4: 2,
5: 5,
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(),
"tech_cost_scale": tech_cost, "custom_data": world.custom_data[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,
"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:
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)
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")
os.makedirs(en_locale_dir, 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)
# 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:
for root, dirs, files in os.walk(mod_dir):
for file in files:
zf.write(os.path.join(root, file),
os.path.relpath(os.path.join(root, file),
os.path.join(mod_dir, '..')))
os.path.relpath(os.path.join(root, file),
os.path.join(mod_dir, '..')))
shutil.rmtree(mod_dir)

View File

@@ -2,21 +2,29 @@ from typing import Dict, List, Set
from BaseClasses import MultiWorld
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]]:
prerequisites: Dict[str, Set[str]] = {}
layout = world.tech_tree_layout[player].value
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:
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:
slice = 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
# 0 |
@@ -24,15 +32,13 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
# 3 V
prerequisites[diamond_3] = {diamond_1, diamond_2}
prerequisites[diamond_2] = prerequisites[diamond_1] = {diamond_0}
elif layout == TechTreeLayout.option_medium_diamonds:
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:
slice = 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 |
# 1 2 |
@@ -52,44 +58,136 @@ def get_shapes(world: MultiWorld, player: int) -> Dict[str, List[str]]:
prerequisites[slice[8]] = {slice[6], slice[7]}
elif layout == TechTreeLayout.option_pyramid:
slice_size = 1
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 = []
elif layout == TechTreeLayout.option_large_diamonds:
slice_size = 16
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
world.random.shuffle(slice)
tech_names = tech_names[slice_size:]
for i, tech_name in enumerate(previous_slice):
prerequisites.setdefault(slice[i], set()).add(tech_name)
prerequisites.setdefault(slice[i + 1], set()).add(tech_name)
previous_slice = slice
slice_size += 1
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
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)
# find largest inverse pyramid
# https://www.wolframalpha.com/input/?i=x+=+1/2+(n++++1)+(2++++n)+solve+for+n
import math
slice_size = int(0.5*(math.sqrt(8*len(tech_names)+1)-3))
tech_names.sort()
world.random.shuffle(tech_names)
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].ingredients))
previous_slice = []
while slice_size:
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], 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]
world.random.shuffle(slice)
tech_names = tech_names[slice_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
slice_size -= 1
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
# 0 |
# 1 2 |
# 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
return prerequisites

View File

@@ -1,24 +1,40 @@
from __future__ import annotations
# Factorio technologies are imported from a .json document in /data
from typing import Dict, Set, FrozenSet
import os
import json
import Options
import Utils
import logging
import functools
factorio_id = 2 ** 17
source_file = Utils.local_path("data", "factorio", "techs.json")
recipe_source_file = Utils.local_path("data", "factorio", "recipes.json")
with open(source_file) as f:
source_folder = Utils.local_path("data", "factorio")
with open(os.path.join(source_folder, "techs.json")) as 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)
with open(os.path.join(source_folder, "machines.json")) as f:
raw_machines = json.load(f)
tech_table: Dict[str, int] = {}
technology_table: Dict[str, Technology] = {}
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):
self.name = name
self.factorio_id = factorio_id
@@ -26,35 +42,20 @@ class Technology(): # maybe make subclass of Location?
def build_rule(self, player: int):
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."""
technologies = set()
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
def __hash__(self):
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:
return CustomTechnology(self, world, allowed_packs, player)
@@ -67,13 +68,12 @@ class CustomTechnology(Technology):
self.player = player
if world.random_tech_ingredients[player]:
ingredients = list(ingredients)
ingredients.sort() # deterministic sample
ingredients.sort() # deterministic sample
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
class Recipe():
class Recipe(FactorioElement):
def __init__(self, name, category, ingredients, products):
self.name = name
self.category = category
@@ -83,12 +83,22 @@ class Recipe():
def __repr__(self):
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
def unlocking_technologies(self) -> Set[Technology]:
"""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, ())}
class Machine(FactorioElement):
def __init__(self, name, categories):
self.name: str = name
self.categories: set = categories
# recipes and technologies can share names in Factorio
for technology_name in sorted(raw):
data = raw[technology_name]
@@ -107,17 +117,27 @@ for technology, data in raw.items():
del (raw)
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
all_product_sources: Dict[str, Set[Recipe]] = {}
recipes = {}
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
for recipe_name, recipe_data in raw_recipes.items():
# example:
# "accumulator":{"ingredients":["iron-plate","battery"],"products":["accumulator"],"category":"crafting"}
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:
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
@@ -126,7 +146,23 @@ for technology in technology_table.values():
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 ingredient_name in _done:
return set()
@@ -139,16 +175,76 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None) -> Set[T
return set()
current_technologies = set()
for recipe in recipes:
current_technologies |= recipe.unlocking_technologies
for ingredient_name in recipe.ingredients:
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done)
current_technologies |= unlock_func(recipe, _done)
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]] = {}
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()
for technologies in required_technologies.values():
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 .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
def gen_factorio(world: MultiWorld, player: int):
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():
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"
if tech_name in static_nodes:
loc = world.get_location(tech_name, player)
loc.item = tech_item
loc.locked = True
loc.event = tech_item.advancement
world.get_location(tech_name, player).place_locked_item(tech_item)
else:
world.itempool.append(tech_item)
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)
nauvis = Region("Nauvis", None, "Nauvis", player)
nauvis.world = menu.world = world
for tech_name, tech_id in tech_table.items():
tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech)
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)
world.regions += [menu, nauvis]
def set_custom_technologies(world: MultiWorld, player: int):
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)
return custom_technologies
def set_rules(world: MultiWorld, player: int, custom_technologies):
shapes = get_shapes(world, player)
if world.logic[player] != 'nologic':
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():
location = world.get_location(tech_name, 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}
Rules.add_rule(location, lambda state,
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: all(state.has(technology, player)
for technology in advancement_technologies)
world.completion_condition[player] = lambda state: state.has('Victory', player)

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'),
"Oh Shiny": AdvData(42001, 'Overworld'),
"Suit Up": AdvData(42002, 'Overworld'),
"Very Very Frightening": AdvData(42003, 'Village'),
"Very Very Frightening": AdvData(42003, 'Overworld'),
"Hot Stuff": AdvData(42004, 'Overworld'),
"Free the End": AdvData(42005, 'The End'),
"A Furious Cocktail": AdvData(42006, 'Nether Fortress'),

View File

@@ -38,12 +38,25 @@ def link_minecraft_structures(world: MultiWorld, player: int):
return False
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
exits.remove(exit)
structs.remove(struct)
# Plando stuff. Remove any utilized exits/structs from the lists.
# 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]:
# Can't put Nether Fortress in the End
@@ -51,7 +64,7 @@ def link_minecraft_structures(world: MultiWorld, player: int):
try:
end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress'])
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
world.random.shuffle(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("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("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("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
state.has("Fishing Rod", player) and # Water Breathing
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("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("Hot Tourist Destinations", 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) 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) 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("Nether", player), lambda state: True)
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 Throwaway Joke", player), lambda state: True) # kill drowned
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("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
@@ -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("Time to Strike!", 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("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))