Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into oot
This commit is contained in:
commit
6641b13511
|
@ -12,10 +12,10 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
@ -12,10 +12,10 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
|
@ -11,7 +11,7 @@ import websockets
|
|||
import Utils
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient")
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
||||
|
@ -39,13 +39,13 @@ class ClientCommandProcessor(CommandProcessor):
|
|||
def _cmd_connect(self, address: str = "") -> bool:
|
||||
"""Connect to a MultiWorld Server"""
|
||||
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
|
||||
|
||||
def _cmd_disconnect(self) -> bool:
|
||||
"""Disconnect from a MultiWorld Server"""
|
||||
self.ctx.server_address = None
|
||||
asyncio.create_task(self.ctx.disconnect())
|
||||
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
|
||||
return True
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
|
@ -81,6 +81,16 @@ class ClientCommandProcessor(CommandProcessor):
|
|||
self.output("No missing location checks found.")
|
||||
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):
|
||||
self.ctx.ready = not self.ctx.ready
|
||||
if self.ctx.ready:
|
||||
|
@ -89,10 +99,10 @@ class ClientCommandProcessor(CommandProcessor):
|
|||
else:
|
||||
state = ClientStatus.CLIENT_CONNECTED
|
||||
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):
|
||||
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():
|
||||
|
@ -149,7 +159,7 @@ class CommonContext():
|
|||
self.set_getters(network_data_package)
|
||||
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self))
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
|
||||
@property
|
||||
def total_locations(self) -> typing.Optional[int]:
|
||||
|
@ -230,13 +240,24 @@ class CommonContext():
|
|||
self.password = await self.console_input()
|
||||
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):
|
||||
self.input_requests += 1
|
||||
return await self.input_queue.get()
|
||||
|
||||
async def connect(self, address=None):
|
||||
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):
|
||||
logger.info(args["text"])
|
||||
|
@ -271,7 +292,7 @@ class CommonContext():
|
|||
logger.info(f"DeathLink: Received from {data['source']}")
|
||||
|
||||
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()
|
||||
await self.send_msgs([{
|
||||
"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):
|
||||
"""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()
|
||||
if ctx.server_address:
|
||||
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
|
||||
|
||||
|
||||
async def server_autoreconnect(ctx: CommonContext):
|
||||
await asyncio.sleep(ctx.current_reconnect_delay)
|
||||
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):
|
||||
|
@ -534,6 +576,7 @@ if __name__ == '__main__':
|
|||
|
||||
class TextContext(CommonContext):
|
||||
tags = {"AP", "IgnoreGame"}
|
||||
game = "Archipelago"
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
|
@ -542,11 +585,7 @@ if __name__ == '__main__':
|
|||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
||||
}])
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
|
@ -555,7 +594,7 @@ if __name__ == '__main__':
|
|||
|
||||
async def main(args):
|
||||
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:
|
||||
input_task = None
|
||||
from kvui import TextManager
|
||||
|
@ -566,16 +605,7 @@ if __name__ == '__main__':
|
|||
ui_task = None
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
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
|
||||
|
||||
await ctx.shutdown()
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from queue import Queue
|
|||
import Utils
|
||||
|
||||
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, \
|
||||
get_base_parser
|
||||
|
@ -65,22 +65,13 @@ class FactorioContext(CommonContext):
|
|||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
|
||||
if not self.auth:
|
||||
if self.rcon_client:
|
||||
get_info(self, self.rcon_client) # retrieve current auth code
|
||||
else:
|
||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||
"bridge to Factorio first.")
|
||||
if self.rcon_client:
|
||||
await get_info(self, self.rcon_client) # retrieve current auth code
|
||||
else:
|
||||
raise Exception("Cannot connect to a server with unknown own identity, "
|
||||
"bridge to Factorio first.")
|
||||
|
||||
await self.send_msgs([{
|
||||
"cmd": 'Connect',
|
||||
'password': self.password,
|
||||
'name': self.auth,
|
||||
'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(),
|
||||
'game': "Factorio"
|
||||
}])
|
||||
await self.send_connect()
|
||||
|
||||
def on_print(self, args: dict):
|
||||
super(FactorioContext, self).on_print(args)
|
||||
|
@ -134,6 +125,8 @@ async def game_watcher(ctx: FactorioContext):
|
|||
research_data = data["research_done"]
|
||||
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
|
||||
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:
|
||||
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)
|
||||
if death_link_tick != ctx.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)
|
||||
|
||||
|
@ -234,14 +228,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||
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"))
|
||||
ctx.auth = info["slot_name"]
|
||||
ctx.seed_name = info["seed_name"]
|
||||
# 0.2.0 addition, not present earlier
|
||||
death_link = bool(info.get("death_link", False))
|
||||
if death_link:
|
||||
ctx.tags.add("DeathLink")
|
||||
await ctx.update_death_link(death_link)
|
||||
|
||||
|
||||
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)
|
||||
if ctx.mod_version == ctx.__class__.mod_version:
|
||||
raise Exception("No Archipelago mod was loaded. Aborting.")
|
||||
get_info(ctx, rcon_client)
|
||||
await get_info(ctx, rcon_client)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
except Exception as e:
|
||||
|
@ -322,14 +315,7 @@ async def main(args):
|
|||
await progression_watcher
|
||||
await factorio_server_task
|
||||
|
||||
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
|
||||
await ctx.shutdown()
|
||||
|
||||
if 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:
|
||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
||||
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:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
else:
|
||||
|
@ -126,7 +127,7 @@ def main(args=None, callback=ERmain):
|
|||
erargs.outputname = seed_name
|
||||
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.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)
|
||||
|
||||
if meta_weights:
|
||||
for player, path in player_path_cache.items():
|
||||
weights_cache[path].setdefault("meta_ignore", [])
|
||||
for key in meta_weights:
|
||||
option = get_choice(key, meta_weights)
|
||||
if option is not None:
|
||||
for player, path in player_path_cache.items():
|
||||
players_meta = weights_cache[path].get("meta_ignore", [])
|
||||
if key not in players_meta:
|
||||
weights_cache[path][key] = option
|
||||
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
|
||||
weights_cache[path][key] = option
|
||||
for category_name, category_dict in meta_weights.items():
|
||||
for key in category_dict:
|
||||
option = get_choice(key, category_dict)
|
||||
if option is not None:
|
||||
for player, path in player_path_cache.items():
|
||||
if category_name is None:
|
||||
weights_cache[path][key] = option
|
||||
elif category_name not in weights_cache[path]:
|
||||
raise Exception(f"Meta: Category {category_name} is not present in {path}.")
|
||||
else:
|
||||
weights_cache[path][category_name][key] = option
|
||||
|
||||
name_counter = Counter()
|
||||
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:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
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'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
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:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
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))
|
||||
|
||||
|
||||
async def server(websocket, path, ctx: Context):
|
||||
async def server(websocket, path: str = "/", ctx: Context = None):
|
||||
client = Client(websocket, ctx)
|
||||
ctx.endpoints.append(client)
|
||||
|
||||
|
@ -591,10 +591,12 @@ def get_status_string(ctx: Context, team: int):
|
|||
text = "Player Status on your team:"
|
||||
for slot in ctx.locations:
|
||||
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])})"
|
||||
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 "."
|
||||
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
|
||||
|
||||
|
||||
|
@ -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]):
|
||||
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:
|
||||
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
for location in new_locations:
|
||||
if location in ctx.locations[slot]:
|
||||
item_id, target_player = ctx.locations[slot][location]
|
||||
new_item = NetworkItem(item_id, location, slot)
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
get_received_items(ctx, team, target_player).append(new_item)
|
||||
item_id, target_player = ctx.locations[slot][location]
|
||||
new_item = NetworkItem(item_id, location, slot)
|
||||
if target_player != slot or slot in ctx.remote_items:
|
||||
get_received_items(ctx, team, target_player).append(new_item)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
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)))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
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)))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
ctx.location_checks[team, slot] |= new_locations
|
||||
send_new_items(ctx)
|
||||
ctx.broadcast(ctx.clients[team][slot], [{
|
||||
"cmd": "RoomUpdate",
|
||||
"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()
|
||||
|
@ -1242,6 +1244,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||
game = ctx.games[slot]
|
||||
if "IgnoreGame" not in args["tags"] and args['game'] != game:
|
||||
errors.add('InvalidGame')
|
||||
minver = ctx.minimum_client_versions[slot]
|
||||
if minver > args['version']:
|
||||
errors.add('IncompatibleVersion')
|
||||
|
||||
# only exact version match allowed
|
||||
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.team = team
|
||||
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.clients[team][slot].append(client)
|
||||
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)
|
||||
|
||||
elif cmd == "GetDataPackage":
|
||||
exclusions = set(args.get("exclusions", []))
|
||||
exclusions = args.get("exclusions", [])
|
||||
if exclusions:
|
||||
exclusions = set(exclusions)
|
||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
||||
if name not in exclusions}
|
||||
package = network_data_package.copy()
|
||||
|
@ -1680,7 +1684,7 @@ async def main(args: argparse.Namespace):
|
|||
|
||||
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)
|
||||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
|
|
|
@ -379,6 +379,7 @@ class StartHints(ItemSet):
|
|||
|
||||
|
||||
class StartLocationHints(OptionSet):
|
||||
"""Start with these locations and their item prefilled into the !hint command"""
|
||||
displayname = "Start Location Hints"
|
||||
|
||||
|
||||
|
@ -399,7 +400,7 @@ per_game_common_options = {
|
|||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"start_location_hints": StartLocationHints,
|
||||
"exclude_locations": OptionSet
|
||||
"exclude_locations": ExcludeLocations
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
59
Patch.py
59
Patch.py
|
@ -1,3 +1,5 @@
|
|||
# TODO: convert this into a system like AutoWorld
|
||||
|
||||
import bsdiff4
|
||||
import yaml
|
||||
import os
|
||||
|
@ -14,16 +16,25 @@ current_patch_version = 3
|
|||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
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:
|
||||
if game == GAME_ALTTP:
|
||||
from worlds.alttp.Rom import JAP10HASH
|
||||
from worlds.alttp.Rom import JAP10HASH as HASH
|
||||
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:
|
||||
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": 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
|
||||
"compatible_version": 3,
|
||||
"version": current_patch_version,
|
||||
"base_checksum": JAP10HASH})
|
||||
"base_checksum": HASH})
|
||||
return patch.encode(encoding="utf-8-sig")
|
||||
|
||||
|
||||
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:
|
||||
metadata = {}
|
||||
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
|
||||
patch = bsdiff4.diff(get_base_rom_data(game), rom)
|
||||
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]:
|
||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||
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:
|
||||
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)]
|
||||
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
|
||||
target = os.path.splitext(patch_file)[0] + ".sfc"
|
||||
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]:
|
||||
data, target, patched_data = create_rom_bytes(patch_file)
|
||||
with open(target, "wb") as f:
|
||||
|
|
99
SNIClient.py
99
SNIClient.py
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import multiprocessing
|
||||
|
@ -14,7 +15,7 @@ from json import loads, dumps
|
|||
from Utils import get_item_name_from_id, init_logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging("SNIClient")
|
||||
init_logging("SNIClient", exception_logger="Client")
|
||||
|
||||
import colorama
|
||||
|
||||
|
@ -72,7 +73,7 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
|||
pass
|
||||
|
||||
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
|
||||
|
||||
def _cmd_snes_close(self) -> bool:
|
||||
|
@ -84,20 +85,17 @@ class LttPCommandProcessor(ClientCommandProcessor):
|
|||
else:
|
||||
return False
|
||||
|
||||
def _cmd_snes_write(self, address, data):
|
||||
"""Write the specified byte (base10) to the SNES' memory address (base16)."""
|
||||
if self.ctx.snes_state != SNESState.SNES_ATTACHED:
|
||||
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))
|
||||
self.output("Data Sent")
|
||||
return True
|
||||
|
||||
def _cmd_test_death(self):
|
||||
self.ctx.on_deathlink({"source": "Console",
|
||||
"time": time.time()})
|
||||
# Left here for quick re-addition for debugging.
|
||||
# def _cmd_snes_write(self, address, data):
|
||||
# """Write the specified byte (base10) to the SNES' memory address (base16)."""
|
||||
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
|
||||
# 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))
|
||||
# self.output("Data Sent")
|
||||
# return True
|
||||
|
||||
|
||||
class Context(CommonContext):
|
||||
|
@ -145,12 +143,7 @@ class Context(CommonContext):
|
|||
self.awaiting_rom = False
|
||||
self.auth = self.rom
|
||||
auth = base64.b64encode(self.rom).decode()
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
|
||||
'tags': self.tags,
|
||||
'uuid': Utils.get_unique_identifier(),
|
||||
'game': self.game
|
||||
}])
|
||||
await self.send_connect(name=auth)
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
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:
|
||||
ctx.finished_game = False
|
||||
gameName = await snes_read(ctx, SM_ROMNAME_START, 2)
|
||||
if gameName is None:
|
||||
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
|
||||
if game_name is None:
|
||||
continue
|
||||
elif gameName == b"SM":
|
||||
elif game_name == b"SM":
|
||||
ctx.game = GAME_SM
|
||||
else:
|
||||
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
|
||||
SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
if death_link:
|
||||
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}])
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
|
||||
ctx.locations_checked = set()
|
||||
ctx.locations_scouted = set()
|
||||
|
@ -1083,14 +1069,24 @@ async def main():
|
|||
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||
args.connect = meta["server"]
|
||||
logging.info(f"Wrote rom file to {romfile}")
|
||||
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))
|
||||
if args.diff_file.endswith(".apsoe"):
|
||||
import webbrowser
|
||||
webbrowser.open("http://www.evermizer.com/apclient/")
|
||||
logging.info("Starting Evermizer Client in your Browser...")
|
||||
import time
|
||||
time.sleep(3)
|
||||
sys.exit()
|
||||
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)
|
||||
if ctx.server_task is None:
|
||||
|
@ -1105,28 +1101,19 @@ async def main():
|
|||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
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")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
if snes_connect_task:
|
||||
snes_connect_task.cancel()
|
||||
|
||||
ctx.server_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:
|
||||
await ctx.snes_socket.close()
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
if snes_connect_task:
|
||||
snes_connect_task.cancel()
|
||||
await watcher_task
|
||||
await ctx.shutdown()
|
||||
|
||||
if 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)
|
||||
|
||||
|
||||
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
|
||||
def get_public_ipv4() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
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:
|
||||
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:
|
||||
logging.exception(e)
|
||||
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 urllib.request
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
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:
|
||||
logging.exception(e)
|
||||
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",
|
||||
"rom_start": True,
|
||||
},
|
||||
"soe_options": {
|
||||
"rom_file": "Secret of Evermore (USA).sfc",
|
||||
},
|
||||
"lttp_options": {
|
||||
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
|
||||
"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",
|
||||
log_format: str = "[%(name)s]: %(message)s"):
|
||||
log_format: str = "[%(name)s]: %(message)s", exception_logger: str = ""):
|
||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||
log_folder = local_path("logs")
|
||||
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(
|
||||
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)
|
||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||
commit()
|
||||
return redirect(url_for("hostRoom", room=room.id))
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
|
||||
|
||||
def _read_log(path: str):
|
||||
|
@ -159,7 +159,7 @@ def display_log(room: UUID):
|
|||
|
||||
|
||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||
def hostRoom(room: UUID):
|
||||
def host_room(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
|
@ -175,20 +175,17 @@ def hostRoom(room: UUID):
|
|||
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')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
|
||||
@app.route('/discord')
|
||||
def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
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.check import get_yaml_data, roll_options
|
||||
from WebHostLib.generate import get_meta
|
||||
|
||||
|
||||
@api_endpoints.route('/generate', methods=['POST'])
|
||||
|
@ -35,9 +36,6 @@ def generate_api():
|
|||
if "race" in json_data:
|
||||
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:
|
||||
return {"text": "No options found. Expected file attachment or json weights."
|
||||
}, 400
|
||||
|
@ -45,7 +43,8 @@ def generate_api():
|
|||
if len(options) > app.config["MAX_ROLL"]:
|
||||
return {"text": "Max size of multiworld exceeded",
|
||||
"detail": app.config["MAX_ROLL"]}, 409
|
||||
|
||||
meta = get_meta(meta_options_source)
|
||||
meta["race"] = race
|
||||
results, gen_options = roll_options(options)
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return {"text": str(results),
|
||||
|
@ -54,7 +53,7 @@ def generate_api():
|
|||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# 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"])
|
||||
commit()
|
||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from flask import send_file, Response, render_template
|
||||
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
|
||||
import zipfile
|
||||
|
||||
|
||||
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
|
||||
def download_patch(room_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 = 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)
|
||||
|
||||
|
||||
|
@ -28,23 +30,6 @@ def download_spoiler(seed_id):
|
|||
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>")
|
||||
def download_slot_file(room_id, player_id: int):
|
||||
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
|
||||
|
||||
|
||||
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/<race>', methods=['GET', 'POST'])
|
||||
def generate(race=False):
|
||||
|
@ -35,9 +45,9 @@ def generate(race=False):
|
|||
else:
|
||||
results, gen_options = roll_options(options)
|
||||
# get form data -> server settings
|
||||
hint_cost = int(request.form.get("hint_cost", 10))
|
||||
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
||||
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
||||
meta = get_meta(request.form)
|
||||
meta["race"] = race
|
||||
|
||||
if race:
|
||||
meta["item_cheat"] = False
|
||||
meta["remaining"] = False
|
||||
|
|
|
@ -40,7 +40,7 @@ class Seed(db.Entity):
|
|||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||
slots = Set(Slot)
|
||||
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):
|
||||
|
@ -53,5 +53,5 @@ class Generation(db.Entity):
|
|||
id = PrimaryKey(UUID, default=uuid4)
|
||||
owner = Required(UUID)
|
||||
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)
|
||||
|
|
|
@ -49,7 +49,7 @@ def create():
|
|||
game_options = {}
|
||||
for option_name, option in world.options.items():
|
||||
if option.options:
|
||||
this_option = {
|
||||
game_options[option_name] = this_option = {
|
||||
"type": "select",
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
|
@ -66,7 +66,10 @@ def create():
|
|||
if sub_option_id == option.default:
|
||||
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"):
|
||||
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.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
@ -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.
|
||||
|
||||
## Hosting a multiworld
|
||||
|
||||
### 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
|
||||
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
|
||||
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
|
||||
1. Ensure your Archipelago Client is running.
|
||||
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"tutorials": [
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"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",
|
||||
"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": [
|
||||
{
|
||||
"language": "English",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# A Link to the Past Randomizer Setup Guide
|
||||
|
||||
## 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)
|
||||
- If installing Archipelago, make sure to check the box for LttPClient during install, or SNI will not be included
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and LttPClient)
|
||||
- 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 SNIClient)
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
|
@ -76,7 +76,7 @@ Firewall.
|
|||
4. In the new window, click **Browse...**
|
||||
5. Select the connector lua file included with your client
|
||||
- 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
|
||||
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.
|
||||
5. Select the `sniConnector.lua` file you downloaded above
|
||||
- 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
|
||||
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?
|
||||
|
||||
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
|
||||
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
|
||||
|
|
|
@ -34,7 +34,14 @@
|
|||
<table>
|
||||
<tbody>
|
||||
<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>
|
||||
<select name="forfeit_mode" id="forfeit_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
|
@ -46,12 +53,49 @@
|
|||
</td>
|
||||
</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>
|
||||
<td>
|
||||
<label for="hint_cost"> Hint Cost:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||
class="interactive"
|
||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||
to get the location of that hint item.">(?)
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
later,
|
||||
you can simply refresh this page and the server will be started again.<br>
|
||||
{% 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 %}
|
||||
{{ macros.list_patches_room(room) }}
|
||||
{% if room.owner == session["_id"] %}
|
||||
|
|
|
@ -28,34 +28,16 @@
|
|||
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
|
||||
</tr>
|
||||
{% 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>
|
||||
<td>Files: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for slot in seed.slots %}
|
||||
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
{% call macros.list_rooms(rooms) %}
|
||||
<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>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endcall %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -10,10 +10,7 @@ from pony.orm import flush, select
|
|||
|
||||
from WebHostLib import app, Seed, Room, Slot
|
||||
from Utils import parse_yaml
|
||||
|
||||
accepted_zip_contents = {"patches": ".apbp",
|
||||
"spoiler": ".txt",
|
||||
"multidata": ".archipelago"}
|
||||
from Patch import preferred_endings
|
||||
|
||||
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):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
elif file.filename.endswith(".apbp"):
|
||||
elif file.filename.endswith(tuple(preferred_endings.values())):
|
||||
data = zfile.open(file, "r").read()
|
||||
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
|
||||
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"]
|
||||
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"],
|
||||
game="A Link to the Past"))
|
||||
game=yaml_data["game"]))
|
||||
|
||||
elif file.filename.endswith(".apmc"):
|
||||
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)
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
multidata = zfile.open(file).read()
|
||||
multidata = None
|
||||
|
||||
if multidata:
|
||||
flush() # commit slots
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<TabbedPanel>
|
||||
tab_width: 200
|
||||
<Row@Label>:
|
||||
<SelectableLabel>:
|
||||
canvas.before:
|
||||
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:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
|
@ -13,10 +13,10 @@
|
|||
font_size: dp(20)
|
||||
markup: True
|
||||
<UILog>:
|
||||
viewclass: 'Row'
|
||||
viewclass: 'SelectableLabel'
|
||||
scroll_y: 0
|
||||
effect_cls: "ScrollEffect"
|
||||
RecycleBoxLayout:
|
||||
SelectableRecycleBoxLayout:
|
||||
default_size: None, dp(20)
|
||||
default_size_hint: 1, 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. |
|
||||
| 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. |
|
||||
|
||||
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
|
||||
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
||||
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: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; 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/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
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; Flags: disablenouninstallwarning
|
||||
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; Flags: disablenouninstallwarning
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; 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; Flags: disablenouninstallwarning
|
||||
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/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
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/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
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
|
||||
|
||||
|
||||
|
||||
[Code]
|
||||
const
|
||||
SHCONTCH_NOPROGRESSBOX = 4;
|
||||
|
@ -221,6 +220,19 @@ var OoTROMFilePage: TInputFileWizardPage;
|
|||
|
||||
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;
|
||||
var rom: string;
|
||||
begin
|
||||
|
@ -229,8 +241,8 @@ begin
|
|||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetMD5OfFile(rom), hash) = 0 then
|
||||
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
|
@ -317,7 +329,16 @@ begin
|
|||
MinecraftDownloadPage.Hide;
|
||||
end;
|
||||
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;
|
||||
end;
|
||||
|
||||
|
@ -327,7 +348,7 @@ begin
|
|||
Result := lttprom
|
||||
else if Assigned(LttPRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
if R <> 0 then
|
||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
|
@ -343,7 +364,7 @@ begin
|
|||
Result := smrom
|
||||
else if Assigned(SMRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||
if R <> 0 then
|
||||
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
|
@ -359,8 +380,7 @@ begin
|
|||
Result := soerom
|
||||
else if Assigned(SoERomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||
log(GetMD5OfFile(SoEROMFilePage.Values[0]))
|
||||
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||
if R <> 0 then
|
||||
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
|
@ -374,7 +394,7 @@ function GetOoTROMPath(Param: string): string;
|
|||
begin
|
||||
if Length(ootrom) > 0 then
|
||||
Result := ootrom
|
||||
else if Assigned(OoTROMFilePage) then
|
||||
else if (Assigned(OoTROMFilePage)) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
|
||||
if R <> 0 then
|
||||
|
@ -417,4 +437,4 @@ begin
|
|||
Result := not (WizardIsComponentSelected('generator/soe'));
|
||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||
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
|
||||
|
||||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
|
||||
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: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; 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/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
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; Flags: disablenouninstallwarning
|
||||
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; Flags: disablenouninstallwarning
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; 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; Flags: disablenouninstallwarning
|
||||
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/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
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/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
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
|
||||
|
||||
|
||||
|
||||
[Code]
|
||||
const
|
||||
SHCONTCH_NOPROGRESSBOX = 4;
|
||||
|
@ -221,6 +220,19 @@ var OoTROMFilePage: TInputFileWizardPage;
|
|||
|
||||
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;
|
||||
var rom: string;
|
||||
begin
|
||||
|
@ -229,8 +241,8 @@ begin
|
|||
if Length(rom) > 0 then
|
||||
begin
|
||||
log('existing ROM found');
|
||||
log(IntToStr(CompareStr(GetMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetMD5OfFile(rom), hash) = 0 then
|
||||
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
|
||||
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
|
||||
begin
|
||||
log('existing ROM verified');
|
||||
Result := rom;
|
||||
|
@ -317,7 +329,16 @@ begin
|
|||
MinecraftDownloadPage.Hide;
|
||||
end;
|
||||
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;
|
||||
end;
|
||||
|
||||
|
@ -327,7 +348,7 @@ begin
|
|||
Result := lttprom
|
||||
else if Assigned(LttPRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
|
||||
if R <> 0 then
|
||||
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
|
@ -343,7 +364,7 @@ begin
|
|||
Result := smrom
|
||||
else if Assigned(SMRomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
|
||||
if R <> 0 then
|
||||
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
|
@ -359,8 +380,7 @@ begin
|
|||
Result := soerom
|
||||
else if Assigned(SoERomFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||
log(GetMD5OfFile(SoEROMFilePage.Values[0]))
|
||||
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
|
||||
if R <> 0 then
|
||||
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 typing
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
|
@ -11,6 +10,8 @@ os.environ["KIVY_LOG_ENABLE"] = "0"
|
|||
|
||||
from kivy.app import App
|
||||
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.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty
|
||||
|
@ -25,6 +26,10 @@ from kivy.uix.label import Label
|
|||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.utils import escape_markup
|
||||
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
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart
|
||||
|
@ -140,6 +145,46 @@ class ContainerLayout(FloatLayout):
|
|||
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):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
|
@ -164,7 +209,8 @@ class GameManager(App):
|
|||
# top part
|
||||
server_label = ServerLabel()
|
||||
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)
|
||||
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)
|
||||
|
@ -201,33 +247,21 @@ class GameManager(App):
|
|||
info_button = Button(height=30, text="Command:", size_hint_x=None)
|
||||
info_button.bind(on_release=self.command_button_action)
|
||||
bottom_layout.add_widget(info_button)
|
||||
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
|
||||
textinput.bind(on_text_validate=self.on_message)
|
||||
bottom_layout.add_widget(textinput)
|
||||
self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False)
|
||||
self.textinput.bind(on_text_validate=self.on_message)
|
||||
|
||||
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.commandprocessor("/help")
|
||||
Clock.schedule_interval(self.update_texts, 1 / 30)
|
||||
self.container.add_widget(self.grid)
|
||||
self.catch_unhandled_exceptions()
|
||||
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):
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
|
@ -242,7 +276,11 @@ class GameManager(App):
|
|||
self.progressbar.value = 0
|
||||
|
||||
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):
|
||||
if self.ctx.server:
|
||||
|
@ -269,6 +307,9 @@ class GameManager(App):
|
|||
self.ctx.input_queue.put_nowait(input_text)
|
||||
elif input_text:
|
||||
self.commandprocessor(input_text)
|
||||
|
||||
Clock.schedule_once(textinput.text_focus)
|
||||
|
||||
except Exception as e:
|
||||
logging.getLogger("Client").exception(e)
|
||||
|
||||
|
@ -304,7 +345,7 @@ class TextManager(GameManager):
|
|||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||
super(LogtoUI, self).__init__(logging.INFO)
|
||||
self.on_log = on_log
|
||||
|
||||
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
|
||||
# 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
|
||||
# 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
|
||||
null:
|
||||
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
|
||||
inverted: 10
|
||||
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.
|
||||
extra: 0 # available = triforce_pieces_extra + 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",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package"}
|
||||
"network_data_package",
|
||||
"AutoWorldRegister"}
|
||||
|
||||
# import all submodules to trigger AutoWorldRegister
|
||||
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}\n \nJust walk away\n >",
|
||||
"{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}\nI heard beans\non toast is a\ngreat meal. >",
|
||||
"{C:GREEN}\n> Sweetcorn\non pizza is a\ngreat choice.",
|
||||
|
|
|
@ -47,10 +47,16 @@ recipe_time_scales = {
|
|||
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):
|
||||
player = world.player
|
||||
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:
|
||||
if not data_final_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")
|
||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||
control_template = template_env.get_template("control.lua")
|
||||
settings_template = template_env.get_template("settings.lua")
|
||||
# get data for templates
|
||||
player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
|
||||
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(),
|
||||
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
|
||||
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
|
||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
|
||||
"slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player,
|
||||
"starting_items": multiworld.starting_items[player], "recipes": recipes,
|
||||
"random": random, "flop_random": flop_random,
|
||||
"static_nodes": multiworld.worlds[player].static_nodes,
|
||||
"recipe_time_scale": recipe_time_scales[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},
|
||||
"progressive_technology_table": {tech.name : tech.progressive for tech in
|
||||
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:
|
||||
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)
|
||||
data_template_code = data_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__)
|
||||
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)
|
||||
with open(os.path.join(mod_dir, "control.lua"), "wt") as f:
|
||||
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)
|
||||
with open(os.path.join(en_locale_dir, "locale.cfg"), "wt") as f:
|
||||
f.write(locale_content)
|
||||
|
|
|
@ -55,6 +55,14 @@ class Silo(Choice):
|
|||
default = 0
|
||||
|
||||
|
||||
class Satellite(Choice):
|
||||
"""Ingredients to craft satellite."""
|
||||
displayname = "Satellite"
|
||||
option_vanilla = 0
|
||||
option_randomize_recipe = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class FreeSamples(Choice):
|
||||
"""Get free items with your technologies."""
|
||||
displayname = "Free Samples"
|
||||
|
@ -91,13 +99,25 @@ class TechTreeInformation(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"
|
||||
option_vanilla = 0
|
||||
option_fast = 1
|
||||
option_normal = 2
|
||||
option_slow = 4
|
||||
option_chaos = 5
|
||||
option_new_fast = 6
|
||||
option_new_normal = 7
|
||||
option_new_slow = 8
|
||||
|
||||
|
||||
class Progressive(Choice):
|
||||
|
@ -289,6 +309,7 @@ factorio_options: typing.Dict[str, type(Option)] = {
|
|||
"tech_tree_layout": TechTreeLayout,
|
||||
"tech_cost": TechCost,
|
||||
"silo": Silo,
|
||||
"satellite": Satellite,
|
||||
"free_samples": FreeSamples,
|
||||
"tech_tree_information": TechTreeInformation,
|
||||
"starting_items": FactorioStartItems,
|
||||
|
|
|
@ -59,8 +59,8 @@ class Technology(FactorioElement): # maybe make subclass of Location?
|
|||
def build_rule(self, player: int):
|
||||
logging.debug(f"Building rules for {self.name}")
|
||||
|
||||
return lambda state, technologies=technologies: all(state.has(f"Automated {ingredient}", player)
|
||||
for ingredient in self.ingredients)
|
||||
return lambda state: all(state.has(f"Automated {ingredient}", player)
|
||||
for ingredient in self.ingredients)
|
||||
|
||||
def get_prior_technologies(self) -> Set[Technology]:
|
||||
"""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(
|
||||
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
|
||||
|
||||
advancement_technologies: Set[str] = set()
|
||||
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]:
|
||||
def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_recipe: Recipe) -> Set[str]:
|
||||
techs = set()
|
||||
if silo_recipe:
|
||||
for ingredient in silo_recipe.ingredients:
|
||||
techs |= recursively_get_unlocking_technologies(ingredient)
|
||||
for ingredient in part_recipe.ingredients:
|
||||
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}
|
||||
|
||||
|
||||
|
@ -335,8 +333,6 @@ rocket_recipes = {
|
|||
{"copper-cable": 10, "iron-plate": 10, "wood": 10}
|
||||
}
|
||||
|
||||
advancement_technologies |= {tech.name for tech in required_technologies["rocket-silo"]}
|
||||
|
||||
# progressive technologies
|
||||
# auto-progressive
|
||||
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))
|
||||
progressive_tech_table[root] = progressive_technology.factorio_id
|
||||
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] = {}
|
||||
for technology in progressive_technology_table.values():
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import collections
|
||||
import typing
|
||||
|
||||
from ..AutoWorld import World
|
||||
|
||||
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, \
|
||||
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
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, Silo, TechTreeInformation
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -32,13 +33,17 @@ class Factorio(World):
|
|||
game: str = "Factorio"
|
||||
static_nodes = {"automation", "logistics", "rocket-silo"}
|
||||
custom_recipes = {}
|
||||
additional_advancement_technologies = set()
|
||||
advancement_technologies: typing.Set[str]
|
||||
|
||||
item_name_to_id = all_items
|
||||
location_name_to_id = base_tech_table
|
||||
|
||||
data_version = 5
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
super(Factorio, self).__init__(world, player)
|
||||
self.advancement_technologies = set()
|
||||
|
||||
def generate_basic(self):
|
||||
player = self.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))
|
||||
|
||||
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
|
||||
else self.custom_recipes["rocket-silo"] \
|
||||
if "rocket-silo" in self.custom_recipes \
|
||||
else self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
|
||||
else next(iter(all_product_sources.get("rocket-silo")))
|
||||
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)
|
||||
for technology in
|
||||
victory_tech_names)
|
||||
|
@ -189,12 +196,12 @@ class Factorio(World):
|
|||
fallback_pool = []
|
||||
|
||||
# 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:
|
||||
max_raw = 1.1 * remaining_raw
|
||||
min_raw = 0.9 * remaining_raw
|
||||
max_energy = 1.1 * remaining_energy
|
||||
min_energy = 1.1 * remaining_energy
|
||||
min_energy = 0.9 * remaining_energy
|
||||
else:
|
||||
max_raw = remaining_raw * 0.75
|
||||
min_raw = (remaining_raw - max_raw) / remaining_num_ingredients
|
||||
|
@ -226,7 +233,7 @@ class Factorio(World):
|
|||
|
||||
# fill failed slots with whatever we got
|
||||
pool = fallback_pool
|
||||
while remaining_num_ingredients > 0 and len(pool) > 0:
|
||||
while remaining_num_ingredients > 0 and pool:
|
||||
ingredient = pool.pop()
|
||||
if ingredient not in recipes:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
|
@ -264,8 +271,6 @@ class Factorio(World):
|
|||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
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]:
|
||||
valid_pool = []
|
||||
|
@ -278,31 +283,45 @@ class Factorio(World):
|
|||
for _ in original.ingredients:
|
||||
new_ingredients[valid_pool.pop()] = 1
|
||||
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
|
||||
|
||||
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 = []
|
||||
for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()):
|
||||
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)
|
||||
self.additional_advancement_technologies |= {tech.name for tech in
|
||||
new_recipe.recursive_unlocking_technologies}
|
||||
self.custom_recipes["rocket-silo"] = new_recipe
|
||||
|
||||
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool.copy(),
|
||||
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||
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
|
||||
prog_add = set()
|
||||
for tech in self.additional_advancement_technologies:
|
||||
for tech in self.advancement_technologies:
|
||||
if tech in tech_to_progressive_lookup:
|
||||
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:
|
||||
if name in tech_table:
|
||||
return FactorioItem(name, name in advancement_technologies or
|
||||
name in self.additional_advancement_technologies,
|
||||
return FactorioItem(name, name in self.advancement_technologies,
|
||||
tech_table[name], self.player)
|
||||
|
||||
item = FactorioItem(name, False, all_items[name], self.player)
|
||||
|
|
|
@ -8,7 +8,14 @@ SLOT_NAME = "{{ slot_name }}"
|
|||
SEED_NAME = "{{ seed_name }}"
|
||||
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
|
||||
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
|
||||
|
||||
|
@ -76,6 +83,27 @@ function on_force_destroyed(event)
|
|||
global.forcedata[event.force.name] = nil
|
||||
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
|
||||
-- added.`
|
||||
function on_player_created(event)
|
||||
|
@ -107,8 +135,19 @@ end
|
|||
script.on_event(defines.events.on_player_removed, on_player_removed)
|
||||
|
||||
function on_rocket_launched(event)
|
||||
global.forcedata[event.rocket.force.name]['victory'] = 1
|
||||
dumpInfo(event.rocket.force)
|
||||
if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then
|
||||
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
|
||||
script.on_event(defines.events.on_rocket_launched, on_rocket_launched)
|
||||
|
||||
|
@ -198,6 +237,10 @@ script.on_init(function()
|
|||
e.player_index = index
|
||||
on_player_created(e)
|
||||
end
|
||||
|
||||
if remote.interfaces["silo_script"] then
|
||||
remote.call("silo_script", "set_no_victory", true)
|
||||
end
|
||||
end)
|
||||
|
||||
-- hook into researches done
|
||||
|
@ -366,18 +409,19 @@ function spawn_entity(surface, force, name, x, y, radius, randomize, avoid_ores)
|
|||
end
|
||||
|
||||
|
||||
if DEATH_LINK == 1 then
|
||||
script.on_event(defines.events.on_entity_died, function(event)
|
||||
if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event
|
||||
return
|
||||
end
|
||||
script.on_event(defines.events.on_entity_died, function(event)
|
||||
if DEATH_LINK == 0 then
|
||||
return
|
||||
end
|
||||
if CURRENTLY_DEATH_LOCK == 1 then -- don't re-trigger on same event
|
||||
return
|
||||
end
|
||||
|
||||
local force = event.entity.force
|
||||
global.forcedata[force.name].death_link_tick = game.tick
|
||||
dumpInfo(force)
|
||||
kill_players(force)
|
||||
end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}})
|
||||
end
|
||||
local force = event.entity.force
|
||||
global.forcedata[force.name].death_link_tick = game.tick
|
||||
dumpInfo(force)
|
||||
kill_players(force)
|
||||
end, {LuaEntityDiedEventFilter = {["filter"] = "name", ["name"] = "character"}})
|
||||
|
||||
|
||||
-- add / commands
|
||||
|
@ -392,7 +436,8 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
|
|||
local data_collection = {
|
||||
["research_done"] = research_done,
|
||||
["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
|
||||
|
@ -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.play_sound({path="utility/research_completed"})
|
||||
tech.researched = true
|
||||
return
|
||||
end
|
||||
return
|
||||
elseif progressive_technologies[item_name] ~= nil then
|
||||
if global.index_sync[index] == nil then -- not yet received prog item
|
||||
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
|
||||
tech = force.technologies[item_name]
|
||||
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
|
||||
if tech.researched ~= true then
|
||||
game.print({"", "Received [technology=" .. tech.name .. "] from ", source})
|
||||
|
|
|
@ -100,6 +100,20 @@ function adjust_energy(recipe_name, factor)
|
|||
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-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)
|
||||
|
@ -144,6 +158,12 @@ data:extend{new_tree_copy}
|
|||
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
||||
{%- endif %}
|
||||
{%- 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 %}
|
||||
|
||||
{%- if silo==2 %}
|
||||
|
|
|
@ -22,4 +22,10 @@ ap-{{ tech_table[original_tech_name] }}-=Researching this technology sends somet
|
|||
{%- 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 }}".
|
||||
{%- 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
|
||||
# ER and glitched logic are not compatible; glitched takes priority
|
||||
if self.logic_rules == 'glitched':
|
||||
self.shuffle_interior_entrances = False
|
||||
self.shuffle_interior_entrances = 'off'
|
||||
self.shuffle_grotto_entrances = False
|
||||
self.shuffle_dungeon_entrances = False
|
||||
self.shuffle_overworld_entrances = False
|
||||
|
|
|
@ -2,7 +2,7 @@ import Utils
|
|||
from Patch import read_rom
|
||||
|
||||
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
ROM_PLAYER_LIMIT = 255
|
||||
ROM_PLAYER_LIMIT = 65535
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
|
|
@ -159,6 +159,9 @@ class SMWorld(World):
|
|||
|
||||
def getWord(self, w):
|
||||
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
|
||||
class DummyLocation:
|
||||
|
@ -232,7 +235,10 @@ class SMWorld(World):
|
|||
multiWorldItems = {}
|
||||
idx = 0
|
||||
itemId = 0
|
||||
self.playerIDMap = {}
|
||||
playerIDCount = 0 # 0 is for "Archipelago" server
|
||||
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.item.type in ItemManager.Items:
|
||||
itemId = ItemManager.Items[itemLoc.item.type].Id
|
||||
|
@ -240,12 +246,21 @@ class SMWorld(World):
|
|||
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
||||
multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name)
|
||||
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)
|
||||
(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)
|
||||
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"]
|
||||
idx = 0
|
||||
|
@ -260,21 +275,24 @@ class SMWorld(World):
|
|||
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
|
||||
|
||||
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,
|
||||
'MultiWorldItems': multiWorldItems,
|
||||
'offworldSprites': offworldSprites,
|
||||
'openTourianGreyDoors': openTourianGreyDoors,
|
||||
'deathLink': deathLink}
|
||||
'deathLink': deathLink,
|
||||
'PlayerName': playerNames,
|
||||
'PlayerNameIDMap': playerNameIDMap}
|
||||
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
|
||||
# 21 bytes
|
||||
from Main import __version__
|
||||
|
|
Binary file not shown.
|
@ -2,16 +2,15 @@ import os, importlib
|
|||
from logic.logic import Logic
|
||||
from patches.common.patches import patches, additional_PLMs
|
||||
from utils.parameters import appDir
|
||||
from Utils import is_frozen
|
||||
|
||||
class PatchAccess(object):
|
||||
def __init__(self):
|
||||
# load all ips patches
|
||||
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):
|
||||
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):
|
||||
self.patchesPath[patch] = logicDir
|
||||
|
||||
|
|
|
@ -327,7 +327,7 @@ class VariaRandomizer:
|
|||
preset = loadRandoPreset(world, self.player, args)
|
||||
# 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:
|
||||
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 args.paramsFileName is not None:
|
||||
|
@ -352,7 +352,7 @@ class VariaRandomizer:
|
|||
sys.exit(-1)
|
||||
else:
|
||||
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
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# the different difficulties available
|
||||
easy = 1
|
||||
|
@ -60,7 +61,7 @@ def diff4solver(difficulty):
|
|||
return "mania"
|
||||
|
||||
# allow multiple local repo
|
||||
appDir = sys.path[0]
|
||||
appDir = Path(__file__).parents[4]
|
||||
|
||||
def isKnows(knows):
|
||||
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 easy, medium, hard, harder, hardcore, mania, text2diff
|
||||
from logic.smbool import SMBool
|
||||
|
||||
from Utils import is_frozen
|
||||
|
||||
def isStdPreset(preset):
|
||||
return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021']
|
||||
|
||||
def getPresetDir(preset):
|
||||
def getPresetDir(preset) -> str:
|
||||
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:
|
||||
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):
|
||||
return re.sub('[{}]+'.format(toRemove), '', string)
|
||||
|
|
|
@ -5,6 +5,7 @@ from Utils import get_options, output_path
|
|||
import typing
|
||||
import lzma
|
||||
import os
|
||||
import os.path
|
||||
import threading
|
||||
|
||||
try:
|
||||
|
@ -200,11 +201,18 @@ class SoEWorld(World):
|
|||
line = f'{loc.type},{loc.index}:{item.type},{item.index}\n'
|
||||
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,
|
||||
flags, money, exp)):
|
||||
if not os.path.exists(rom_file):
|
||||
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()
|
||||
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:
|
||||
raise
|
||||
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 1', 1337089),
|
||||
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', 'Rats guarded chest', 1337093),
|
||||
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', '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', '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', 'Above the gap', 1337143),
|
||||
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:
|
||||
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:
|
||||
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 (elevator)', 'Varndagroth tower right (upper)')
|
||||
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)', '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))
|
||||
|
|
Loading…
Reference in New Issue