Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into oot
This commit is contained in:
commit
6641b13511
|
@ -12,10 +12,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
|
@ -12,10 +12,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
|
@ -11,7 +11,7 @@ import websockets
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Utils.init_logging("TextClient")
|
Utils.init_logging("TextClient", exception_logger="Client")
|
||||||
|
|
||||||
from MultiServer import CommandProcessor
|
from MultiServer import CommandProcessor
|
||||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
||||||
|
@ -39,13 +39,13 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
def _cmd_connect(self, address: str = "") -> bool:
|
def _cmd_connect(self, address: str = "") -> bool:
|
||||||
"""Connect to a MultiWorld Server"""
|
"""Connect to a MultiWorld Server"""
|
||||||
self.ctx.server_address = None
|
self.ctx.server_address = None
|
||||||
asyncio.create_task(self.ctx.connect(address if address else None))
|
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cmd_disconnect(self) -> bool:
|
def _cmd_disconnect(self) -> bool:
|
||||||
"""Disconnect from a MultiWorld Server"""
|
"""Disconnect from a MultiWorld Server"""
|
||||||
self.ctx.server_address = None
|
self.ctx.server_address = None
|
||||||
asyncio.create_task(self.ctx.disconnect())
|
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cmd_received(self) -> bool:
|
def _cmd_received(self) -> bool:
|
||||||
|
@ -81,6 +81,16 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
self.output("No missing location checks found.")
|
self.output("No missing location checks found.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _cmd_items(self):
|
||||||
|
self.output(f"Item Names for {self.ctx.game}")
|
||||||
|
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
|
||||||
|
self.output(item_name)
|
||||||
|
|
||||||
|
def _cmd_locations(self):
|
||||||
|
self.output(f"Location Names for {self.ctx.game}")
|
||||||
|
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
|
||||||
|
self.output(location_name)
|
||||||
|
|
||||||
def _cmd_ready(self):
|
def _cmd_ready(self):
|
||||||
self.ctx.ready = not self.ctx.ready
|
self.ctx.ready = not self.ctx.ready
|
||||||
if self.ctx.ready:
|
if self.ctx.ready:
|
||||||
|
@ -89,10 +99,10 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
else:
|
else:
|
||||||
state = ClientStatus.CLIENT_CONNECTED
|
state = ClientStatus.CLIENT_CONNECTED
|
||||||
self.output("Unreadied.")
|
self.output("Unreadied.")
|
||||||
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]))
|
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
|
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
|
||||||
|
|
||||||
|
|
||||||
class CommonContext():
|
class CommonContext():
|
||||||
|
@ -149,7 +159,7 @@ class CommonContext():
|
||||||
self.set_getters(network_data_package)
|
self.set_getters(network_data_package)
|
||||||
|
|
||||||
# execution
|
# execution
|
||||||
self.keep_alive_task = asyncio.create_task(keep_alive(self))
|
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_locations(self) -> typing.Optional[int]:
|
def total_locations(self) -> typing.Optional[int]:
|
||||||
|
@ -230,13 +240,24 @@ class CommonContext():
|
||||||
self.password = await self.console_input()
|
self.password = await self.console_input()
|
||||||
return self.password
|
return self.password
|
||||||
|
|
||||||
|
async def send_connect(self, **kwargs):
|
||||||
|
payload = {
|
||||||
|
"cmd": 'Connect',
|
||||||
|
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||||
|
'tags': self.tags,
|
||||||
|
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
||||||
|
}
|
||||||
|
if kwargs:
|
||||||
|
payload.update(kwargs)
|
||||||
|
await self.send_msgs([payload])
|
||||||
|
|
||||||
async def console_input(self):
|
async def console_input(self):
|
||||||
self.input_requests += 1
|
self.input_requests += 1
|
||||||
return await self.input_queue.get()
|
return await self.input_queue.get()
|
||||||
|
|
||||||
async def connect(self, address=None):
|
async def connect(self, address=None):
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
self.server_task = asyncio.create_task(server_loop(self, address))
|
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
|
||||||
|
|
||||||
def on_print(self, args: dict):
|
def on_print(self, args: dict):
|
||||||
logger.info(args["text"])
|
logger.info(args["text"])
|
||||||
|
@ -271,7 +292,7 @@ class CommonContext():
|
||||||
logger.info(f"DeathLink: Received from {data['source']}")
|
logger.info(f"DeathLink: Received from {data['source']}")
|
||||||
|
|
||||||
async def send_death(self, death_text: str = ""):
|
async def send_death(self, death_text: str = ""):
|
||||||
logger.info("Sending death to your friends...")
|
logger.info("DeathLink: Sending death to your friends...")
|
||||||
self.last_death_link = time.time()
|
self.last_death_link = time.time()
|
||||||
await self.send_msgs([{
|
await self.send_msgs([{
|
||||||
"cmd": "Bounce", "tags": ["DeathLink"],
|
"cmd": "Bounce", "tags": ["DeathLink"],
|
||||||
|
@ -282,6 +303,27 @@ class CommonContext():
|
||||||
}
|
}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
self.server_address = None
|
||||||
|
if self.server and not self.server.socket.closed:
|
||||||
|
await self.server.socket.close()
|
||||||
|
if self.server_task:
|
||||||
|
await self.server_task
|
||||||
|
|
||||||
|
while self.input_requests > 0:
|
||||||
|
self.input_queue.put_nowait(None)
|
||||||
|
self.input_requests -= 1
|
||||||
|
self.keep_alive_task.cancel()
|
||||||
|
|
||||||
|
async def update_death_link(self, death_link):
|
||||||
|
old_tags = self.tags.copy()
|
||||||
|
if death_link:
|
||||||
|
self.tags.add("DeathLink")
|
||||||
|
else:
|
||||||
|
self.tags -= {"DeathLink"}
|
||||||
|
if old_tags != self.tags and self.server and not self.server.socket.closed:
|
||||||
|
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
|
||||||
|
|
||||||
|
|
||||||
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
|
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
|
||||||
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
|
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
|
||||||
|
@ -340,14 +382,14 @@ async def server_loop(ctx: CommonContext, address=None):
|
||||||
await ctx.connection_closed()
|
await ctx.connection_closed()
|
||||||
if ctx.server_address:
|
if ctx.server_address:
|
||||||
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
|
||||||
asyncio.create_task(server_autoreconnect(ctx))
|
asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
|
||||||
ctx.current_reconnect_delay *= 2
|
ctx.current_reconnect_delay *= 2
|
||||||
|
|
||||||
|
|
||||||
async def server_autoreconnect(ctx: CommonContext):
|
async def server_autoreconnect(ctx: CommonContext):
|
||||||
await asyncio.sleep(ctx.current_reconnect_delay)
|
await asyncio.sleep(ctx.current_reconnect_delay)
|
||||||
if ctx.server_address and ctx.server_task is None:
|
if ctx.server_address and ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
|
|
||||||
|
|
||||||
async def process_server_cmd(ctx: CommonContext, args: dict):
|
async def process_server_cmd(ctx: CommonContext, args: dict):
|
||||||
|
@ -534,6 +576,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
tags = {"AP", "IgnoreGame"}
|
tags = {"AP", "IgnoreGame"}
|
||||||
|
game = "Archipelago"
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
|
@ -542,11 +585,7 @@ if __name__ == '__main__':
|
||||||
logger.info('Enter slot name:')
|
logger.info('Enter slot name:')
|
||||||
self.auth = await self.console_input()
|
self.auth = await self.console_input()
|
||||||
|
|
||||||
await self.send_msgs([{"cmd": 'Connect',
|
await self.send_connect()
|
||||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
|
||||||
'tags': self.tags,
|
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
|
||||||
}])
|
|
||||||
|
|
||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
|
@ -555,7 +594,7 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
async def main(args):
|
async def main(args):
|
||||||
ctx = TextContext(args.connect, args.password)
|
ctx = TextContext(args.connect, args.password)
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||||
if gui_enabled:
|
if gui_enabled:
|
||||||
input_task = None
|
input_task = None
|
||||||
from kvui import TextManager
|
from kvui import TextManager
|
||||||
|
@ -566,16 +605,7 @@ if __name__ == '__main__':
|
||||||
ui_task = None
|
ui_task = None
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
|
|
||||||
ctx.server_address = None
|
await ctx.shutdown()
|
||||||
if ctx.server and not ctx.server.socket.closed:
|
|
||||||
await ctx.server.socket.close()
|
|
||||||
if ctx.server_task:
|
|
||||||
await ctx.server_task
|
|
||||||
|
|
||||||
while ctx.input_requests > 0:
|
|
||||||
ctx.input_queue.put_nowait(None)
|
|
||||||
ctx.input_requests -= 1
|
|
||||||
|
|
||||||
if ui_task:
|
if ui_task:
|
||||||
await ui_task
|
await ui_task
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from queue import Queue
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Utils.init_logging("FactorioClient")
|
Utils.init_logging("FactorioClient", exception_logger="Client")
|
||||||
|
|
||||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
||||||
get_base_parser
|
get_base_parser
|
||||||
|
@ -65,22 +65,13 @@ class FactorioContext(CommonContext):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
await super(FactorioContext, self).server_auth(password_requested)
|
await super(FactorioContext, self).server_auth(password_requested)
|
||||||
|
|
||||||
if not self.auth:
|
if self.rcon_client:
|
||||||
if self.rcon_client:
|
await get_info(self, self.rcon_client) # retrieve current auth code
|
||||||
get_info(self, self.rcon_client) # retrieve current auth code
|
else:
|
||||||
else:
|
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
"bridge to Factorio first.")
|
||||||
"bridge to Factorio first.")
|
|
||||||
|
|
||||||
await self.send_msgs([{
|
await self.send_connect()
|
||||||
"cmd": 'Connect',
|
|
||||||
'password': self.password,
|
|
||||||
'name': self.auth,
|
|
||||||
'version': Utils.version_tuple,
|
|
||||||
'tags': self.tags,
|
|
||||||
'uuid': Utils.get_unique_identifier(),
|
|
||||||
'game': "Factorio"
|
|
||||||
}])
|
|
||||||
|
|
||||||
def on_print(self, args: dict):
|
def on_print(self, args: dict):
|
||||||
super(FactorioContext, self).on_print(args)
|
super(FactorioContext, self).on_print(args)
|
||||||
|
@ -134,6 +125,8 @@ async def game_watcher(ctx: FactorioContext):
|
||||||
research_data = data["research_done"]
|
research_data = data["research_done"]
|
||||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||||
victory = data["victory"]
|
victory = data["victory"]
|
||||||
|
if "death_link" in data: # TODO: Remove this if statement around version 0.2.4 or so
|
||||||
|
await ctx.update_death_link(data["death_link"])
|
||||||
|
|
||||||
if not ctx.finished_game and victory:
|
if not ctx.finished_game and victory:
|
||||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||||
|
@ -148,7 +141,8 @@ async def game_watcher(ctx: FactorioContext):
|
||||||
death_link_tick = data.get("death_link_tick", 0)
|
death_link_tick = data.get("death_link_tick", 0)
|
||||||
if death_link_tick != ctx.death_link_tick:
|
if death_link_tick != ctx.death_link_tick:
|
||||||
ctx.death_link_tick = death_link_tick
|
ctx.death_link_tick = death_link_tick
|
||||||
await ctx.send_death()
|
if "DeathLink" in ctx.tags:
|
||||||
|
await ctx.send_death()
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
@ -234,14 +228,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||||
factorio_process.wait(5)
|
factorio_process.wait(5)
|
||||||
|
|
||||||
|
|
||||||
def get_info(ctx, rcon_client):
|
async def get_info(ctx, rcon_client):
|
||||||
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
|
||||||
ctx.auth = info["slot_name"]
|
ctx.auth = info["slot_name"]
|
||||||
ctx.seed_name = info["seed_name"]
|
ctx.seed_name = info["seed_name"]
|
||||||
# 0.2.0 addition, not present earlier
|
# 0.2.0 addition, not present earlier
|
||||||
death_link = bool(info.get("death_link", False))
|
death_link = bool(info.get("death_link", False))
|
||||||
if death_link:
|
await ctx.update_death_link(death_link)
|
||||||
ctx.tags.add("DeathLink")
|
|
||||||
|
|
||||||
|
|
||||||
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||||
|
@ -280,7 +273,7 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
||||||
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||||
if ctx.mod_version == ctx.__class__.mod_version:
|
if ctx.mod_version == ctx.__class__.mod_version:
|
||||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||||
get_info(ctx, rcon_client)
|
await get_info(ctx, rcon_client)
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -322,14 +315,7 @@ async def main(args):
|
||||||
await progression_watcher
|
await progression_watcher
|
||||||
await factorio_server_task
|
await factorio_server_task
|
||||||
|
|
||||||
if ctx.server and not ctx.server.socket.closed:
|
await ctx.shutdown()
|
||||||
await ctx.server.socket.close()
|
|
||||||
if ctx.server_task:
|
|
||||||
await ctx.server_task
|
|
||||||
|
|
||||||
while ctx.input_requests > 0:
|
|
||||||
ctx.input_queue.put_nowait(None)
|
|
||||||
ctx.input_requests -= 1
|
|
||||||
|
|
||||||
if ui_task:
|
if ui_task:
|
||||||
await ui_task
|
await ui_task
|
||||||
|
|
27
Generate.py
27
Generate.py
|
@ -90,7 +90,8 @@ def main(args=None, callback=ERmain):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
||||||
meta_weights = weights_cache[args.meta_file_path]
|
meta_weights = weights_cache[args.meta_file_path]
|
||||||
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
|
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||||
|
del(meta_weights["meta_description"])
|
||||||
if args.samesettings:
|
if args.samesettings:
|
||||||
raise Exception("Cannot mix --samesettings with --meta")
|
raise Exception("Cannot mix --samesettings with --meta")
|
||||||
else:
|
else:
|
||||||
|
@ -126,7 +127,7 @@ def main(args=None, callback=ERmain):
|
||||||
erargs.outputname = seed_name
|
erargs.outputname = seed_name
|
||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
|
|
||||||
Utils.init_logging(f"Generate_{seed}.txt", loglevel=args.log_level)
|
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
|
||||||
|
|
||||||
erargs.lttp_rom = args.lttp_rom
|
erargs.lttp_rom = args.lttp_rom
|
||||||
erargs.sm_rom = args.sm_rom
|
erargs.sm_rom = args.sm_rom
|
||||||
|
@ -139,17 +140,17 @@ def main(args=None, callback=ERmain):
|
||||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||||
|
|
||||||
if meta_weights:
|
if meta_weights:
|
||||||
for player, path in player_path_cache.items():
|
for category_name, category_dict in meta_weights.items():
|
||||||
weights_cache[path].setdefault("meta_ignore", [])
|
for key in category_dict:
|
||||||
for key in meta_weights:
|
option = get_choice(key, category_dict)
|
||||||
option = get_choice(key, meta_weights)
|
if option is not None:
|
||||||
if option is not None:
|
for player, path in player_path_cache.items():
|
||||||
for player, path in player_path_cache.items():
|
if category_name is None:
|
||||||
players_meta = weights_cache[path].get("meta_ignore", [])
|
weights_cache[path][key] = option
|
||||||
if key not in players_meta:
|
elif category_name not in weights_cache[path]:
|
||||||
weights_cache[path][key] = option
|
raise Exception(f"Meta: Category {category_name} is not present in {path}.")
|
||||||
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
|
else:
|
||||||
weights_cache[path][key] = option
|
weights_cache[path][category_name][key] = option
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
erargs.player_settings = {}
|
erargs.player_settings = {}
|
||||||
|
|
6
Main.py
6
Main.py
|
@ -197,8 +197,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||||
er_hint_data[region.player][location.address] = main_entrance.name
|
er_hint_data[region.player][location.address] = main_entrance.name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||||
for player in range(1, world.players + 1)}
|
for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
|
@ -215,6 +213,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||||
checks_in_area[location.player][dungeonname].append(location.address)
|
checks_in_area[location.player][dungeonname].append(location.address)
|
||||||
|
elif location.parent_region.type == RegionType.LightWorld:
|
||||||
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
|
elif location.parent_region.type == RegionType.DarkWorld:
|
||||||
|
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||||
checks_in_area[location.player]["Light World"].append(location.address)
|
checks_in_area[location.player]["Light World"].append(location.address)
|
||||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||||
|
|
|
@ -469,7 +469,7 @@ def update_aliases(ctx: Context, team: int):
|
||||||
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
|
||||||
|
|
||||||
|
|
||||||
async def server(websocket, path, ctx: Context):
|
async def server(websocket, path: str = "/", ctx: Context = None):
|
||||||
client = Client(websocket, ctx)
|
client = Client(websocket, ctx)
|
||||||
ctx.endpoints.append(client)
|
ctx.endpoints.append(client)
|
||||||
|
|
||||||
|
@ -591,10 +591,12 @@ def get_status_string(ctx: Context, team: int):
|
||||||
text = "Player Status on your team:"
|
text = "Player Status on your team:"
|
||||||
for slot in ctx.locations:
|
for slot in ctx.locations:
|
||||||
connected = len(ctx.clients[team][slot])
|
connected = len(ctx.clients[team][slot])
|
||||||
|
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
|
||||||
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
||||||
|
death_text = f" {death_link} of which are death link" if connected else ""
|
||||||
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
|
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
|
||||||
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
|
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
|
||||||
f"{goal_text} {completion_text}"
|
f"{death_text}{goal_text} {completion_text}"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@ -652,27 +654,27 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
|
||||||
|
|
||||||
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
|
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
|
||||||
new_locations = set(locations) - ctx.location_checks[team, slot]
|
new_locations = set(locations) - ctx.location_checks[team, slot]
|
||||||
|
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
|
||||||
if new_locations:
|
if new_locations:
|
||||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||||
for location in new_locations:
|
for location in new_locations:
|
||||||
if location in ctx.locations[slot]:
|
item_id, target_player = ctx.locations[slot][location]
|
||||||
item_id, target_player = ctx.locations[slot][location]
|
new_item = NetworkItem(item_id, location, slot)
|
||||||
new_item = NetworkItem(item_id, location, slot)
|
if target_player != slot or slot in ctx.remote_items:
|
||||||
if target_player != slot or slot in ctx.remote_items:
|
get_received_items(ctx, team, target_player).append(new_item)
|
||||||
get_received_items(ctx, team, target_player).append(new_item)
|
|
||||||
|
|
||||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
||||||
info_text = json_format_send_event(new_item, target_player)
|
info_text = json_format_send_event(new_item, target_player)
|
||||||
ctx.broadcast_team(team, [info_text])
|
ctx.broadcast_team(team, [info_text])
|
||||||
|
|
||||||
ctx.location_checks[team, slot] |= new_locations
|
ctx.location_checks[team, slot] |= new_locations
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
ctx.broadcast(ctx.clients[team][slot], [{
|
ctx.broadcast(ctx.clients[team][slot], [{
|
||||||
"cmd": "RoomUpdate",
|
"cmd": "RoomUpdate",
|
||||||
"hint_points": get_slot_points(ctx, team, slot),
|
"hint_points": get_slot_points(ctx, team, slot),
|
||||||
"checked_locations": locations, # duplicated data, but used for coop
|
"checked_locations": new_locations, # send back new checks only
|
||||||
}])
|
}])
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
@ -1242,6 +1244,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
game = ctx.games[slot]
|
game = ctx.games[slot]
|
||||||
if "IgnoreGame" not in args["tags"] and args['game'] != game:
|
if "IgnoreGame" not in args["tags"] and args['game'] != game:
|
||||||
errors.add('InvalidGame')
|
errors.add('InvalidGame')
|
||||||
|
minver = ctx.minimum_client_versions[slot]
|
||||||
|
if minver > args['version']:
|
||||||
|
errors.add('IncompatibleVersion')
|
||||||
|
|
||||||
# only exact version match allowed
|
# only exact version match allowed
|
||||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||||
|
@ -1257,9 +1262,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
client.auth = False # swapping Team/Slot
|
client.auth = False # swapping Team/Slot
|
||||||
client.team = team
|
client.team = team
|
||||||
client.slot = slot
|
client.slot = slot
|
||||||
minver = ctx.minimum_client_versions[slot]
|
|
||||||
if minver > args['version']:
|
|
||||||
errors.add('IncompatibleVersion')
|
|
||||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||||
ctx.clients[team][slot].append(client)
|
ctx.clients[team][slot].append(client)
|
||||||
client.version = args['version']
|
client.version = args['version']
|
||||||
|
@ -1283,8 +1286,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
await ctx.send_msgs(client, reply)
|
await ctx.send_msgs(client, reply)
|
||||||
|
|
||||||
elif cmd == "GetDataPackage":
|
elif cmd == "GetDataPackage":
|
||||||
exclusions = set(args.get("exclusions", []))
|
exclusions = args.get("exclusions", [])
|
||||||
if exclusions:
|
if exclusions:
|
||||||
|
exclusions = set(exclusions)
|
||||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
||||||
if name not in exclusions}
|
if name not in exclusions}
|
||||||
package = network_data_package.copy()
|
package = network_data_package.copy()
|
||||||
|
@ -1680,7 +1684,7 @@ async def main(args: argparse.Namespace):
|
||||||
|
|
||||||
ctx.init_save(not args.disable_save)
|
ctx.init_save(not args.disable_save)
|
||||||
|
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
|
||||||
ping_interval=None)
|
ping_interval=None)
|
||||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||||
|
|
|
@ -379,6 +379,7 @@ class StartHints(ItemSet):
|
||||||
|
|
||||||
|
|
||||||
class StartLocationHints(OptionSet):
|
class StartLocationHints(OptionSet):
|
||||||
|
"""Start with these locations and their item prefilled into the !hint command"""
|
||||||
displayname = "Start Location Hints"
|
displayname = "Start Location Hints"
|
||||||
|
|
||||||
|
|
||||||
|
@ -399,7 +400,7 @@ per_game_common_options = {
|
||||||
"start_inventory": StartInventory,
|
"start_inventory": StartInventory,
|
||||||
"start_hints": StartHints,
|
"start_hints": StartHints,
|
||||||
"start_location_hints": StartLocationHints,
|
"start_location_hints": StartLocationHints,
|
||||||
"exclude_locations": OptionSet
|
"exclude_locations": ExcludeLocations
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
59
Patch.py
59
Patch.py
|
@ -1,3 +1,5 @@
|
||||||
|
# TODO: convert this into a system like AutoWorld
|
||||||
|
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
import yaml
|
import yaml
|
||||||
import os
|
import os
|
||||||
|
@ -14,16 +16,25 @@ current_patch_version = 3
|
||||||
|
|
||||||
GAME_ALTTP = "A Link to the Past"
|
GAME_ALTTP = "A Link to the Past"
|
||||||
GAME_SM = "Super Metroid"
|
GAME_SM = "Super Metroid"
|
||||||
supported_games = {"A Link to the Past", "Super Metroid"}
|
GAME_SOE = "Secret of Evermore"
|
||||||
|
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore"}
|
||||||
|
|
||||||
|
preferred_endings = {
|
||||||
|
GAME_ALTTP: "apbp",
|
||||||
|
GAME_SM: "apm3",
|
||||||
|
GAME_SOE: "apsoe"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||||
if game == GAME_ALTTP:
|
if game == GAME_ALTTP:
|
||||||
from worlds.alttp.Rom import JAP10HASH
|
from worlds.alttp.Rom import JAP10HASH as HASH
|
||||||
elif game == GAME_SM:
|
elif game == GAME_SM:
|
||||||
from worlds.sm.Rom import JAP10HASH
|
from worlds.sm.Rom import JAP10HASH as HASH
|
||||||
|
elif game == GAME_SOE:
|
||||||
|
from worlds.soe.Patch import USHASH as HASH
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Selected game for base rom not found.")
|
raise RuntimeError(f"Selected game {game} for base rom not found.")
|
||||||
|
|
||||||
patch = yaml.dump({"meta": metadata,
|
patch = yaml.dump({"meta": metadata,
|
||||||
"patch": patch,
|
"patch": patch,
|
||||||
|
@ -31,21 +42,14 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAM
|
||||||
# minimum version of patch system expected for patching to be successful
|
# minimum version of patch system expected for patching to be successful
|
||||||
"compatible_version": 3,
|
"compatible_version": 3,
|
||||||
"version": current_patch_version,
|
"version": current_patch_version,
|
||||||
"base_checksum": JAP10HASH})
|
"base_checksum": HASH})
|
||||||
return patch.encode(encoding="utf-8-sig")
|
return patch.encode(encoding="utf-8-sig")
|
||||||
|
|
||||||
|
|
||||||
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
|
||||||
if game == GAME_ALTTP:
|
|
||||||
from worlds.alttp.Rom import get_base_rom_bytes
|
|
||||||
elif game == GAME_SM:
|
|
||||||
from worlds.sm.Rom import get_base_rom_bytes
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Selected game for base rom not found.")
|
|
||||||
|
|
||||||
if metadata is None:
|
if metadata is None:
|
||||||
metadata = {}
|
metadata = {}
|
||||||
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
|
patch = bsdiff4.diff(get_base_rom_data(game), rom)
|
||||||
return generate_yaml(patch, metadata, game)
|
return generate_yaml(patch, metadata, game)
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,27 +70,30 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
|
||||||
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
|
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
|
||||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||||
game_name = data["game"]
|
game_name = data["game"]
|
||||||
if game_name in supported_games:
|
|
||||||
if game_name == GAME_ALTTP:
|
|
||||||
from worlds.alttp.Rom import get_base_rom_bytes
|
|
||||||
elif game_name == GAME_SM:
|
|
||||||
from worlds.sm.Rom import get_base_rom_bytes
|
|
||||||
else:
|
|
||||||
raise Exception(f"No Patch handler for game {game_name}")
|
|
||||||
elif game_name == "alttp": # old version for A Link to the Past
|
|
||||||
from worlds.alttp.Rom import get_base_rom_bytes
|
|
||||||
else:
|
|
||||||
raise Exception(f"Cannot handle game {game_name}")
|
|
||||||
|
|
||||||
if not ignore_version and data["compatible_version"] > current_patch_version:
|
if not ignore_version and data["compatible_version"] > current_patch_version:
|
||||||
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
|
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
|
||||||
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
|
patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"])
|
||||||
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
|
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
|
||||||
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
|
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
|
||||||
target = os.path.splitext(patch_file)[0] + ".sfc"
|
target = os.path.splitext(patch_file)[0] + ".sfc"
|
||||||
return data["meta"], target, patched_data
|
return data["meta"], target, patched_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_rom_data(game: str):
|
||||||
|
if game == GAME_ALTTP:
|
||||||
|
from worlds.alttp.Rom import get_base_rom_bytes
|
||||||
|
elif game == "alttp": # old version for A Link to the Past
|
||||||
|
from worlds.alttp.Rom import get_base_rom_bytes
|
||||||
|
elif game == GAME_SM:
|
||||||
|
from worlds.sm.Rom import get_base_rom_bytes
|
||||||
|
elif game == GAME_SOE:
|
||||||
|
file_name = Utils.get_options()["soe_options"]["rom"]
|
||||||
|
get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb")))
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Selected game for base rom not found.")
|
||||||
|
return get_base_rom_bytes()
|
||||||
|
|
||||||
|
|
||||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||||
data, target, patched_data = create_rom_bytes(patch_file)
|
data, target, patched_data = create_rom_bytes(patch_file)
|
||||||
with open(target, "wb") as f:
|
with open(target, "wb") as f:
|
||||||
|
|
99
SNIClient.py
99
SNIClient.py
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
@ -14,7 +15,7 @@ from json import loads, dumps
|
||||||
from Utils import get_item_name_from_id, init_logging
|
from Utils import get_item_name_from_id, init_logging
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_logging("SNIClient")
|
init_logging("SNIClient", exception_logger="Client")
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.ctx.snes_reconnect_address = None
|
self.ctx.snes_reconnect_address = None
|
||||||
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
|
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), name="SNES Connect")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cmd_snes_close(self) -> bool:
|
def _cmd_snes_close(self) -> bool:
|
||||||
|
@ -84,20 +85,17 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_snes_write(self, address, data):
|
# Left here for quick re-addition for debugging.
|
||||||
"""Write the specified byte (base10) to the SNES' memory address (base16)."""
|
# def _cmd_snes_write(self, address, data):
|
||||||
if self.ctx.snes_state != SNESState.SNES_ATTACHED:
|
# """Write the specified byte (base10) to the SNES' memory address (base16)."""
|
||||||
self.output("No attached SNES Device.")
|
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
|
||||||
return False
|
# self.output("No attached SNES Device.")
|
||||||
|
# return False
|
||||||
snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
|
#
|
||||||
asyncio.create_task(snes_flush_writes(self.ctx))
|
# snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
|
||||||
self.output("Data Sent")
|
# asyncio.create_task(snes_flush_writes(self.ctx))
|
||||||
return True
|
# self.output("Data Sent")
|
||||||
|
# return True
|
||||||
def _cmd_test_death(self):
|
|
||||||
self.ctx.on_deathlink({"source": "Console",
|
|
||||||
"time": time.time()})
|
|
||||||
|
|
||||||
|
|
||||||
class Context(CommonContext):
|
class Context(CommonContext):
|
||||||
|
@ -145,12 +143,7 @@ class Context(CommonContext):
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
self.auth = self.rom
|
self.auth = self.rom
|
||||||
auth = base64.b64encode(self.rom).decode()
|
auth = base64.b64encode(self.rom).decode()
|
||||||
await self.send_msgs([{"cmd": 'Connect',
|
await self.send_connect(name=auth)
|
||||||
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
|
|
||||||
'tags': self.tags,
|
|
||||||
'uuid': Utils.get_unique_identifier(),
|
|
||||||
'game': self.game
|
|
||||||
}])
|
|
||||||
|
|
||||||
def on_deathlink(self, data: dict):
|
def on_deathlink(self, data: dict):
|
||||||
if not self.killing_player_task or self.killing_player_task.done():
|
if not self.killing_player_task or self.killing_player_task.done():
|
||||||
|
@ -896,10 +889,10 @@ async def game_watcher(ctx: Context):
|
||||||
|
|
||||||
if not ctx.rom:
|
if not ctx.rom:
|
||||||
ctx.finished_game = False
|
ctx.finished_game = False
|
||||||
gameName = await snes_read(ctx, SM_ROMNAME_START, 2)
|
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
|
||||||
if gameName is None:
|
if game_name is None:
|
||||||
continue
|
continue
|
||||||
elif gameName == b"SM":
|
elif game_name == b"SM":
|
||||||
ctx.game = GAME_SM
|
ctx.game = GAME_SM
|
||||||
else:
|
else:
|
||||||
ctx.game = GAME_ALTTP
|
ctx.game = GAME_ALTTP
|
||||||
|
@ -912,14 +905,7 @@ async def game_watcher(ctx: Context):
|
||||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
|
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
|
||||||
SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||||
if death_link:
|
if death_link:
|
||||||
death_link = bool(death_link[0] & 0b1)
|
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||||
old_tags = ctx.tags.copy()
|
|
||||||
if death_link:
|
|
||||||
ctx.tags.add("DeathLink")
|
|
||||||
else:
|
|
||||||
ctx.tags -= {"DeathLink"}
|
|
||||||
if old_tags != ctx.tags and ctx.server and not ctx.server.socket.closed:
|
|
||||||
await ctx.send_msgs([{"cmd": "ConnectUpdate", "tags": ctx.tags}])
|
|
||||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||||
ctx.locations_checked = set()
|
ctx.locations_checked = set()
|
||||||
ctx.locations_scouted = set()
|
ctx.locations_scouted = set()
|
||||||
|
@ -1083,14 +1069,24 @@ async def main():
|
||||||
meta, romfile = Patch.create_rom_file(args.diff_file)
|
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||||
args.connect = meta["server"]
|
args.connect = meta["server"]
|
||||||
logging.info(f"Wrote rom file to {romfile}")
|
logging.info(f"Wrote rom file to {romfile}")
|
||||||
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
|
if args.diff_file.endswith(".apsoe"):
|
||||||
if adjusted:
|
import webbrowser
|
||||||
try:
|
webbrowser.open("http://www.evermizer.com/apclient/")
|
||||||
shutil.move(adjustedromfile, romfile)
|
logging.info("Starting Evermizer Client in your Browser...")
|
||||||
adjustedromfile = romfile
|
import time
|
||||||
except Exception as e:
|
time.sleep(3)
|
||||||
logging.exception(e)
|
sys.exit()
|
||||||
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
elif args.diff_file.endswith((".apbp", "apz3")):
|
||||||
|
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
|
||||||
|
if adjusted:
|
||||||
|
try:
|
||||||
|
shutil.move(adjustedromfile, romfile)
|
||||||
|
adjustedromfile = romfile
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
|
||||||
|
else:
|
||||||
|
asyncio.create_task(run_game(romfile))
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password)
|
ctx = Context(args.snes, args.connect, args.password)
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
|
@ -1105,28 +1101,19 @@ async def main():
|
||||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||||
ui_task = None
|
ui_task = None
|
||||||
|
|
||||||
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address))
|
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address), name="SNES Connect")
|
||||||
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
if snes_connect_task:
|
|
||||||
snes_connect_task.cancel()
|
|
||||||
ctx.server_address = None
|
ctx.server_address = None
|
||||||
ctx.snes_reconnect_address = None
|
ctx.snes_reconnect_address = None
|
||||||
|
|
||||||
await watcher_task
|
|
||||||
|
|
||||||
if ctx.server and not ctx.server.socket.closed:
|
|
||||||
await ctx.server.socket.close()
|
|
||||||
if ctx.server_task:
|
|
||||||
await ctx.server_task
|
|
||||||
|
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
|
if snes_connect_task:
|
||||||
while ctx.input_requests > 0:
|
snes_connect_task.cancel()
|
||||||
ctx.input_queue.put_nowait(None)
|
await watcher_task
|
||||||
ctx.input_requests -= 1
|
await ctx.shutdown()
|
||||||
|
|
||||||
if ui_task:
|
if ui_task:
|
||||||
await ui_task
|
await ui_task
|
||||||
|
|
37
Utils.py
37
Utils.py
|
@ -122,16 +122,25 @@ parse_yaml = safe_load
|
||||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cert_none_ssl_context():
|
||||||
|
import ssl
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
|
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
|
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
|
||||||
except:
|
except:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
|
@ -143,8 +152,9 @@ def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
|
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
|
@ -166,6 +176,9 @@ def get_default_options() -> dict:
|
||||||
"sni": "SNI",
|
"sni": "SNI",
|
||||||
"rom_start": True,
|
"rom_start": True,
|
||||||
},
|
},
|
||||||
|
"soe_options": {
|
||||||
|
"rom_file": "Secret of Evermore (USA).sfc",
|
||||||
|
},
|
||||||
"lttp_options": {
|
"lttp_options": {
|
||||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||||
"sni": "SNI",
|
"sni": "SNI",
|
||||||
|
@ -414,7 +427,7 @@ loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': log
|
||||||
|
|
||||||
|
|
||||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
||||||
log_format: str = "[%(name)s]: %(message)s"):
|
log_format: str = "[%(name)s]: %(message)s", exception_logger: str = ""):
|
||||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||||
log_folder = local_path("logs")
|
log_folder = local_path("logs")
|
||||||
os.makedirs(log_folder, exist_ok=True)
|
os.makedirs(log_folder, exist_ok=True)
|
||||||
|
@ -433,3 +446,19 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||||
root_logger.addHandler(
|
root_logger.addHandler(
|
||||||
logging.StreamHandler(sys.stdout)
|
logging.StreamHandler(sys.stdout)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Relay unhandled exceptions to logger.
|
||||||
|
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
|
||||||
|
orig_hook = sys.excepthook
|
||||||
|
|
||||||
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||||
|
if issubclass(exc_type, KeyboardInterrupt):
|
||||||
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||||
|
return
|
||||||
|
logging.getLogger(exception_logger).exception("Uncaught exception",
|
||||||
|
exc_info=(exc_type, exc_value, exc_traceback))
|
||||||
|
return orig_hook(exc_type, exc_value, exc_traceback)
|
||||||
|
|
||||||
|
handle_exception._wrapped = True
|
||||||
|
|
||||||
|
sys.excepthook = handle_exception
|
||||||
|
|
|
@ -141,7 +141,7 @@ def new_room(seed: UUID):
|
||||||
abort(404)
|
abort(404)
|
||||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||||
commit()
|
commit()
|
||||||
return redirect(url_for("hostRoom", room=room.id))
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
def _read_log(path: str):
|
def _read_log(path: str):
|
||||||
|
@ -159,7 +159,7 @@ def display_log(room: UUID):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||||
def hostRoom(room: UUID):
|
def host_room(room: UUID):
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
@ -175,20 +175,17 @@ def hostRoom(room: UUID):
|
||||||
return render_template("hostRoom.html", room=room)
|
return render_template("hostRoom.html", room=room)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
|
|
||||||
def hostRoomRedirect(room: UUID):
|
|
||||||
return redirect(url_for("hostRoom", room=room))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/discord')
|
@app.route('/discord')
|
||||||
def discord():
|
def discord():
|
||||||
return redirect("https://discord.gg/archipelago")
|
return redirect("https://discord.gg/archipelago")
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
from WebHostLib.customserver import run_server_process
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ from pony.orm import commit
|
||||||
|
|
||||||
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
|
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
|
||||||
from WebHostLib.check import get_yaml_data, roll_options
|
from WebHostLib.check import get_yaml_data, roll_options
|
||||||
|
from WebHostLib.generate import get_meta
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/generate', methods=['POST'])
|
@api_endpoints.route('/generate', methods=['POST'])
|
||||||
|
@ -35,9 +36,6 @@ def generate_api():
|
||||||
if "race" in json_data:
|
if "race" in json_data:
|
||||||
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
|
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
|
||||||
|
|
||||||
hint_cost = int(meta_options_source.get("hint_cost", 10))
|
|
||||||
forfeit_mode = meta_options_source.get("forfeit_mode", "goal")
|
|
||||||
|
|
||||||
if not options:
|
if not options:
|
||||||
return {"text": "No options found. Expected file attachment or json weights."
|
return {"text": "No options found. Expected file attachment or json weights."
|
||||||
}, 400
|
}, 400
|
||||||
|
@ -45,7 +43,8 @@ def generate_api():
|
||||||
if len(options) > app.config["MAX_ROLL"]:
|
if len(options) > app.config["MAX_ROLL"]:
|
||||||
return {"text": "Max size of multiworld exceeded",
|
return {"text": "Max size of multiworld exceeded",
|
||||||
"detail": app.config["MAX_ROLL"]}, 409
|
"detail": app.config["MAX_ROLL"]}, 409
|
||||||
|
meta = get_meta(meta_options_source)
|
||||||
|
meta["race"] = race
|
||||||
results, gen_options = roll_options(options)
|
results, gen_options = roll_options(options)
|
||||||
if any(type(result) == str for result in results.values()):
|
if any(type(result) == str for result in results.values()):
|
||||||
return {"text": str(results),
|
return {"text": str(results),
|
||||||
|
@ -54,7 +53,7 @@ def generate_api():
|
||||||
gen = Generation(
|
gen = Generation(
|
||||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||||
# convert to json compatible
|
# convert to json compatible
|
||||||
meta=json.dumps({"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}), state=STATE_QUEUED,
|
meta=json.dumps(meta), state=STATE_QUEUED,
|
||||||
owner=session["_id"])
|
owner=session["_id"])
|
||||||
commit()
|
commit()
|
||||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from flask import send_file, Response, render_template
|
from flask import send_file, Response, render_template
|
||||||
from pony.orm import select
|
from pony.orm import select
|
||||||
|
|
||||||
from Patch import update_patch_data
|
from Patch import update_patch_data, preferred_endings
|
||||||
from WebHostLib import app, Slot, Room, Seed, cache
|
from WebHostLib import app, Slot, Room, Seed, cache
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||||
def download_patch(room_id, patch_id):
|
def download_patch(room_id, patch_id):
|
||||||
patch = Slot.get(id=patch_id)
|
patch = Slot.get(id=patch_id)
|
||||||
|
@ -19,7 +20,8 @@ def download_patch(room_id, patch_id):
|
||||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||||
patch_data = io.BytesIO(patch_data)
|
patch_data = io.BytesIO(patch_data)
|
||||||
|
|
||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
|
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||||
|
f"{preferred_endings[patch.game]}"
|
||||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,23 +30,6 @@ def download_spoiler(seed_id):
|
||||||
return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain")
|
return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
|
|
||||||
def download_raw_patch(seed_id, player_id: int):
|
|
||||||
seed = Seed.get(id=seed_id)
|
|
||||||
patch = select(patch for patch in seed.slots if
|
|
||||||
patch.player_id == player_id).first()
|
|
||||||
|
|
||||||
if not patch:
|
|
||||||
return "Patch not found"
|
|
||||||
else:
|
|
||||||
import io
|
|
||||||
|
|
||||||
patch_data = update_patch_data(patch.data, server="")
|
|
||||||
patch_data = io.BytesIO(patch_data)
|
|
||||||
|
|
||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
|
|
||||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
|
||||||
|
|
||||||
@app.route("/slot_file/<suuid:room_id>/<int:player_id>")
|
@app.route("/slot_file/<suuid:room_id>/<int:player_id>")
|
||||||
def download_slot_file(room_id, player_id: int):
|
def download_slot_file(room_id, player_id: int):
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
|
|
|
@ -20,6 +20,16 @@ from .check import get_yaml_data, roll_options
|
||||||
from .upload import upload_zip_to_db
|
from .upload import upload_zip_to_db
|
||||||
|
|
||||||
|
|
||||||
|
def get_meta(options_source: dict) -> dict:
|
||||||
|
meta = {
|
||||||
|
"hint_cost": int(options_source.get("hint_cost", 10)),
|
||||||
|
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
|
||||||
|
"remaining_mode": options_source.get("forfeit_mode", "disabled"),
|
||||||
|
"collect_mode": options_source.get("collect_mode", "disabled"),
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generate', methods=['GET', 'POST'])
|
@app.route('/generate', methods=['GET', 'POST'])
|
||||||
@app.route('/generate/<race>', methods=['GET', 'POST'])
|
@app.route('/generate/<race>', methods=['GET', 'POST'])
|
||||||
def generate(race=False):
|
def generate(race=False):
|
||||||
|
@ -35,9 +45,9 @@ def generate(race=False):
|
||||||
else:
|
else:
|
||||||
results, gen_options = roll_options(options)
|
results, gen_options = roll_options(options)
|
||||||
# get form data -> server settings
|
# get form data -> server settings
|
||||||
hint_cost = int(request.form.get("hint_cost", 10))
|
meta = get_meta(request.form)
|
||||||
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
meta["race"] = race
|
||||||
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
|
||||||
if race:
|
if race:
|
||||||
meta["item_cheat"] = False
|
meta["item_cheat"] = False
|
||||||
meta["remaining"] = False
|
meta["remaining"] = False
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Seed(db.Entity):
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||||
slots = Set(Slot)
|
slots = Set(Slot)
|
||||||
spoiler = Optional(LongStr, lazy=True)
|
spoiler = Optional(LongStr, lazy=True)
|
||||||
meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags
|
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
|
||||||
|
|
||||||
|
|
||||||
class Command(db.Entity):
|
class Command(db.Entity):
|
||||||
|
@ -53,5 +53,5 @@ class Generation(db.Entity):
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
owner = Required(UUID)
|
owner = Required(UUID)
|
||||||
options = Required(buffer, lazy=True)
|
options = Required(buffer, lazy=True)
|
||||||
meta = Required(str, default=lambda: "{\"race\": false}")
|
meta = Required(LongStr, default=lambda: "{\"race\": false}")
|
||||||
state = Required(int, default=0, index=True)
|
state = Required(int, default=0, index=True)
|
||||||
|
|
|
@ -49,7 +49,7 @@ def create():
|
||||||
game_options = {}
|
game_options = {}
|
||||||
for option_name, option in world.options.items():
|
for option_name, option in world.options.items():
|
||||||
if option.options:
|
if option.options:
|
||||||
this_option = {
|
game_options[option_name] = this_option = {
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||||
|
@ -66,7 +66,10 @@ def create():
|
||||||
if sub_option_id == option.default:
|
if sub_option_id == option.default:
|
||||||
this_option["defaultValue"] = sub_option_name
|
this_option["defaultValue"] = sub_option_name
|
||||||
|
|
||||||
game_options[option_name] = this_option
|
this_option["options"].append({
|
||||||
|
"name": "Random",
|
||||||
|
"value": "random",
|
||||||
|
})
|
||||||
|
|
||||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Advanced Game Options Guide
|
||||||
|
|
||||||
|
|
||||||
|
The Archipelago system generates games using player configuration files as input. Generally these are going to be
|
||||||
|
YAML files and each player will have one of these containing their custom settings for the randomized game they want to play.
|
||||||
|
On the website when you customize your settings from one of the game player settings pages which you can reach from the
|
||||||
|
[supported games page](/games). Clicking on the export settings button at the bottom will provide you with a pre-filled out
|
||||||
|
YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every
|
||||||
|
option with every available setting for the available options.
|
||||||
|
|
||||||
|
## YAML Formatting
|
||||||
|
YAML files are a format of <span data-tooltip="Allegedly.">human-readable</span> markup config files. The basic syntax
|
||||||
|
of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine
|
||||||
|
your settings. To nest text, the correct syntax is **two spaces over** from its root option. A YAML file can be edited
|
||||||
|
with whatever text editor you choose to use though I personally recommend that you use [Sublime Text](https://www.sublimetext.com/).
|
||||||
|
This program out of the box supports the correct formatting for the YAML file, so you will be able to tab and get proper
|
||||||
|
highlighting for any potential errors made while editing the file. If using any other text editor such as Notepad or
|
||||||
|
Notepad++ whenever you move to nest an option that it is done with two spaces and not tabs.
|
||||||
|
|
||||||
|
Typical YAML format will look as follows:
|
||||||
|
```yaml
|
||||||
|
root_option:
|
||||||
|
nested_option_one:
|
||||||
|
option_one_setting_one: 1
|
||||||
|
option_one_setting_two: 0
|
||||||
|
nested_option_two:
|
||||||
|
option_two_setting_one: 14
|
||||||
|
option_two_setting_two: 43
|
||||||
|
```
|
||||||
|
|
||||||
|
In Archipelago YAML options are always written out in full lowercase with underscores separating any words. The numbers
|
||||||
|
following the colons here are weights. The generator will read the weight of every option the roll that option that many
|
||||||
|
times, the next option as many times as its numbered and so forth. For the above example `nested_option_one` will have
|
||||||
|
`option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur.
|
||||||
|
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
|
||||||
|
times against each other. This means `option_two_setting_two` will be more likely to occur but it isn't guaranteed adding
|
||||||
|
more randomness and "mystery" to your settings. Every configurable setting supports weights.
|
||||||
|
|
||||||
|
### Root Options
|
||||||
|
Currently there are only a few options that are root options. Everything else should be nested within one of these root
|
||||||
|
options or in some cases nested within other nested options. The only options that should exist in root are `description`,
|
||||||
|
`name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want
|
||||||
|
settings for.
|
||||||
|
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using
|
||||||
|
this to detail the intention of the file.
|
||||||
|
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also
|
||||||
|
be filled with multiple names each having a weight to it.
|
||||||
|
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different
|
||||||
|
weights.
|
||||||
|
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
|
||||||
|
is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be
|
||||||
|
missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure
|
||||||
|
it will be used is good practice.
|
||||||
|
* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your
|
||||||
|
completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default.
|
||||||
|
* `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly
|
||||||
|
comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas
|
||||||
|
that make them unreachable.
|
||||||
|
* `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but
|
||||||
|
may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest
|
||||||
|
in a dungeon in ALTTP making it impossible to get and finish the dungeon.
|
||||||
|
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This
|
||||||
|
primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that
|
||||||
|
players almost always have something to do. This can be turned `on` or `off` and is `on` by default.
|
||||||
|
* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more
|
||||||
|
about this [here](/tutorial/archipelago/triggers/en).
|
||||||
|
|
||||||
|
### Game Options
|
||||||
|
|
||||||
|
One of your root settings will be the name of the game you would like to populate with settings in the format
|
||||||
|
`GameName`. since it is possible to give a weight to any option it is possible to have one file that can generate a seed
|
||||||
|
for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game
|
||||||
|
that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty.
|
||||||
|
|
||||||
|
#### Universal Game Options
|
||||||
|
|
||||||
|
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||||
|
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`,
|
||||||
|
`exclude_locations`, and various [plando options](tutorial/archipelago/plando/en).
|
||||||
|
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
||||||
|
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
||||||
|
will give you 30 rupees.
|
||||||
|
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
|
||||||
|
the location without using any hint points.
|
||||||
|
* `local_items` will force any items you want to be in your world instead of being in another world.
|
||||||
|
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located
|
||||||
|
in your own.
|
||||||
|
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in
|
||||||
|
it to see how important the location is.
|
||||||
|
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
|
||||||
|
item which isn't necessary for progression to go in these locations.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
description: An example using various advanced options
|
||||||
|
name: Example Player
|
||||||
|
game: A Link to the Past
|
||||||
|
requires:
|
||||||
|
version: 0.2.0
|
||||||
|
accessibility: none
|
||||||
|
progression_balancing: on
|
||||||
|
A Link to the Past:
|
||||||
|
smallkey_shuffle:
|
||||||
|
original_dungeon: 1
|
||||||
|
any_world: 1
|
||||||
|
start_inventory:
|
||||||
|
Pegasus Boots: 1
|
||||||
|
Bombs (3): 2
|
||||||
|
start_hints:
|
||||||
|
- Hammer
|
||||||
|
local_items:
|
||||||
|
- Bombos
|
||||||
|
- Ether
|
||||||
|
- Quake
|
||||||
|
non_local_items:
|
||||||
|
- Moon Pearl
|
||||||
|
start_location_hints:
|
||||||
|
- Spike Cave
|
||||||
|
exclude_locations:
|
||||||
|
- Cave 45
|
||||||
|
triggers:
|
||||||
|
- option_category: A Link to the Past
|
||||||
|
option_name: smallkey_shuffle
|
||||||
|
option_result: any_world
|
||||||
|
options:
|
||||||
|
A Link to the Past:
|
||||||
|
bigkey_shuffle: any_world
|
||||||
|
map_shuffle: any_world
|
||||||
|
compass_shuffle: any_world
|
||||||
|
```
|
||||||
|
|
||||||
|
#### This is a fully functional yaml file that will do all the following things:
|
||||||
|
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
|
||||||
|
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
|
||||||
|
* `game` is set to `A Link to the Past` meaning that is what game we will play with this file.
|
||||||
|
* `requires` is set to require release version 0.2.0 or higher.
|
||||||
|
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
|
||||||
|
completely inaccessible but the seed will still be completable.
|
||||||
|
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having
|
||||||
|
things to do.
|
||||||
|
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our game `A Link to the Past`.
|
||||||
|
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example
|
||||||
|
we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere
|
||||||
|
amongst the multiworld.
|
||||||
|
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example
|
||||||
|
we have:
|
||||||
|
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
|
||||||
|
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
|
||||||
|
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use with no cost.
|
||||||
|
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||||
|
have to find it ourselves.
|
||||||
|
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||||
|
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld
|
||||||
|
that can be used for no cost.
|
||||||
|
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
|
||||||
|
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
|
||||||
|
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world`
|
||||||
|
result.
|
|
@ -12,6 +12,17 @@ game/games you plan to play are available here go ahead and install these as wel
|
||||||
supported by Archipelago but not listed in the installation check the relevant tutorial.
|
supported by Archipelago but not listed in the installation check the relevant tutorial.
|
||||||
|
|
||||||
## Generating a game
|
## Generating a game
|
||||||
|
|
||||||
|
### Creating a YAML
|
||||||
|
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
|
||||||
|
native coop system or using archipelago's coop support. Each world will hold one slot in the multiworld and will have a
|
||||||
|
slot name and, if the relevant game requires it, files to associate it with that multiworld. If multiple people plan to
|
||||||
|
play in one world cooperatively then they will only need one YAML for their coop world, but if each player is planning on
|
||||||
|
playing their own game then they will each need a YAML. These YAML files can be generated by going to the relevant game's
|
||||||
|
player settings page, entering the name they want to use for the game, setting the options to what they would like to
|
||||||
|
play with and then clicking on the export settings button. This will then download a YAML file that will contain all of
|
||||||
|
these options and this can then be given to whoever is going to generate the game.
|
||||||
|
|
||||||
### Gather all player YAMLS
|
### Gather all player YAMLS
|
||||||
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they wish to play with.
|
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they wish to play with.
|
||||||
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
|
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
|
||||||
|
@ -51,6 +62,7 @@ The generator will put a zip folder into your `Archipelago\output` folder with t
|
||||||
This contains the patch files and relevant mods for the players as well as the serverdata for the host.
|
This contains the patch files and relevant mods for the players as well as the serverdata for the host.
|
||||||
|
|
||||||
## Hosting a multiworld
|
## Hosting a multiworld
|
||||||
|
|
||||||
### Uploading the seed to the website
|
### Uploading the seed to the website
|
||||||
The easiest and most recommended method is to generate the game on the website which will allow you to create a private
|
The easiest and most recommended method is to generate the game on the website which will allow you to create a private
|
||||||
room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various games.
|
room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various games.
|
||||||
|
|
|
@ -107,6 +107,9 @@ Archipelago if you chose to include it during the installation process.
|
||||||
10. Enter `localhost` into the server address box
|
10. Enter `localhost` into the server address box
|
||||||
11. Click "Connect"
|
11. Click "Connect"
|
||||||
|
|
||||||
|
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP
|
||||||
|
server, you can also issue the `!help` command to learn about additional commands like `!hint`.
|
||||||
|
|
||||||
## Allowing Other People to Join Your Game
|
## Allowing Other People to Join Your Game
|
||||||
1. Ensure your Archipelago Client is running.
|
1. Ensure your Archipelago Client is running.
|
||||||
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
|
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"tutorials": [
|
"tutorials": [
|
||||||
{
|
{
|
||||||
"name": "Multiworld Setup Tutorial",
|
"name": "Multiworld Setup Tutorial",
|
||||||
"description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.",
|
"description": "A guide to setting up the Archipelago software to generate and host multiworld games on your computer and using the website.",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"language": "English",
|
"language": "English",
|
||||||
|
@ -16,9 +16,23 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Using Advanced Settings",
|
||||||
|
"description": "A guide to reading yaml files and editing them to fully customize your game.",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "archipelago/advanced_settings_en.md",
|
||||||
|
"link": "archipelago/advanced_settings/en",
|
||||||
|
"authors": [
|
||||||
|
"alwaysintreble"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Archipelago Triggers Guide",
|
"name": "Archipelago Triggers Guide",
|
||||||
"description": "A Guide to setting up and using triggers in your game settings.",
|
"description": "A guide to setting up and using triggers in your game settings.",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"language": "English",
|
"language": "English",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# A Link to the Past Randomizer Setup Guide
|
# A Link to the Past Randomizer Setup Guide
|
||||||
|
|
||||||
## Required Software
|
## Required Software
|
||||||
- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the LttPClient included with
|
- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with
|
||||||
[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
- If installing Archipelago, make sure to check the box for LttPClient during install, or SNI will not be included
|
- If installing Archipelago, make sure to check the box for SNIClient -> A Link to the Past Patch Setup during install, or SNI will not be included
|
||||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and LttPClient)
|
- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and SNIClient)
|
||||||
- Hardware or software capable of loading and playing SNES ROM files
|
- Hardware or software capable of loading and playing SNES ROM files
|
||||||
- An emulator capable of connecting to SNI
|
- An emulator capable of connecting to SNI
|
||||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||||
|
@ -76,7 +76,7 @@ Firewall.
|
||||||
4. In the new window, click **Browse...**
|
4. In the new window, click **Browse...**
|
||||||
5. Select the connector lua file included with your client
|
5. Select the connector lua file included with your client
|
||||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||||
- LttPClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
||||||
|
|
||||||
##### BizHawk
|
##### BizHawk
|
||||||
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
|
||||||
|
@ -88,7 +88,7 @@ Firewall.
|
||||||
4. Click the button to open a new Lua script.
|
4. Click the button to open a new Lua script.
|
||||||
5. Select the `sniConnector.lua` file you downloaded above
|
5. Select the `sniConnector.lua` file you downloaded above
|
||||||
- Z3Client users should download `sniConnector.lua` from the client download page
|
- Z3Client users should download `sniConnector.lua` from the client download page
|
||||||
- LttPClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
|
||||||
|
|
||||||
#### With hardware
|
#### With hardware
|
||||||
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
This guide assumes you have downloaded the correct firmware for your device. If you have not
|
||||||
|
|
|
@ -28,7 +28,7 @@ can all have different options.
|
||||||
|
|
||||||
### Where do I get a YAML file?
|
### Where do I get a YAML file?
|
||||||
|
|
||||||
A basic OOT yaml will look like this. (There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] and look for the sample file in the "Players" folder))
|
A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in the "Players" folder.
|
||||||
```yaml
|
```yaml
|
||||||
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
|
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
|
||||||
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||||
|
|
|
@ -34,7 +34,14 @@
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
|
<td>
|
||||||
|
<label for="forfeit_mode">Forfeit Permission:</label>
|
||||||
|
<span
|
||||||
|
class="interactive"
|
||||||
|
data-tooltip="A forfeit releases all remaining items from the locations
|
||||||
|
in your world.">(?)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select name="forfeit_mode" id="forfeit_mode">
|
<select name="forfeit_mode" id="forfeit_mode">
|
||||||
<option value="auto">Automatic on goal completion</option>
|
<option value="auto">Automatic on goal completion</option>
|
||||||
|
@ -46,12 +53,49 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="collect_mode">Collect Permission:</label>
|
||||||
|
<span
|
||||||
|
class="interactive"
|
||||||
|
data-tooltip="A collect releases all of your remaining items to you
|
||||||
|
from across the multiworld.">(?)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="collect_mode" id="collect_mode">
|
||||||
|
<option value="goal">Allow !collect after goal completion</option>
|
||||||
|
<option value="auto">Automatic on goal completion</option>
|
||||||
|
<option value="auto-enabled">Automatic on goal completion and manual !collect</option>
|
||||||
|
<option value="enabled">Manual !collect</option>
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<label for="remaining_mode">Remaining Permission:</label>
|
||||||
|
<span
|
||||||
|
class="interactive"
|
||||||
|
data-tooltip="Remaining lists all items still in your world by name only.">(?)
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="remaining_mode" id="remaining_mode">
|
||||||
|
<option value="disabled">Disabled</option>
|
||||||
|
<option value="goal">Allow !remaining after goal completion</option>
|
||||||
|
<option value="enabled">Manual !remaining</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<label for="hint_cost"> Hint Cost:</label>
|
<label for="hint_cost"> Hint Cost:</label>
|
||||||
<span
|
<span
|
||||||
class="interactive"
|
class="interactive"
|
||||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||||
to get the location of that hint item.">(?)
|
to get the location of that hint item.">(?)
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
later,
|
later,
|
||||||
you can simply refresh this page and the server will be started again.<br>
|
you can simply refresh this page and the server will be started again.<br>
|
||||||
{% if room.last_port %}
|
{% if room.last_port %}
|
||||||
You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}'
|
You can connect to this room by using '/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}'
|
||||||
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>{% endif %}
|
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>{% endif %}
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
|
|
|
@ -28,34 +28,16 @@
|
||||||
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
|
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if seed.multidata %}
|
|
||||||
<tr>
|
|
||||||
<td>Rooms: </td>
|
|
||||||
<td>
|
|
||||||
{% call macros.list_rooms(rooms) %}
|
|
||||||
<li>
|
|
||||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
|
||||||
</li>
|
|
||||||
{% endcall %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Files: </td>
|
<td>Rooms: </td>
|
||||||
<td>
|
<td>
|
||||||
<ul>
|
{% call macros.list_rooms(rooms) %}
|
||||||
{% for slot in seed.slots %}
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=slot.player_id) }}">Player {{ slot.player_name }}</a>
|
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,10 +10,7 @@ from pony.orm import flush, select
|
||||||
|
|
||||||
from WebHostLib import app, Seed, Room, Slot
|
from WebHostLib import app, Seed, Room, Slot
|
||||||
from Utils import parse_yaml
|
from Utils import parse_yaml
|
||||||
|
from Patch import preferred_endings
|
||||||
accepted_zip_contents = {"patches": ".apbp",
|
|
||||||
"spoiler": ".txt",
|
|
||||||
"multidata": ".archipelago"}
|
|
||||||
|
|
||||||
banned_zip_contents = (".sfc",)
|
banned_zip_contents = (".sfc",)
|
||||||
|
|
||||||
|
@ -29,15 +26,17 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||||
if file.filename.endswith(banned_zip_contents):
|
if file.filename.endswith(banned_zip_contents):
|
||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||||
"Your file was deleted."
|
"Your file was deleted."
|
||||||
elif file.filename.endswith(".apbp"):
|
elif file.filename.endswith(tuple(preferred_endings.values())):
|
||||||
data = zfile.open(file, "r").read()
|
data = zfile.open(file, "r").read()
|
||||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||||
if yaml_data["version"] < 2:
|
if yaml_data["version"] < 2:
|
||||||
return "Old format cannot be uploaded (outdated .apbp)", 500
|
return "Old format cannot be uploaded (outdated .apbp)"
|
||||||
metadata = yaml_data["meta"]
|
metadata = yaml_data["meta"]
|
||||||
slots.add(Slot(data=data, player_name=metadata["player_name"],
|
|
||||||
|
slots.add(Slot(data=data,
|
||||||
|
player_name=metadata["player_name"],
|
||||||
player_id=metadata["player_id"],
|
player_id=metadata["player_id"],
|
||||||
game="A Link to the Past"))
|
game=yaml_data["game"]))
|
||||||
|
|
||||||
elif file.filename.endswith(".apmc"):
|
elif file.filename.endswith(".apmc"):
|
||||||
data = zfile.open(file, "r").read()
|
data = zfile.open(file, "r").read()
|
||||||
|
@ -66,8 +65,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||||
MultiServer.Context._decompress(multidata)
|
MultiServer.Context._decompress(multidata)
|
||||||
except:
|
except:
|
||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
else:
|
multidata = None
|
||||||
multidata = zfile.open(file).read()
|
|
||||||
if multidata:
|
if multidata:
|
||||||
flush() # commit slots
|
flush() # commit slots
|
||||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<TabbedPanel>
|
<TabbedPanel>
|
||||||
tab_width: 200
|
tab_width: 200
|
||||||
<Row@Label>:
|
<SelectableLabel>:
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: 0.2, 0.2, 0.2, 1
|
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
|
||||||
Rectangle:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
|
@ -13,10 +13,10 @@
|
||||||
font_size: dp(20)
|
font_size: dp(20)
|
||||||
markup: True
|
markup: True
|
||||||
<UILog>:
|
<UILog>:
|
||||||
viewclass: 'Row'
|
viewclass: 'SelectableLabel'
|
||||||
scroll_y: 0
|
scroll_y: 0
|
||||||
effect_cls: "ScrollEffect"
|
effect_cls: "ScrollEffect"
|
||||||
RecycleBoxLayout:
|
SelectableRecycleBoxLayout:
|
||||||
default_size: None, dp(20)
|
default_size: None, dp(20)
|
||||||
default_size_hint: 1, None
|
default_size_hint: 1, None
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
|
|
|
@ -140,7 +140,7 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
|
||||||
| ---- | ---- | ----- |
|
| ---- | ---- | ----- |
|
||||||
| hint_points | int | New argument. The client's current hint points. |
|
| hint_points | int | New argument. The client's current hint points. |
|
||||||
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. |
|
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. |
|
||||||
| checked_locations | May be a partial update, containing new locations that were checked. |
|
| checked_locations | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
|
||||||
| missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
| missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. |
|
||||||
|
|
||||||
All arguments for this packet are optional, only changes are sent.
|
All arguments for this packet are optional, only changes are sent.
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# This is a sample configuration for the Web host.
|
||||||
|
# If you wish to change any of these, rename this file to config.yaml
|
||||||
|
# Default values are shown here. Uncomment and change the values as desired.
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#SELFHOST: true
|
||||||
|
|
||||||
|
# Maximum concurrent world gens
|
||||||
|
#GENERATORS: 8
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#SELFLAUNCH: true
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#DEBUG: false
|
||||||
|
|
||||||
|
# Web hosting port
|
||||||
|
#PORT: 80
|
||||||
|
|
||||||
|
# Place where uploads go.
|
||||||
|
#UPLOAD_FOLDER: uploads
|
||||||
|
|
||||||
|
# Maximum upload size. Default is 64 megabyte (64 * 1024 * 1024)
|
||||||
|
#MAX_CONTENT_LENGTH: 67108864
|
||||||
|
|
||||||
|
# Secret key used to determine important things like cookie authentication of room/seed page ownership.
|
||||||
|
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
|
||||||
|
# SECRET_KEY: "Your secret key here"
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#JOB_THRESHOLD: 2
|
||||||
|
|
||||||
|
# waitress uses one thread for I/O, these are for processing of view that get sent
|
||||||
|
#WAITRESS_THREADS: 10
|
||||||
|
|
||||||
|
# Database provider details:
|
||||||
|
#PONY:
|
||||||
|
# provider: "sqlite"
|
||||||
|
# filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file.
|
||||||
|
# create_db: true
|
||||||
|
|
||||||
|
# Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results.
|
||||||
|
#MAX_ROLL: 20
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#CACHE_TYPE: "simple"
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
#JSON_AS_ASCII: false
|
||||||
|
|
||||||
|
# Patch target. This is the address encoded into the patch that will be used for client auto-connect.
|
||||||
|
#PATCH_TARGET: archipelago.gg
|
|
@ -48,21 +48,21 @@ Name: "playing"; Description: "Installation for playing purposes"
|
||||||
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||||
|
|
||||||
[Components]
|
[Components]
|
||||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||||
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
|
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
||||||
Name: "server"; Description: "Server"; Types: full hosting
|
Name: "server"; Description: "Server"; Types: full hosting
|
||||||
Name: "client"; Description: "Clients"; Types: full playing
|
Name: "client"; Description: "Clients"; Types: full playing
|
||||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||||
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing
|
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
|
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
||||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||||
|
@ -136,7 +136,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{ap
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
const
|
const
|
||||||
SHCONTCH_NOPROGRESSBOX = 4;
|
SHCONTCH_NOPROGRESSBOX = 4;
|
||||||
|
@ -221,6 +220,19 @@ var OoTROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
var MinecraftDownloadPage: TDownloadWizardPage;
|
var MinecraftDownloadPage: TDownloadWizardPage;
|
||||||
|
|
||||||
|
function GetSNESMD5OfFile(const rom: string): string;
|
||||||
|
var data: AnsiString;
|
||||||
|
begin
|
||||||
|
if LoadStringFromFile(rom, data) then
|
||||||
|
begin
|
||||||
|
if Length(data) mod 1024 = 512 then
|
||||||
|
begin
|
||||||
|
data := copy(data, 513, Length(data)-512);
|
||||||
|
end;
|
||||||
|
Result := GetMD5OfString(data);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function CheckRom(name: string; hash: string): string;
|
function CheckRom(name: string; hash: string): string;
|
||||||
var rom: string;
|
var rom: string;
|
||||||
begin
|
begin
|
||||||
|
@ -229,8 +241,8 @@ begin
|
||||||
if Length(rom) > 0 then
|
if Length(rom) > 0 then
|
||||||
begin
|
begin
|
||||||
log('existing ROM found');
|
log('existing ROM found');
|
||||||
log(IntToStr(CompareStr(GetMD5OfFile(rom), hash)));
|
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
|
||||||
if CompareStr(GetMD5OfFile(rom), hash) = 0 then
|
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
|
||||||
begin
|
begin
|
||||||
log('existing ROM verified');
|
log('existing ROM verified');
|
||||||
Result := rom;
|
Result := rom;
|
||||||
|
@ -317,7 +329,16 @@ begin
|
||||||
MinecraftDownloadPage.Hide;
|
MinecraftDownloadPage.Hide;
|
||||||
end;
|
end;
|
||||||
Result := True;
|
Result := True;
|
||||||
end else
|
end
|
||||||
|
else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||||
|
Result := not (LttPROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
|
||||||
|
Result := not (SMROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
|
||||||
|
Result := not (SoEROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||||
|
Result := not (OoTROMFilePage.Values[0] = '')
|
||||||
|
else
|
||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
@ -327,7 +348,7 @@ begin
|
||||||
Result := lttprom
|
Result := lttprom
|
||||||
else if Assigned(LttPRomFilePage) then
|
else if Assigned(LttPRomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
@ -343,7 +364,7 @@ begin
|
||||||
Result := smrom
|
Result := smrom
|
||||||
else if Assigned(SMRomFilePage) then
|
else if Assigned(SMRomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
@ -359,8 +380,7 @@ begin
|
||||||
Result := soerom
|
Result := soerom
|
||||||
else if Assigned(SoERomFilePage) then
|
else if Assigned(SoERomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||||
log(GetMD5OfFile(SoEROMFilePage.Values[0]))
|
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
@ -374,7 +394,7 @@ function GetOoTROMPath(Param: string): string;
|
||||||
begin
|
begin
|
||||||
if Length(ootrom) > 0 then
|
if Length(ootrom) > 0 then
|
||||||
Result := ootrom
|
Result := ootrom
|
||||||
else if Assigned(OoTROMFilePage) then
|
else if (Assigned(OoTROMFilePage)) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
|
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
|
@ -417,4 +437,4 @@ begin
|
||||||
Result := not (WizardIsComponentSelected('generator/soe'));
|
Result := not (WizardIsComponentSelected('generator/soe'));
|
||||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||||
Result := not (WizardIsComponentSelected('generator/oot'));
|
Result := not (WizardIsComponentSelected('generator/oot'));
|
||||||
end;
|
end;
|
||||||
|
|
|
@ -48,21 +48,21 @@ Name: "playing"; Description: "Installation for playing purposes"
|
||||||
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
||||||
|
|
||||||
[Components]
|
[Components]
|
||||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||||
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
|
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
||||||
Name: "server"; Description: "Server"; Types: full hosting
|
Name: "server"; Description: "Server"; Types: full hosting
|
||||||
Name: "client"; Description: "Clients"; Types: full playing
|
Name: "client"; Description: "Clients"; Types: full playing
|
||||||
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
Name: "client/sni"; Description: "SNI Client"; Types: full playing
|
||||||
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing
|
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
|
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
||||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||||
|
|
||||||
[Dirs]
|
[Dirs]
|
||||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||||
|
@ -136,7 +136,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{ap
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
const
|
const
|
||||||
SHCONTCH_NOPROGRESSBOX = 4;
|
SHCONTCH_NOPROGRESSBOX = 4;
|
||||||
|
@ -221,6 +220,19 @@ var OoTROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
var MinecraftDownloadPage: TDownloadWizardPage;
|
var MinecraftDownloadPage: TDownloadWizardPage;
|
||||||
|
|
||||||
|
function GetSNESMD5OfFile(const rom: string): string;
|
||||||
|
var data: AnsiString;
|
||||||
|
begin
|
||||||
|
if LoadStringFromFile(rom, data) then
|
||||||
|
begin
|
||||||
|
if Length(data) mod 1024 = 512 then
|
||||||
|
begin
|
||||||
|
data := copy(data, 513, Length(data)-512);
|
||||||
|
end;
|
||||||
|
Result := GetMD5OfString(data);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function CheckRom(name: string; hash: string): string;
|
function CheckRom(name: string; hash: string): string;
|
||||||
var rom: string;
|
var rom: string;
|
||||||
begin
|
begin
|
||||||
|
@ -229,8 +241,8 @@ begin
|
||||||
if Length(rom) > 0 then
|
if Length(rom) > 0 then
|
||||||
begin
|
begin
|
||||||
log('existing ROM found');
|
log('existing ROM found');
|
||||||
log(IntToStr(CompareStr(GetMD5OfFile(rom), hash)));
|
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
|
||||||
if CompareStr(GetMD5OfFile(rom), hash) = 0 then
|
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
|
||||||
begin
|
begin
|
||||||
log('existing ROM verified');
|
log('existing ROM verified');
|
||||||
Result := rom;
|
Result := rom;
|
||||||
|
@ -317,7 +329,16 @@ begin
|
||||||
MinecraftDownloadPage.Hide;
|
MinecraftDownloadPage.Hide;
|
||||||
end;
|
end;
|
||||||
Result := True;
|
Result := True;
|
||||||
end else
|
end
|
||||||
|
else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||||
|
Result := not (LttPROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
|
||||||
|
Result := not (SMROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
|
||||||
|
Result := not (SoEROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||||
|
Result := not (OoTROMFilePage.Values[0] = '')
|
||||||
|
else
|
||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
@ -327,7 +348,7 @@ begin
|
||||||
Result := lttprom
|
Result := lttprom
|
||||||
else if Assigned(LttPRomFilePage) then
|
else if Assigned(LttPRomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
@ -343,7 +364,7 @@ begin
|
||||||
Result := smrom
|
Result := smrom
|
||||||
else if Assigned(SMRomFilePage) then
|
else if Assigned(SMRomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
@ -359,8 +380,7 @@ begin
|
||||||
Result := soerom
|
Result := soerom
|
||||||
else if Assigned(SoERomFilePage) then
|
else if Assigned(SoERomFilePage) then
|
||||||
begin
|
begin
|
||||||
R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||||
log(GetMD5OfFile(SoEROMFilePage.Values[0]))
|
|
||||||
if R <> 0 then
|
if R <> 0 then
|
||||||
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
|
91
kvui.py
91
kvui.py
|
@ -2,7 +2,6 @@ import os
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||||
|
@ -11,6 +10,8 @@ os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
|
from kivy.core.clipboard import Clipboard
|
||||||
|
from kivy.core.text.markup import MarkupLabel
|
||||||
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock
|
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
|
@ -25,6 +26,10 @@ from kivy.uix.label import Label
|
||||||
from kivy.uix.progressbar import ProgressBar
|
from kivy.uix.progressbar import ProgressBar
|
||||||
from kivy.utils import escape_markup
|
from kivy.utils import escape_markup
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
|
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||||
|
from kivy.uix.behaviors import FocusBehavior
|
||||||
|
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||||
|
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from NetUtils import JSONtoTextParser, JSONMessagePart
|
from NetUtils import JSONtoTextParser, JSONMessagePart
|
||||||
|
@ -140,6 +145,46 @@ class ContainerLayout(FloatLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
|
||||||
|
RecycleBoxLayout):
|
||||||
|
""" Adds selection and focus behaviour to the view. """
|
||||||
|
|
||||||
|
|
||||||
|
class SelectableLabel(RecycleDataViewBehavior, Label):
|
||||||
|
""" Add selection support to the Label """
|
||||||
|
index = None
|
||||||
|
selected = BooleanProperty(False)
|
||||||
|
|
||||||
|
def refresh_view_attrs(self, rv, index, data):
|
||||||
|
""" Catch and handle the view changes """
|
||||||
|
self.index = index
|
||||||
|
return super(SelectableLabel, self).refresh_view_attrs(
|
||||||
|
rv, index, data)
|
||||||
|
|
||||||
|
def on_touch_down(self, touch):
|
||||||
|
""" Add selection on touch down """
|
||||||
|
if super(SelectableLabel, self).on_touch_down(touch):
|
||||||
|
return True
|
||||||
|
if self.collide_point(*touch.pos):
|
||||||
|
if self.selected:
|
||||||
|
self.parent.clear_selection()
|
||||||
|
else:
|
||||||
|
# Not a fan of the following few lines, but they work.
|
||||||
|
temp = MarkupLabel(text=self.text).markup
|
||||||
|
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]")))
|
||||||
|
cmdinput = App.get_running_app().textinput
|
||||||
|
if not cmdinput.text and text.startswith("Didn't find something that closely matches, did you mean "):
|
||||||
|
name = Utils.get_text_between(text, "Didn't find something that closely matches, did you mean ",
|
||||||
|
"? (")
|
||||||
|
cmdinput.text = f"!hint {name}"
|
||||||
|
Clipboard.copy(text)
|
||||||
|
return self.parent.select_with_touch(self.index, touch)
|
||||||
|
|
||||||
|
def apply_selection(self, rv, index, is_selected):
|
||||||
|
""" Respond to the selection of items in the view. """
|
||||||
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class GameManager(App):
|
class GameManager(App):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
|
@ -164,7 +209,8 @@ class GameManager(App):
|
||||||
# top part
|
# top part
|
||||||
server_label = ServerLabel()
|
server_label = ServerLabel()
|
||||||
connect_layout.add_widget(server_label)
|
connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False)
|
self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False,
|
||||||
|
write_tab=False)
|
||||||
self.server_connect_bar.bind(on_text_validate=self.connect_button_action)
|
self.server_connect_bar.bind(on_text_validate=self.connect_button_action)
|
||||||
connect_layout.add_widget(self.server_connect_bar)
|
connect_layout.add_widget(self.server_connect_bar)
|
||||||
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
|
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
|
||||||
|
@ -201,33 +247,21 @@ class GameManager(App):
|
||||||
info_button = Button(height=30, text="Command:", size_hint_x=None)
|
info_button = Button(height=30, text="Command:", size_hint_x=None)
|
||||||
info_button.bind(on_release=self.command_button_action)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
|
self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False)
|
||||||
textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
bottom_layout.add_widget(textinput)
|
|
||||||
|
def text_focus(event):
|
||||||
|
"""Needs to be set via delay, as unfocusing happens after on_message"""
|
||||||
|
self.textinput.focus = True
|
||||||
|
|
||||||
|
self.textinput.text_focus = text_focus
|
||||||
|
bottom_layout.add_widget(self.textinput)
|
||||||
self.grid.add_widget(bottom_layout)
|
self.grid.add_widget(bottom_layout)
|
||||||
self.commandprocessor("/help")
|
self.commandprocessor("/help")
|
||||||
Clock.schedule_interval(self.update_texts, 1 / 30)
|
Clock.schedule_interval(self.update_texts, 1 / 30)
|
||||||
self.container.add_widget(self.grid)
|
self.container.add_widget(self.grid)
|
||||||
self.catch_unhandled_exceptions()
|
|
||||||
return self.container
|
return self.container
|
||||||
|
|
||||||
def catch_unhandled_exceptions(self):
|
|
||||||
"""Relay unhandled exceptions to UI logger."""
|
|
||||||
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
|
|
||||||
orig_hook = sys.excepthook
|
|
||||||
|
|
||||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
|
||||||
if issubclass(exc_type, KeyboardInterrupt):
|
|
||||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
||||||
return
|
|
||||||
logging.getLogger("Client").exception("Uncaught exception",
|
|
||||||
exc_info=(exc_type, exc_value, exc_traceback))
|
|
||||||
return orig_hook(exc_type, exc_value, exc_traceback)
|
|
||||||
|
|
||||||
handle_exception._wrapped = True
|
|
||||||
|
|
||||||
sys.excepthook = handle_exception
|
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
self.title = self.base_title + " " + Utils.__version__ + \
|
self.title = self.base_title + " " + Utils.__version__ + \
|
||||||
|
@ -242,7 +276,11 @@ class GameManager(App):
|
||||||
self.progressbar.value = 0
|
self.progressbar.value = 0
|
||||||
|
|
||||||
def command_button_action(self, button):
|
def command_button_action(self, button):
|
||||||
logging.getLogger("Client").info("/help for client commands and !help for server commands.")
|
if self.ctx.server:
|
||||||
|
logging.getLogger("Client").info("/help for client commands and !help for server commands.")
|
||||||
|
else:
|
||||||
|
logging.getLogger("Client").info("/help for client commands and once you are connected, "
|
||||||
|
"!help for server commands.")
|
||||||
|
|
||||||
def connect_button_action(self, button):
|
def connect_button_action(self, button):
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
|
@ -269,6 +307,9 @@ class GameManager(App):
|
||||||
self.ctx.input_queue.put_nowait(input_text)
|
self.ctx.input_queue.put_nowait(input_text)
|
||||||
elif input_text:
|
elif input_text:
|
||||||
self.commandprocessor(input_text)
|
self.commandprocessor(input_text)
|
||||||
|
|
||||||
|
Clock.schedule_once(textinput.text_focus)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.getLogger("Client").exception(e)
|
logging.getLogger("Client").exception(e)
|
||||||
|
|
||||||
|
@ -304,7 +345,7 @@ class TextManager(GameManager):
|
||||||
|
|
||||||
class LogtoUI(logging.Handler):
|
class LogtoUI(logging.Handler):
|
||||||
def __init__(self, on_log):
|
def __init__(self, on_log):
|
||||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
super(LogtoUI, self).__init__(logging.INFO)
|
||||||
self.on_log = on_log
|
self.on_log = on_log
|
||||||
|
|
||||||
def handle(self, record: logging.LogRecord) -> None:
|
def handle(self, record: logging.LogRecord) -> None:
|
||||||
|
|
26
meta.yaml
26
meta.yaml
|
@ -4,12 +4,6 @@
|
||||||
# For example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal
|
# For example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal
|
||||||
# There is the special case of null, which ignores that part of the meta.yaml,
|
# There is the special case of null, which ignores that part of the meta.yaml,
|
||||||
# allowing for a chance for that meta to not take effect
|
# allowing for a chance for that meta to not take effect
|
||||||
# Players can also have a meta_ignore option to ignore specific options
|
|
||||||
# Example of ignore that would be in a player's file:
|
|
||||||
# meta_ignore:
|
|
||||||
# mode:
|
|
||||||
# inverted
|
|
||||||
# This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
|
|
||||||
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
||||||
null:
|
null:
|
||||||
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
||||||
|
@ -33,26 +27,6 @@ A Link to the Past:
|
||||||
open: 60
|
open: 60
|
||||||
inverted: 10
|
inverted: 10
|
||||||
null: 10 # Maintain individual world states
|
null: 10 # Maintain individual world states
|
||||||
tower_open:
|
|
||||||
'0': 8
|
|
||||||
'1': 7
|
|
||||||
'2': 6
|
|
||||||
'3': 5
|
|
||||||
'4': 4
|
|
||||||
'5': 3
|
|
||||||
'6': 2
|
|
||||||
'7': 1
|
|
||||||
random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
|
|
||||||
ganon_open:
|
|
||||||
'0': 3
|
|
||||||
'1': 4
|
|
||||||
'2': 5
|
|
||||||
'3': 6
|
|
||||||
'4': 7
|
|
||||||
'5': 8
|
|
||||||
'6': 9
|
|
||||||
'7': 10
|
|
||||||
random: 5 # This will mean differing completion times. But leaving it for that surprise effect
|
|
||||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||||
|
|
1148
playerSettings.yaml
1148
playerSettings.yaml
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,8 @@ import os
|
||||||
|
|
||||||
__all__ = {"lookup_any_item_id_to_name",
|
__all__ = {"lookup_any_item_id_to_name",
|
||||||
"lookup_any_location_id_to_name",
|
"lookup_any_location_id_to_name",
|
||||||
"network_data_package"}
|
"network_data_package",
|
||||||
|
"AutoWorldRegister"}
|
||||||
|
|
||||||
# import all submodules to trigger AutoWorldRegister
|
# import all submodules to trigger AutoWorldRegister
|
||||||
for file in os.scandir(os.path.dirname(__file__)):
|
for file in os.scandir(os.path.dirname(__file__)):
|
||||||
|
|
|
@ -284,7 +284,7 @@ junk_texts = [
|
||||||
"{C:GREEN}\nThere’s always\nmoney in the\nBanana Stand>",
|
"{C:GREEN}\nThere’s always\nmoney in the\nBanana Stand>",
|
||||||
"{C:GREEN}\n \nJust walk away\n >",
|
"{C:GREEN}\n \nJust walk away\n >",
|
||||||
"{C:GREEN}\neverybody is\nlooking for\nsomething >",
|
"{C:GREEN}\neverybody is\nlooking for\nsomething >",
|
||||||
"{C:GREEN}\nSpring Ball\nare behind\nRidley >",
|
# "{C:GREEN}\nSpring Ball\nare behind\nRidley >", removed as people may assume it's a real hint
|
||||||
"{C:GREEN}\nThe gnome asks\nyou to guess\nhis name. >",
|
"{C:GREEN}\nThe gnome asks\nyou to guess\nhis name. >",
|
||||||
"{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >",
|
"{C:GREEN}\nI heard beans\non toast is a\ngreat meal. >",
|
||||||
"{C:GREEN}\n> Sweetcorn\non pizza is a\ngreat choice.",
|
"{C:GREEN}\n> Sweetcorn\non pizza is a\ngreat choice.",
|
||||||
|
|
|
@ -47,10 +47,16 @@ recipe_time_scales = {
|
||||||
Options.RecipeTime.option_vanilla: None
|
Options.RecipeTime.option_vanilla: None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recipe_time_ranges = {
|
||||||
|
Options.RecipeTime.option_new_fast: (0.25, 2),
|
||||||
|
Options.RecipeTime.option_new_normal: (0.25, 10),
|
||||||
|
Options.RecipeTime.option_slow: (5, 10)
|
||||||
|
}
|
||||||
|
|
||||||
def generate_mod(world, output_directory: str):
|
def generate_mod(world, output_directory: str):
|
||||||
player = world.player
|
player = world.player
|
||||||
multiworld = world.world
|
multiworld = world.world
|
||||||
global data_final_template, locale_template, control_template, data_template
|
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||||
with template_load_lock:
|
with template_load_lock:
|
||||||
if not data_final_template:
|
if not data_final_template:
|
||||||
mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
||||||
|
@ -60,6 +66,7 @@ def generate_mod(world, output_directory: str):
|
||||||
data_final_template = template_env.get_template("data-final-fixes.lua")
|
data_final_template = template_env.get_template("data-final-fixes.lua")
|
||||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||||
control_template = template_env.get_template("control.lua")
|
control_template = template_env.get_template("control.lua")
|
||||||
|
settings_template = template_env.get_template("settings.lua")
|
||||||
# get data for templates
|
# get data for templates
|
||||||
player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
|
player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
|
||||||
locations = []
|
locations = []
|
||||||
|
@ -91,11 +98,12 @@ def generate_mod(world, output_directory: str):
|
||||||
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
|
||||||
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
|
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||||
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
|
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
|
||||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player,
|
||||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||||
"random": random, "flop_random": flop_random,
|
"random": random, "flop_random": flop_random,
|
||||||
"static_nodes": multiworld.worlds[player].static_nodes,
|
"static_nodes": multiworld.worlds[player].static_nodes,
|
||||||
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
|
"recipe_time_scale": recipe_time_scales.get(multiworld.recipe_time[player].value, None),
|
||||||
|
"recipe_time_range": recipe_time_ranges.get(multiworld.recipe_time[player].value, None),
|
||||||
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
|
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
|
||||||
"progressive_technology_table": {tech.name : tech.progressive for tech in
|
"progressive_technology_table": {tech.name : tech.progressive for tech in
|
||||||
progressive_technology_table.values()},
|
progressive_technology_table.values()},
|
||||||
|
@ -107,10 +115,14 @@ def generate_mod(world, output_directory: str):
|
||||||
|
|
||||||
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
||||||
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||||
|
|
||||||
|
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
|
||||||
|
template_data["free_sample_blacklist"]["satellite"] = 1
|
||||||
|
|
||||||
control_code = control_template.render(**template_data)
|
control_code = control_template.render(**template_data)
|
||||||
data_template_code = data_template.render(**template_data)
|
data_template_code = data_template.render(**template_data)
|
||||||
data_final_fixes_code = data_final_template.render(**template_data)
|
data_final_fixes_code = data_final_template.render(**template_data)
|
||||||
|
settings_code = settings_template.render(**template_data)
|
||||||
|
|
||||||
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
||||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||||
|
@ -122,6 +134,8 @@ def generate_mod(world, output_directory: str):
|
||||||
f.write(data_final_fixes_code)
|
f.write(data_final_fixes_code)
|
||||||
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
||||||
f.write(control_code)
|
f.write(control_code)
|
||||||
|
with open(os.path.join(mod_dir, "settings.lua"), "wt") as f:
|
||||||
|
f.write(settings_code)
|
||||||
locale_content = locale_template.render(**template_data)
|
locale_content = locale_template.render(**template_data)
|
||||||
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
||||||
f.write(locale_content)
|
f.write(locale_content)
|
||||||
|
|
|
@ -55,6 +55,14 @@ class Silo(Choice):
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Satellite(Choice):
|
||||||
|
"""Ingredients to craft satellite."""
|
||||||
|
displayname = "Satellite"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_randomize_recipe = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class FreeSamples(Choice):
|
class FreeSamples(Choice):
|
||||||
"""Get free items with your technologies."""
|
"""Get free items with your technologies."""
|
||||||
displayname = "Free Samples"
|
displayname = "Free Samples"
|
||||||
|
@ -91,13 +99,25 @@ class TechTreeInformation(Choice):
|
||||||
|
|
||||||
|
|
||||||
class RecipeTime(Choice):
|
class RecipeTime(Choice):
|
||||||
"""randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc."""
|
"""Randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
|
||||||
|
Fast: 0.25X - 1X
|
||||||
|
Normal: 0.5X - 2X
|
||||||
|
Slow: 1X - 4X
|
||||||
|
Chaos: 0.25X - 4X
|
||||||
|
New category: ignores vanilla recipe time and rolls new one
|
||||||
|
New Fast: 0.25 - 2 seconds
|
||||||
|
New Normal: 0.25 - 10 seconds
|
||||||
|
New Slow: 5 - 10 seconds
|
||||||
|
"""
|
||||||
displayname = "Recipe Time"
|
displayname = "Recipe Time"
|
||||||
option_vanilla = 0
|
option_vanilla = 0
|
||||||
option_fast = 1
|
option_fast = 1
|
||||||
option_normal = 2
|
option_normal = 2
|
||||||
option_slow = 4
|
option_slow = 4
|
||||||
option_chaos = 5
|
option_chaos = 5
|
||||||
|
option_new_fast = 6
|
||||||
|
option_new_normal = 7
|
||||||
|
option_new_slow = 8
|
||||||
|
|
||||||
|
|
||||||
class Progressive(Choice):
|
class Progressive(Choice):
|
||||||
|
@ -289,6 +309,7 @@ factorio_options: typing.Dict[str, type(Option)] = {
|
||||||
"tech_tree_layout": TechTreeLayout,
|
"tech_tree_layout": TechTreeLayout,
|
||||||
"tech_cost": TechCost,
|
"tech_cost": TechCost,
|
||||||
"silo": Silo,
|
"silo": Silo,
|
||||||
|
"satellite": Satellite,
|
||||||
"free_samples": FreeSamples,
|
"free_samples": FreeSamples,
|
||||||
"tech_tree_information": TechTreeInformation,
|
"tech_tree_information": TechTreeInformation,
|
||||||
"starting_items": FactorioStartItems,
|
"starting_items": FactorioStartItems,
|
||||||
|
|
|
@ -59,8 +59,8 @@ class Technology(FactorioElement): # maybe make subclass of Location?
|
||||||
def build_rule(self, player: int):
|
def build_rule(self, player: int):
|
||||||
logging.debug(f"Building rules for {self.name}")
|
logging.debug(f"Building rules for {self.name}")
|
||||||
|
|
||||||
return lambda state, technologies=technologies: all(state.has(f"Automated {ingredient}", player)
|
return lambda state: all(state.has(f"Automated {ingredient}", player)
|
||||||
for ingredient in self.ingredients)
|
for ingredient in self.ingredients)
|
||||||
|
|
||||||
def get_prior_technologies(self) -> Set[Technology]:
|
def get_prior_technologies(self) -> Set[Technology]:
|
||||||
"""Get Technologies that have to precede this one to resolve tree connections."""
|
"""Get Technologies that have to precede this one to resolve tree connections."""
|
||||||
|
@ -300,19 +300,17 @@ for category_name, machine_name in machine_per_category.items():
|
||||||
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
|
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
|
||||||
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
|
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
|
||||||
|
|
||||||
advancement_technologies: Set[str] = set()
|
def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]:
|
||||||
for ingredient_name in all_ingredient_names:
|
|
||||||
technologies = required_technologies[ingredient_name]
|
|
||||||
advancement_technologies |= {technology.name for technology in technologies}
|
|
||||||
|
|
||||||
|
|
||||||
def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe) -> Set[str]:
|
|
||||||
techs = set()
|
techs = set()
|
||||||
if silo_recipe:
|
if silo_recipe:
|
||||||
for ingredient in silo_recipe.ingredients:
|
for ingredient in silo_recipe.ingredients:
|
||||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||||
for ingredient in part_recipe.ingredients:
|
for ingredient in part_recipe.ingredients:
|
||||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||||
|
if satellite_recipe:
|
||||||
|
techs |= satellite_recipe.unlocking_technologies
|
||||||
|
for ingredient in satellite_recipe.ingredients:
|
||||||
|
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||||
return {tech.name for tech in techs}
|
return {tech.name for tech in techs}
|
||||||
|
|
||||||
|
|
||||||
|
@ -335,8 +333,6 @@ rocket_recipes = {
|
||||||
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
|
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
|
||||||
}
|
}
|
||||||
|
|
||||||
advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]}
|
|
||||||
|
|
||||||
# progressive technologies
|
# progressive technologies
|
||||||
# auto-progressive
|
# auto-progressive
|
||||||
progressive_rows: Dict[str, Union[List[str], Tuple[str, ...]]] = {}
|
progressive_rows: Dict[str, Union[List[str], Tuple[str, ...]]] = {}
|
||||||
|
@ -430,8 +426,6 @@ for root in sorted_rows:
|
||||||
unlocks=any(technology_table[tech].unlocks for tech in progressive))
|
unlocks=any(technology_table[tech].unlocks for tech in progressive))
|
||||||
progressive_tech_table[root] = progressive_technology.factorio_id
|
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||||
progressive_technology_table[root] = progressive_technology
|
progressive_technology_table[root] = progressive_technology
|
||||||
if any(tech in advancement_technologies for tech in progressive):
|
|
||||||
advancement_technologies.add(root)
|
|
||||||
|
|
||||||
tech_to_progressive_lookup: Dict[str, str] = {}
|
tech_to_progressive_lookup: Dict[str, str] = {}
|
||||||
for technology in progressive_technology_table.values():
|
for technology in progressive_technology_table.values():
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import collections
|
import collections
|
||||||
|
import typing
|
||||||
|
|
||||||
from ..AutoWorld import World
|
from ..AutoWorld import World
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, Item
|
from BaseClasses import Region, Entrance, Location, Item
|
||||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
|
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, rocket_recipes, \
|
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, rocket_recipes, \
|
||||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
||||||
from .Shapes import get_shapes
|
from .Shapes import get_shapes
|
||||||
from .Mod import generate_mod
|
from .Mod import generate_mod
|
||||||
from .Options import factorio_options, Silo, TechTreeInformation
|
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -32,13 +33,17 @@ class Factorio(World):
|
||||||
game: str = "Factorio"
|
game: str = "Factorio"
|
||||||
static_nodes = {"automation", "logistics", "rocket-silo"}
|
static_nodes = {"automation", "logistics", "rocket-silo"}
|
||||||
custom_recipes = {}
|
custom_recipes = {}
|
||||||
additional_advancement_technologies = set()
|
advancement_technologies: typing.Set[str]
|
||||||
|
|
||||||
item_name_to_id = all_items
|
item_name_to_id = all_items
|
||||||
location_name_to_id = base_tech_table
|
location_name_to_id = base_tech_table
|
||||||
|
|
||||||
data_version = 5
|
data_version = 5
|
||||||
|
|
||||||
|
def __init__(self, world, player: int):
|
||||||
|
super(Factorio, self).__init__(world, player)
|
||||||
|
self.advancement_technologies = set()
|
||||||
|
|
||||||
def generate_basic(self):
|
def generate_basic(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
|
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
|
||||||
|
@ -137,11 +142,13 @@ class Factorio(World):
|
||||||
locations=locations: all(state.can_reach(loc) for loc in locations))
|
locations=locations: all(state.can_reach(loc) for loc in locations))
|
||||||
|
|
||||||
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
|
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
|
||||||
else self.custom_recipes["rocket-silo"] \
|
else self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||||
if "rocket-silo" in self.custom_recipes \
|
|
||||||
else next(iter(all_product_sources.get("rocket-silo")))
|
else next(iter(all_product_sources.get("rocket-silo")))
|
||||||
part_recipe = self.custom_recipes["rocket-part"]
|
part_recipe = self.custom_recipes["rocket-part"]
|
||||||
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe)
|
satellite_recipe = None if self.world.max_science_pack[self.player].value != MaxSciencePack.option_space_science_pack \
|
||||||
|
else self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
|
||||||
|
else next(iter(all_product_sources.get("satellite")))
|
||||||
|
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
|
||||||
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
|
||||||
for technology in
|
for technology in
|
||||||
victory_tech_names)
|
victory_tech_names)
|
||||||
|
@ -189,12 +196,12 @@ class Factorio(World):
|
||||||
fallback_pool = []
|
fallback_pool = []
|
||||||
|
|
||||||
# fill all but one slot with random ingredients, last with a good match
|
# fill all but one slot with random ingredients, last with a good match
|
||||||
while remaining_num_ingredients > 0 and len(pool) > 0:
|
while remaining_num_ingredients > 0 and pool:
|
||||||
if remaining_num_ingredients == 1:
|
if remaining_num_ingredients == 1:
|
||||||
max_raw = 1.1 * remaining_raw
|
max_raw = 1.1 * remaining_raw
|
||||||
min_raw = 0.9 * remaining_raw
|
min_raw = 0.9 * remaining_raw
|
||||||
max_energy = 1.1 * remaining_energy
|
max_energy = 1.1 * remaining_energy
|
||||||
min_energy = 1.1 * remaining_energy
|
min_energy = 0.9 * remaining_energy
|
||||||
else:
|
else:
|
||||||
max_raw = remaining_raw * 0.75
|
max_raw = remaining_raw * 0.75
|
||||||
min_raw = (remaining_raw - max_raw) / remaining_num_ingredients
|
min_raw = (remaining_raw - max_raw) / remaining_num_ingredients
|
||||||
|
@ -226,7 +233,7 @@ class Factorio(World):
|
||||||
|
|
||||||
# fill failed slots with whatever we got
|
# fill failed slots with whatever we got
|
||||||
pool = fallback_pool
|
pool = fallback_pool
|
||||||
while remaining_num_ingredients > 0 and len(pool) > 0:
|
while remaining_num_ingredients > 0 and pool:
|
||||||
ingredient = pool.pop()
|
ingredient = pool.pop()
|
||||||
if ingredient not in recipes:
|
if ingredient not in recipes:
|
||||||
logging.warning(f"missing recipe for {ingredient}")
|
logging.warning(f"missing recipe for {ingredient}")
|
||||||
|
@ -264,8 +271,6 @@ class Factorio(World):
|
||||||
{valid_pool[x]: 10 for x in range(3)},
|
{valid_pool[x]: 10 for x in range(3)},
|
||||||
original_rocket_part.products,
|
original_rocket_part.products,
|
||||||
original_rocket_part.energy)}
|
original_rocket_part.energy)}
|
||||||
self.additional_advancement_technologies = {tech.name for tech in
|
|
||||||
self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
|
|
||||||
|
|
||||||
if self.world.recipe_ingredients[self.player]:
|
if self.world.recipe_ingredients[self.player]:
|
||||||
valid_pool = []
|
valid_pool = []
|
||||||
|
@ -278,31 +283,45 @@ class Factorio(World):
|
||||||
for _ in original.ingredients:
|
for _ in original.ingredients:
|
||||||
new_ingredients[valid_pool.pop()] = 1
|
new_ingredients[valid_pool.pop()] = 1
|
||||||
new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy)
|
new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy)
|
||||||
self.additional_advancement_technologies |= {tech.name for tech in
|
|
||||||
new_recipe.recursive_unlocking_technologies}
|
|
||||||
self.custom_recipes[pack] = new_recipe
|
self.custom_recipes[pack] = new_recipe
|
||||||
|
|
||||||
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
if self.world.silo[self.player].value == Silo.option_randomize_recipe \
|
||||||
|
or self.world.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||||
valid_pool = []
|
valid_pool = []
|
||||||
for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()):
|
for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()):
|
||||||
valid_pool += sorted(science_pack_pools[pack])
|
valid_pool += sorted(science_pack_pools[pack])
|
||||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
|
||||||
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
||||||
self.additional_advancement_technologies |= {tech.name for tech in
|
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool.copy(),
|
||||||
new_recipe.recursive_unlocking_technologies}
|
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||||
self.custom_recipes["rocket-silo"] = new_recipe
|
self.custom_recipes["rocket-silo"] = new_recipe
|
||||||
|
|
||||||
|
if self.world.satellite[self.player].value == Satellite.option_randomize_recipe:
|
||||||
|
new_recipe = self.make_balanced_recipe(recipes["satellite"], valid_pool,
|
||||||
|
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||||
|
self.custom_recipes["satellite"] = new_recipe
|
||||||
|
|
||||||
|
needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
|
||||||
|
if self.world.silo[self.player] != Silo.option_spawn:
|
||||||
|
needed_recipes |= {"rocket-silo"}
|
||||||
|
if self.world.max_science_pack[self.player].value == MaxSciencePack.option_space_science_pack:
|
||||||
|
needed_recipes |= {"satellite"}
|
||||||
|
|
||||||
|
for recipe in needed_recipes:
|
||||||
|
recipe = self.custom_recipes.get(recipe, recipes[recipe])
|
||||||
|
self.advancement_technologies |= {tech.name for tech in recipe.unlocking_technologies}
|
||||||
|
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
|
||||||
|
|
||||||
# handle marking progressive techs as advancement
|
# handle marking progressive techs as advancement
|
||||||
prog_add = set()
|
prog_add = set()
|
||||||
for tech in self.additional_advancement_technologies:
|
for tech in self.advancement_technologies:
|
||||||
if tech in tech_to_progressive_lookup:
|
if tech in tech_to_progressive_lookup:
|
||||||
prog_add.add(tech_to_progressive_lookup[tech])
|
prog_add.add(tech_to_progressive_lookup[tech])
|
||||||
self.additional_advancement_technologies |= prog_add
|
self.advancement_technologies |= prog_add
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
if name in tech_table:
|
if name in tech_table:
|
||||||
return FactorioItem(name, name in advancement_technologies or
|
return FactorioItem(name, name in self.advancement_technologies,
|
||||||
name in self.additional_advancement_technologies,
|
|
||||||
tech_table[name], self.player)
|
tech_table[name], self.player)
|
||||||
|
|
||||||
item = FactorioItem(name, False, all_items[name], self.player)
|
item = FactorioItem(name, False, all_items[name], self.player)
|
||||||
|
|
|
@ -8,7 +8,14 @@ SLOT_NAME = "{{ slot_name }}"
|
||||||
SEED_NAME = "{{ seed_name }}"
|
SEED_NAME = "{{ seed_name }}"
|
||||||
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
||||||
TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
|
TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
|
||||||
DEATH_LINK = {{ death_link | int }}
|
MAX_SCIENCE_PACK = {{ max_science_pack }}
|
||||||
|
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
|
||||||
|
|
||||||
|
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
|
||||||
|
DEATH_LINK = 1
|
||||||
|
else
|
||||||
|
DEATH_LINK = 0
|
||||||
|
end
|
||||||
|
|
||||||
CURRENTLY_DEATH_LOCK = 0
|
CURRENTLY_DEATH_LOCK = 0
|
||||||
|
|
||||||
|
@ -76,6 +83,27 @@ function on_force_destroyed(event)
|
||||||
global.forcedata[event.force.name] = nil
|
global.forcedata[event.force.name] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function on_runtime_mod_setting_changed(event)
|
||||||
|
local force
|
||||||
|
if event.player_index == nil then
|
||||||
|
force = game.forces.player
|
||||||
|
else
|
||||||
|
force = game.players[event.player_index].force
|
||||||
|
end
|
||||||
|
|
||||||
|
if event.setting == ARCHIPELAGO_DEATH_LINK_SETTING then
|
||||||
|
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
|
||||||
|
DEATH_LINK = 1
|
||||||
|
else
|
||||||
|
DEATH_LINK = 0
|
||||||
|
end
|
||||||
|
if force ~= nil then
|
||||||
|
dumpInfo(force)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
script.on_event(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed)
|
||||||
|
|
||||||
-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
|
-- Initialize player data, either from them joining the game or them already being part of the game when the mod was
|
||||||
-- added.`
|
-- added.`
|
||||||
function on_player_created(event)
|
function on_player_created(event)
|
||||||
|
@ -107,8 +135,19 @@ end
|
||||||
script.on_event(defines.events.on_player_removed, on_player_removed)
|
script.on_event(defines.events.on_player_removed, on_player_removed)
|
||||||
|
|
||||||
function on_rocket_launched(event)
|
function on_rocket_launched(event)
|
||||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then
|
||||||
dumpInfo(event.rocket.force)
|
if event.rocket.get_item_count("satellite") > 0 or MAX_SCIENCE_PACK < 6 then
|
||||||
|
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||||
|
dumpInfo(event.rocket.force)
|
||||||
|
game.set_game_state
|
||||||
|
{
|
||||||
|
game_finished = true,
|
||||||
|
player_won = true,
|
||||||
|
can_continue = true,
|
||||||
|
victorious_force = event.rocket.force
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
||||||
|
|
||||||
|
@ -198,6 +237,10 @@ script.on_init(function()
|
||||||
e.player_index = index
|
e.player_index = index
|
||||||
on_player_created(e)
|
on_player_created(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if remote.interfaces["silo_script"] then
|
||||||
|
remote.call("silo_script", "set_no_victory", true)
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- hook into researches done
|
-- hook into researches done
|
||||||
|
@ -366,18 +409,19 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
if DEATH_LINK == 1 then
|
script.on_event(defines.events.on_entity_died, function(event)
|
||||||
script.on_event(defines.events.on_entity_died, function(event)
|
if DEATH_LINK == 0 then
|
||||||
if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event
|
return
|
||||||
return
|
end
|
||||||
end
|
if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local force = event.entity.force
|
local force = event.entity.force
|
||||||
global.forcedata[force.name].death_link_tick = game.tick
|
global.forcedata[force.name].death_link_tick = game.tick
|
||||||
dumpInfo(force)
|
dumpInfo(force)
|
||||||
kill_players(force)
|
kill_players(force)
|
||||||
end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}})
|
end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}})
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- add / commands
|
-- add / commands
|
||||||
|
@ -392,7 +436,8 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
|
||||||
local data_collection = {
|
local data_collection = {
|
||||||
["research_done"] = research_done,
|
["research_done"] = research_done,
|
||||||
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
["victory"] = chain_lookup(global, "forcedata", force.name, "victory"),
|
||||||
["death_link_tick"] = chain_lookup(global, "forcedata", force.name, "death_link_tick")
|
["death_link_tick"] = chain_lookup(global, "forcedata", force.name, "death_link_tick"),
|
||||||
|
["death_link"] = DEATH_LINK
|
||||||
}
|
}
|
||||||
|
|
||||||
for tech_name, tech in pairs(force.technologies) do
|
for tech_name, tech in pairs(force.technologies) do
|
||||||
|
@ -423,8 +468,8 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||||
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
|
||||||
game.play_sound({path="utility/research_completed"})
|
game.play_sound({path="utility/research_completed"})
|
||||||
tech.researched = true
|
tech.researched = true
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
return
|
||||||
elseif progressive_technologies[item_name] ~= nil then
|
elseif progressive_technologies[item_name] ~= nil then
|
||||||
if global.index_sync[index] == nil then -- not yet received prog item
|
if global.index_sync[index] == nil then -- not yet received prog item
|
||||||
global.index_sync[index] = item_name
|
global.index_sync[index] = item_name
|
||||||
|
@ -442,9 +487,6 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||||
elseif force.technologies[item_name] ~= nil then
|
elseif force.technologies[item_name] ~= nil then
|
||||||
tech = force.technologies[item_name]
|
tech = force.technologies[item_name]
|
||||||
if tech ~= nil then
|
if tech ~= nil then
|
||||||
if global.index_sync[index] ~= nil and global.index_sync[index] ~= tech then
|
|
||||||
game.print("Warning: Desync Detected. Duplicate/Missing items may occur.")
|
|
||||||
end
|
|
||||||
global.index_sync[index] = tech
|
global.index_sync[index] = tech
|
||||||
if tech.researched ~= true then
|
if tech.researched ~= true then
|
||||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||||
|
|
|
@ -100,6 +100,20 @@ function adjust_energy(recipe_name, factor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function set_energy(recipe_name, energy)
|
||||||
|
local recipe = data.raw.recipe[recipe_name]
|
||||||
|
|
||||||
|
if (recipe.normal ~= nil) then
|
||||||
|
recipe.normal.energy_required = energy
|
||||||
|
end
|
||||||
|
if (recipe.expensive ~= nil) then
|
||||||
|
recipe.expensive.energy_required = energy
|
||||||
|
end
|
||||||
|
if (recipe.expensive == nil and recipe.normal == nil) then
|
||||||
|
recipe.energy_required = energy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||||
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||||
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||||
|
@ -144,6 +158,12 @@ data:extend{new_tree_copy}
|
||||||
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
{% elif recipe_time_range %}
|
||||||
|
{%- for recipe_name, recipe in recipes.items() %}
|
||||||
|
{%- if recipe.category != "mining" %}
|
||||||
|
set_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_range) }})
|
||||||
|
{%- endif %}
|
||||||
|
{%- endfor -%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{%- if silo==2 %}
|
{%- if silo==2 %}
|
||||||
|
|
|
@ -22,4 +22,10 @@ ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends somet
|
||||||
{%- else %}
|
{%- else %}
|
||||||
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}".
|
ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends something to someone. For purposes of hints, this location is called "{{ original_tech_name }}".
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
[mod-setting-name]
|
||||||
|
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
|
||||||
|
|
||||||
|
[mod-setting-description]
|
||||||
|
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
|
|
@ -0,0 +1,12 @@
|
||||||
|
data:extend({
|
||||||
|
{
|
||||||
|
type = "bool-setting",
|
||||||
|
name = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}",
|
||||||
|
setting_type = "runtime-global",
|
||||||
|
{% if death_link %}
|
||||||
|
default_value = true
|
||||||
|
{% else %}
|
||||||
|
default_value = false
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
})
|
|
@ -147,7 +147,7 @@ class OOTWorld(World):
|
||||||
# Incompatible option handling
|
# Incompatible option handling
|
||||||
# ER and glitched logic are not compatible; glitched takes priority
|
# ER and glitched logic are not compatible; glitched takes priority
|
||||||
if self.logic_rules == 'glitched':
|
if self.logic_rules == 'glitched':
|
||||||
self.shuffle_interior_entrances = False
|
self.shuffle_interior_entrances = 'off'
|
||||||
self.shuffle_grotto_entrances = False
|
self.shuffle_grotto_entrances = False
|
||||||
self.shuffle_dungeon_entrances = False
|
self.shuffle_dungeon_entrances = False
|
||||||
self.shuffle_overworld_entrances = False
|
self.shuffle_overworld_entrances = False
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Utils
|
||||||
from Patch import read_rom
|
from Patch import read_rom
|
||||||
|
|
||||||
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||||
ROM_PLAYER_LIMIT = 255
|
ROM_PLAYER_LIMIT = 65535
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
|
|
@ -159,6 +159,9 @@ class SMWorld(World):
|
||||||
|
|
||||||
def getWord(self, w):
|
def getWord(self, w):
|
||||||
return (w & 0x00FF, (w & 0xFF00) >> 8)
|
return (w & 0x00FF, (w & 0xFF00) >> 8)
|
||||||
|
|
||||||
|
def getWordArray(self, w):
|
||||||
|
return [w & 0x00FF, (w & 0xFF00) >> 8]
|
||||||
|
|
||||||
# used for remote location Credits Spoiler of local items
|
# used for remote location Credits Spoiler of local items
|
||||||
class DummyLocation:
|
class DummyLocation:
|
||||||
|
@ -232,7 +235,10 @@ class SMWorld(World):
|
||||||
multiWorldItems = {}
|
multiWorldItems = {}
|
||||||
idx = 0
|
idx = 0
|
||||||
itemId = 0
|
itemId = 0
|
||||||
|
self.playerIDMap = {}
|
||||||
|
playerIDCount = 0 # 0 is for "Archipelago" server
|
||||||
for itemLoc in self.world.get_locations():
|
for itemLoc in self.world.get_locations():
|
||||||
|
romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0
|
||||||
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
|
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
|
||||||
if itemLoc.item.type in ItemManager.Items:
|
if itemLoc.item.type in ItemManager.Items:
|
||||||
itemId = ItemManager.Items[itemLoc.item.type].Id
|
itemId = ItemManager.Items[itemLoc.item.type].Id
|
||||||
|
@ -240,12 +246,21 @@ class SMWorld(World):
|
||||||
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
||||||
multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name)
|
multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name)
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
|
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
|
||||||
|
playerIDCount += 1
|
||||||
|
self.playerIDMap[romPlayerID] = playerIDCount
|
||||||
|
|
||||||
(w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1)
|
(w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1)
|
||||||
(w2, w3) = self.getWord(itemId)
|
(w2, w3) = self.getWord(itemId)
|
||||||
(w4, w5) = self.getWord(itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0)
|
(w4, w5) = self.getWord(romPlayerID)
|
||||||
(w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1)
|
(w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1)
|
||||||
multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7]
|
multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7]
|
||||||
|
|
||||||
|
if itemLoc.item.player == self.player:
|
||||||
|
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
|
||||||
|
playerIDCount += 1
|
||||||
|
self.playerIDMap[romPlayerID] = playerIDCount
|
||||||
|
|
||||||
itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"]
|
itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"]
|
||||||
idx = 0
|
idx = 0
|
||||||
|
@ -260,21 +275,24 @@ class SMWorld(World):
|
||||||
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
|
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
|
||||||
|
|
||||||
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
|
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
|
||||||
|
|
||||||
|
playerNames = {}
|
||||||
|
playerNameIDMap = {}
|
||||||
|
playerNames[0x1C5000] = "Archipelago".upper().center(16).encode()
|
||||||
|
playerNameIDMap[0x1C5800] = self.getWordArray(0)
|
||||||
|
for key,value in self.playerIDMap.items():
|
||||||
|
playerNames[0x1C5000 + value * 16] = self.world.player_name[key][:16].upper().center(16).encode()
|
||||||
|
playerNameIDMap[0x1C5800 + value * 2] = self.getWordArray(key)
|
||||||
|
|
||||||
patchDict = { 'MultiWorldLocations': multiWorldLocations,
|
patchDict = { 'MultiWorldLocations': multiWorldLocations,
|
||||||
'MultiWorldItems': multiWorldItems,
|
'MultiWorldItems': multiWorldItems,
|
||||||
'offworldSprites': offworldSprites,
|
'offworldSprites': offworldSprites,
|
||||||
'openTourianGreyDoors': openTourianGreyDoors,
|
'openTourianGreyDoors': openTourianGreyDoors,
|
||||||
'deathLink': deathLink}
|
'deathLink': deathLink,
|
||||||
|
'PlayerName': playerNames,
|
||||||
|
'PlayerNameIDMap': playerNameIDMap}
|
||||||
romPatcher.applyIPSPatchDict(patchDict)
|
romPatcher.applyIPSPatchDict(patchDict)
|
||||||
|
|
||||||
playerNames = {}
|
|
||||||
playerNames[0x1C5000] = "Archipelago".upper().center(16).encode()
|
|
||||||
for p in range(1, min(self.world.players, ROM_PLAYER_LIMIT) + 1):
|
|
||||||
playerNames[0x1C5000 + p * 16] = self.world.player_name[p][:16].upper().center(16).encode()
|
|
||||||
|
|
||||||
|
|
||||||
romPatcher.applyIPSPatch('PlayerName', { 'PlayerName': playerNames })
|
|
||||||
|
|
||||||
# set rom name
|
# set rom name
|
||||||
# 21 bytes
|
# 21 bytes
|
||||||
from Main import __version__
|
from Main import __version__
|
||||||
|
|
Binary file not shown.
|
@ -2,16 +2,15 @@ import os, importlib
|
||||||
from logic.logic import Logic
|
from logic.logic import Logic
|
||||||
from patches.common.patches import patches, additional_PLMs
|
from patches.common.patches import patches, additional_PLMs
|
||||||
from utils.parameters import appDir
|
from utils.parameters import appDir
|
||||||
from Utils import is_frozen
|
|
||||||
|
|
||||||
class PatchAccess(object):
|
class PatchAccess(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# load all ips patches
|
# load all ips patches
|
||||||
self.patchesPath = {}
|
self.patchesPath = {}
|
||||||
commonDir = os.path.join(appDir, 'lib' if is_frozen() else '', 'worlds/sm/variaRandomizer/patches/common/ips/')
|
commonDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/common/ips/')
|
||||||
for patch in os.listdir(commonDir):
|
for patch in os.listdir(commonDir):
|
||||||
self.patchesPath[patch] = commonDir
|
self.patchesPath[patch] = commonDir
|
||||||
logicDir = os.path.join(appDir, 'lib' if is_frozen() else '', 'worlds/sm/variaRandomizer/patches/{}/ips/'.format(Logic.patches))
|
logicDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/{}/ips/'.format(Logic.patches))
|
||||||
for patch in os.listdir(logicDir):
|
for patch in os.listdir(logicDir):
|
||||||
self.patchesPath[patch] = logicDir
|
self.patchesPath[patch] = logicDir
|
||||||
|
|
||||||
|
|
|
@ -327,7 +327,7 @@ class VariaRandomizer:
|
||||||
preset = loadRandoPreset(world, self.player, args)
|
preset = loadRandoPreset(world, self.player, args)
|
||||||
# use the skill preset from the rando preset
|
# use the skill preset from the rando preset
|
||||||
if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None:
|
if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None:
|
||||||
args.paramsFileName = '{}/{}/{}.json'.format(appDir, getPresetDir(preset), preset)
|
args.paramsFileName = os.path.join(appDir, getPresetDir(preset), preset+".json")
|
||||||
|
|
||||||
# if diff preset given, load it
|
# if diff preset given, load it
|
||||||
if args.paramsFileName is not None:
|
if args.paramsFileName is not None:
|
||||||
|
@ -352,7 +352,7 @@ class VariaRandomizer:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
else:
|
else:
|
||||||
preset = 'default'
|
preset = 'default'
|
||||||
PresetLoader.factory('{}/{}/{}.json'.format(appDir, getPresetDir('casual'), 'casual')).load(self.player)
|
PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from logic.smbool import SMBool
|
from logic.smbool import SMBool
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# the different difficulties available
|
# the different difficulties available
|
||||||
easy = 1
|
easy = 1
|
||||||
|
@ -60,7 +61,7 @@ def diff4solver(difficulty):
|
||||||
return "mania"
|
return "mania"
|
||||||
|
|
||||||
# allow multiple local repo
|
# allow multiple local repo
|
||||||
appDir = sys.path[0]
|
appDir = Path(__file__).parents[4]
|
||||||
|
|
||||||
def isKnows(knows):
|
def isKnows(knows):
|
||||||
return knows[0:len('__')] != '__' and knows[0] == knows[0].upper()
|
return knows[0:len('__')] != '__' and knows[0] == knows[0].upper()
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import os, json, sys, re, random
|
import os, json, re, random
|
||||||
|
|
||||||
from utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton
|
from utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton
|
||||||
from utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff
|
from utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff
|
||||||
from logic.smbool import SMBool
|
from logic.smbool import SMBool
|
||||||
|
|
||||||
from Utils import is_frozen
|
|
||||||
|
|
||||||
def isStdPreset(preset):
|
def isStdPreset(preset):
|
||||||
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
|
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
|
||||||
|
|
||||||
def getPresetDir(preset):
|
def getPresetDir(preset) -> str:
|
||||||
if isStdPreset(preset):
|
if isStdPreset(preset):
|
||||||
return 'lib/worlds/sm/variaRandomizer/standard_presets' if is_frozen() else 'worlds/sm/variaRandomizer/standard_presets'
|
return 'worlds/sm/variaRandomizer/standard_presets'
|
||||||
else:
|
else:
|
||||||
return 'lib/worlds/sm/variaRandomizer/community_presets' if is_frozen() else 'worlds/sm/variaRandomizer/community_presets'
|
return 'worlds/sm/variaRandomizer/community_presets'
|
||||||
|
|
||||||
def removeChars(string, toRemove):
|
def removeChars(string, toRemove):
|
||||||
return re.sub('[{}]+'.format(toRemove), '', string)
|
return re.sub('[{}]+'.format(toRemove), '', string)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from Utils import get_options, output_path
|
||||||
import typing
|
import typing
|
||||||
import lzma
|
import lzma
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -200,11 +201,18 @@ class SoEWorld(World):
|
||||||
line = f'{loc.type},{loc.index}:{item.type},{item.index}\n'
|
line = f'{loc.type},{loc.index}:{item.type},{item.index}\n'
|
||||||
f.write(line.encode('utf-8'))
|
f.write(line.encode('utf-8'))
|
||||||
|
|
||||||
if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name, self.evermizer_seed,
|
if not os.path.exists(rom_file):
|
||||||
flags, money, exp)):
|
raise FileNotFoundError(rom_file)
|
||||||
|
if (pyevermizer.main(rom_file, out_file, placement_file, self.world.seed_name, self.connect_name,
|
||||||
|
self.evermizer_seed, flags, money, exp)):
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
with lzma.LZMAFile(patch_file, 'wb') as f:
|
with lzma.LZMAFile(patch_file, 'wb') as f:
|
||||||
f.write(generate_patch(rom_file, out_file))
|
f.write(generate_patch(rom_file, out_file,
|
||||||
|
{
|
||||||
|
# used by WebHost
|
||||||
|
"player_name": self.world.player_name[self.player],
|
||||||
|
"player_id": self.player
|
||||||
|
}))
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -106,7 +106,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||||
LocationData('Refugee Camp', 'Refugee camp storage chest 2', 1337088),
|
LocationData('Refugee Camp', 'Refugee camp storage chest 2', 1337088),
|
||||||
LocationData('Refugee Camp', 'Refugee camp storage chest 1', 1337089),
|
LocationData('Refugee Camp', 'Refugee camp storage chest 1', 1337089),
|
||||||
LocationData('Forest', 'Refugee camp roof', 1337090),
|
LocationData('Forest', 'Refugee camp roof', 1337090),
|
||||||
LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)),
|
LocationData('Forest', 'Bat jump chest', 1337091, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)),
|
||||||
LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)),
|
LocationData('Forest', 'Green platform secret', 1337092, lambda state: state._timespinner_can_break_walls(world, player)),
|
||||||
LocationData('Forest', 'Rats guarded chest', 1337093),
|
LocationData('Forest', 'Rats guarded chest', 1337093),
|
||||||
LocationData('Forest', 'Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)),
|
LocationData('Forest', 'Waterfall chest 1', 1337094, lambda state: state.has('Water Mask', player)),
|
||||||
|
@ -158,7 +158,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||||
LocationData('Castle Keep', 'Out of the way', 1337139),
|
LocationData('Castle Keep', 'Out of the way', 1337139),
|
||||||
LocationData('Castle Keep', 'Killed Twins', EventId, lambda state: state._timespinner_has_timestop(world, player)),
|
LocationData('Castle Keep', 'Killed Twins', EventId, lambda state: state._timespinner_has_timestop(world, player)),
|
||||||
LocationData('Castle Keep', 'Twins', 1337140, lambda state: state._timespinner_has_timestop(world, player)),
|
LocationData('Castle Keep', 'Twins', 1337140, lambda state: state._timespinner_has_timestop(world, player)),
|
||||||
LocationData('Castle Keep', 'Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player)),
|
LocationData('Castle Keep', 'Royal guard tiny room', 1337141, lambda state: state._timespinner_has_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world,player)),
|
||||||
LocationData('Royal towers (lower)', 'Royal tower floor secret', 1337142, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_can_break_walls(world, player)),
|
LocationData('Royal towers (lower)', 'Royal tower floor secret', 1337142, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_can_break_walls(world, player)),
|
||||||
LocationData('Royal towers', 'Above the gap', 1337143),
|
LocationData('Royal towers', 'Above the gap', 1337143),
|
||||||
LocationData('Royal towers', 'Under the ice mage', 1337144),
|
LocationData('Royal towers', 'Under the ice mage', 1337144),
|
||||||
|
|
|
@ -15,6 +15,9 @@ class TimespinnerLogic(LogicMixin):
|
||||||
def _timespinner_has_doublejump_of_npc(self, world: MultiWorld, player: int) -> bool:
|
def _timespinner_has_doublejump_of_npc(self, world: MultiWorld, player: int) -> bool:
|
||||||
return self._timespinner_has_upwarddash(world, player) or (self.has('Timespinner Wheel', player) and self._timespinner_has_doublejump(world, player))
|
return self._timespinner_has_upwarddash(world, player) or (self.has('Timespinner Wheel', player) and self._timespinner_has_doublejump(world, player))
|
||||||
|
|
||||||
|
def _timespinner_has_fastjump_on_npc(self, world: MultiWorld, player: int) -> bool:
|
||||||
|
return self.has_all(['Timespinner Wheel', 'Talaria Attachment'], player)
|
||||||
|
|
||||||
def _timespinner_has_multiple_small_jumps_of_npc(self, world: MultiWorld, player: int) -> bool:
|
def _timespinner_has_multiple_small_jumps_of_npc(self, world: MultiWorld, player: int) -> bool:
|
||||||
return self.has('Timespinner Wheel', player) or self._timespinner_has_upwarddash(world, player)
|
return self.has('Timespinner Wheel', player) or self._timespinner_has_upwarddash(world, player)
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||||
connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player))
|
connect(world, player, names, 'Varndagroth tower right (upper)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player))
|
||||||
connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)')
|
connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (upper)')
|
||||||
connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)')
|
connect(world, player, names, 'Varndagroth tower right (elevator)', 'Varndagroth tower right (lower)')
|
||||||
connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left')
|
connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_B(world, player))
|
||||||
connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player))
|
connect(world, player, names, 'Varndagroth tower right (lower)', 'Varndagroth tower right (elevator)', lambda state: state.has('Elevator Keycard', player))
|
||||||
connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: state._timespinner_has_keycard_B(world, player) and state.has('Elevator Keycard', player))
|
connect(world, player, names, 'Varndagroth tower right (lower)', 'Sealed Caves (Sirens)', lambda state: state._timespinner_has_keycard_B(world, player) and state.has('Elevator Keycard', player))
|
||||||
connect(world, player, names, 'Varndagroth tower right (lower)', 'Militairy Fortress', lambda state: state._timespinner_can_kill_all_3_bosses(world, player))
|
connect(world, player, names, 'Varndagroth tower right (lower)', 'Militairy Fortress', lambda state: state._timespinner_can_kill_all_3_bosses(world, player))
|
||||||
|
|
Loading…
Reference in New Issue