WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									181cc47079
								
							
						
					
					
						commit
						c1e9d0ab4f
					
				
							
								
								
									
										165
									
								
								MultiServer.py
								
								
								
								
							
							
						
						
									
										165
									
								
								MultiServer.py
								
								
								
								
							|  | @ -30,13 +30,8 @@ except ImportError: | |||
|     OperationalError = ConnectionError | ||||
| 
 | ||||
| import NetUtils | ||||
| from worlds.AutoWorld import AutoWorldRegister | ||||
| 
 | ||||
| proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()} | ||||
| from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name | ||||
| import Utils | ||||
| from Utils import get_item_name_from_id, get_location_name_from_id, \ | ||||
|     version_tuple, restricted_loads, Version | ||||
| from Utils import version_tuple, restricted_loads, Version | ||||
| from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ | ||||
|     SlotType | ||||
| 
 | ||||
|  | @ -126,6 +121,11 @@ class Context: | |||
|     stored_data: typing.Dict[str, object] | ||||
|     stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] | ||||
| 
 | ||||
|     item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') | ||||
|     location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') | ||||
|     all_item_and_group_names: typing.Dict[str, typing.Set[str]] | ||||
|     forced_auto_forfeits: typing.Dict[str, bool] | ||||
| 
 | ||||
|     def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, | ||||
|                  hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", | ||||
|                  remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, | ||||
|  | @ -190,8 +190,43 @@ class Context: | |||
|         self.stored_data = {} | ||||
|         self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) | ||||
| 
 | ||||
|     # General networking | ||||
|         # init empty to satisfy linter, I suppose | ||||
|         self.gamespackage = {} | ||||
|         self.item_name_groups = {} | ||||
|         self.all_item_and_group_names = {} | ||||
|         self.forced_auto_forfeits = collections.defaultdict(lambda: False) | ||||
|         self.non_hintable_names = {} | ||||
| 
 | ||||
|         self._load_game_data() | ||||
|         self._init_game_data() | ||||
| 
 | ||||
|     # Datapackage retrieval | ||||
|     def _load_game_data(self): | ||||
|         import worlds | ||||
|         self.gamespackage = worlds.network_data_package["games"] | ||||
| 
 | ||||
|         self.item_name_groups = {world_name: world.item_name_groups for world_name, world in | ||||
|                                  worlds.AutoWorldRegister.world_types.items()} | ||||
|         for world_name, world in worlds.AutoWorldRegister.world_types.items(): | ||||
|             self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit | ||||
|             self.non_hintable_names[world_name] = world.hint_blacklist | ||||
| 
 | ||||
|     def _init_game_data(self): | ||||
|         for game_name, game_package in self.gamespackage.items(): | ||||
|             for item_name, item_id in game_package["item_name_to_id"].items(): | ||||
|                 self.item_names[item_id] = item_name | ||||
|             for location_name, location_id in game_package["location_name_to_id"].items(): | ||||
|                 self.location_names[location_id] = location_name | ||||
|             self.all_item_and_group_names[game_name] = \ | ||||
|                 set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) | ||||
| 
 | ||||
|     def item_names_for_game(self, game: str) -> typing.Dict[str, int]: | ||||
|         return self.gamespackage[game]["item_name_to_id"] | ||||
| 
 | ||||
|     def location_names_for_game(self, game: str) -> typing.Dict[str, int]: | ||||
|         return self.gamespackage[game]["location_name_to_id"] | ||||
| 
 | ||||
|     # General networking | ||||
|     async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: | ||||
|         if not endpoint.socket or not endpoint.socket.open: | ||||
|             return False | ||||
|  | @ -546,7 +581,7 @@ class Context: | |||
|         self.notify_all(finished_msg) | ||||
|         if "auto" in self.forfeit_mode: | ||||
|             forfeit_player(self, client.team, client.slot) | ||||
|         elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit: | ||||
|         elif self.forced_auto_forfeits[self.games[client.slot]]: | ||||
|             forfeit_player(self, client.team, client.slot) | ||||
|         if "auto" in self.collect_mode: | ||||
|             collect_player(self, client.team, client.slot) | ||||
|  | @ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client): | |||
|         'permissions': get_permissions(ctx), | ||||
|         'hint_cost': ctx.hint_cost, | ||||
|         'location_check_points': ctx.location_check_points, | ||||
|         'datapackage_version': network_data_package["version"], | ||||
|         'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values()) | ||||
|         if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0, | ||||
|         'datapackage_versions': {game: game_data["version"] for game, game_data | ||||
|                                  in network_data_package["games"].items()}, | ||||
|                                  in ctx.gamespackage.items()}, | ||||
|         'seed_name': ctx.seed_name, | ||||
|         'time': time.time(), | ||||
|     }]) | ||||
|  | @ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi | |||
|             send_items_to(ctx, team, target_player, 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))) | ||||
|                 team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], | ||||
|                 ctx.player_names[(team, target_player)], ctx.location_names[location])) | ||||
|             info_text = json_format_send_event(new_item, target_player) | ||||
|             ctx.broadcast_team(team, [info_text]) | ||||
| 
 | ||||
|  | @ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi | |||
|         ctx.save() | ||||
| 
 | ||||
| 
 | ||||
| def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: | ||||
| def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]: | ||||
|     hints = [] | ||||
|     slots: typing.Set[int] = {slot} | ||||
|     for group_id, group in ctx.groups.items(): | ||||
|         if slot in group: | ||||
|             slots.add(group_id) | ||||
|     seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item] | ||||
| 
 | ||||
|     seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name] | ||||
|     for finding_player, check_data in ctx.locations.items(): | ||||
|         for location_id, (item_id, receiving_player, item_flags) in check_data.items(): | ||||
|             if receiving_player in slots and item_id == seeked_item_id: | ||||
|  | @ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ | |||
| 
 | ||||
| 
 | ||||
| def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: | ||||
|     seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] | ||||
|     seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] | ||||
|     return collect_hint_location_id(ctx, team, slot, seeked_location) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location | |||
| 
 | ||||
| def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: | ||||
|     text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ | ||||
|            f"{lookup_any_item_id_to_name[hint.item]} is " \ | ||||
|            f"at {get_location_name_from_id(hint.location)} " \ | ||||
|            f"{ctx.item_names[hint.item]} is " \ | ||||
|            f"at {ctx.location_names[hint.location]} " \ | ||||
|            f"in {ctx.player_names[team, hint.finding_player]}'s World" | ||||
| 
 | ||||
|     if hint.entrance: | ||||
|  | @ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|             forfeit_player(self.ctx, self.client.team, self.client.slot) | ||||
|             return True | ||||
|         elif "disabled" in self.ctx.forfeit_mode: | ||||
|             self.output( | ||||
|                 "Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release") | ||||
|             self.output("Sorry, client item releasing has been disabled on this server. " | ||||
|                         "You can ask the server admin for a /release") | ||||
|             return False | ||||
|         else:  # is auto or goal | ||||
|             if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: | ||||
|  | @ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|         if self.ctx.remaining_mode == "enabled": | ||||
|             remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) | ||||
|             if remaining_item_ids: | ||||
|                 self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") | ||||
|                 self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] | ||||
|                                                             for item_id in remaining_item_ids)) | ||||
|             else: | ||||
|                 self.output("No remaining items found.") | ||||
|  | @ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|             if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: | ||||
|                 remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) | ||||
|                 if remaining_item_ids: | ||||
|                     self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") | ||||
|                     self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] | ||||
|                                                                 for item_id in remaining_item_ids)) | ||||
|                 else: | ||||
|                     self.output("No remaining items found.") | ||||
|  | @ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|         locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) | ||||
| 
 | ||||
|         if locations: | ||||
|             texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations] | ||||
|             texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] | ||||
|             texts.append(f"Found {len(locations)} missing location checks") | ||||
|             self.ctx.notify_client_multiple(self.client, texts) | ||||
|         else: | ||||
|  | @ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|         locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) | ||||
| 
 | ||||
|         if locations: | ||||
|             texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations] | ||||
|             texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] | ||||
|             texts.append(f"Found {len(locations)} done location checks") | ||||
|             self.ctx.notify_client_multiple(self.client, texts) | ||||
|         else: | ||||
|  | @ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|     def _cmd_getitem(self, item_name: str) -> bool: | ||||
|         """Cheat in an item, if it is enabled on this server""" | ||||
|         if self.ctx.item_cheat: | ||||
|             world = proxy_worlds[self.ctx.games[self.client.slot]] | ||||
|             item_name, usable, response = get_intended_text(item_name, | ||||
|                                                             world.item_names) | ||||
|             names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot]) | ||||
|             item_name, usable, response = get_intended_text( | ||||
|                 item_name, | ||||
|                 names | ||||
|             ) | ||||
|             if usable: | ||||
|                 new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot) | ||||
|                 new_item = NetworkItem(names[item_name], -1, self.client.slot) | ||||
|                 get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) | ||||
|                 get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) | ||||
|                 self.ctx.notify_all( | ||||
|  | @ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|                         f"You have {points_available} points.") | ||||
|             return True | ||||
|         else: | ||||
|             world = proxy_worlds[self.ctx.games[self.client.slot]] | ||||
|             names = world.location_names if for_location else world.all_item_and_group_names | ||||
|             game = self.ctx.games[self.client.slot] | ||||
|             names = self.ctx.location_names_for_game(game) \ | ||||
|                 if for_location else \ | ||||
|                 self.ctx.all_item_and_group_names[game] | ||||
|             hint_name, usable, response = get_intended_text(input_text, | ||||
|                                                             names) | ||||
|             if usable: | ||||
|                 if hint_name in world.hint_blacklist: | ||||
|                 if hint_name in self.ctx.non_hintable_names[game]: | ||||
|                     self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") | ||||
|                     hints = [] | ||||
|                 elif not for_location and hint_name in world.item_name_groups:  # item group name | ||||
|                 elif not for_location and hint_name in self.ctx.item_name_groups[game]:  # item group name | ||||
|                     hints = [] | ||||
|                     for item in world.item_name_groups[hint_name]: | ||||
|                         if item in world.item_name_to_id:  # ensure item has an ID | ||||
|                             hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) | ||||
|                 elif not for_location and hint_name in world.item_names:  # item name | ||||
|                     for item_name in self.ctx.item_name_groups[game][hint_name]: | ||||
|                         if item_name in self.ctx.item_names_for_game(game):  # ensure item has an ID | ||||
|                             hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) | ||||
|                 elif not for_location and hint_name in self.ctx.item_names_for_game(game):  # item name | ||||
|                     hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) | ||||
|                 else:  # location name | ||||
|                     hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) | ||||
|  | @ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor): | |||
|                 return False | ||||
| 
 | ||||
|     @mark_raw | ||||
|     def _cmd_hint(self, item: str = "") -> bool: | ||||
|     def _cmd_hint(self, item_name: str = "") -> bool: | ||||
|         """Use !hint {item_name}, | ||||
|         for example !hint Lamp to get a spoiler peek for that item. | ||||
|         If hint costs are on, this will only give you one new result, | ||||
|         you can rerun the command to get more in that case.""" | ||||
|         return self.get_hints(item) | ||||
|         return self.get_hints(item_name) | ||||
| 
 | ||||
|     @mark_raw | ||||
|     def _cmd_hint_location(self, location: str = "") -> bool: | ||||
|  | @ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): | |||
|     elif cmd == "GetDataPackage": | ||||
|         exclusions = args.get("exclusions", []) | ||||
|         if "games" in args: | ||||
|             games = {name: game_data for name, game_data in network_data_package["games"].items() | ||||
|             games = {name: game_data for name, game_data in ctx.gamespackage.items() | ||||
|                      if name in set(args.get("games", []))} | ||||
|             await ctx.send_msgs(client, [{"cmd": "DataPackage", | ||||
|                                           "data": {"games": games}}]) | ||||
|         # TODO: remove exclusions behaviour around 0.5.0 | ||||
|         elif exclusions: | ||||
|             exclusions = set(exclusions) | ||||
|             games = {name: game_data for name, game_data in network_data_package["games"].items() | ||||
|             games = {name: game_data for name, game_data in ctx.gamespackage.items() | ||||
|                      if name not in exclusions} | ||||
|             package = network_data_package.copy() | ||||
|             package["games"] = games | ||||
| 
 | ||||
|             package = {"games": games} | ||||
|             await ctx.send_msgs(client, [{"cmd": "DataPackage", | ||||
|                                           "data": package}]) | ||||
| 
 | ||||
|         else: | ||||
|             await ctx.send_msgs(client, [{"cmd": "DataPackage", | ||||
|                                           "data": network_data_package}]) | ||||
|                                           "data": {"games": ctx.gamespackage}}]) | ||||
| 
 | ||||
|     elif client.auth: | ||||
|         if cmd == "ConnectUpdate": | ||||
|  | @ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): | |||
|             create_as_hint: int = int(args.get("create_as_hint", 0)) | ||||
|             hints = [] | ||||
|             for location in args["locations"]: | ||||
|                 if type(location) is not int or location not in lookup_any_location_id_to_name: | ||||
|                 if type(location) is not int: | ||||
|                     await ctx.send_msgs(client, | ||||
|                                         [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', | ||||
|                                           "original_cmd": cmd}]) | ||||
|  | @ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor): | |||
|         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | ||||
|         if usable: | ||||
|             team, slot = self.ctx.player_name_lookup[seeked_player] | ||||
|             item = " ".join(item_name) | ||||
|             world = proxy_worlds[self.ctx.games[slot]] | ||||
|             item, usable, response = get_intended_text(item, world.item_names) | ||||
|             item_name = " ".join(item_name) | ||||
|             names = self.ctx.item_names_for_game(self.ctx.games[slot]) | ||||
|             item_name, usable, response = get_intended_text(item_name, names) | ||||
|             if usable: | ||||
|                 amount: int = int(amount) | ||||
|                 new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] | ||||
|                 new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] | ||||
|                 send_items_to(self.ctx, team, slot, *new_items) | ||||
| 
 | ||||
|                 send_new_items(self.ctx) | ||||
|                 self.ctx.notify_all( | ||||
|                     'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + | ||||
|                     f'"{item}" to {self.ctx.get_aliased_name(team, slot)}') | ||||
|                     f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') | ||||
|                 return True | ||||
|             else: | ||||
|                 self.output(response) | ||||
|  | @ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor): | |||
|         """Sends an item to the specified player""" | ||||
|         return self._cmd_send_multiple(1, player_name, *item_name) | ||||
| 
 | ||||
|     def _cmd_hint(self, player_name: str, *item: str) -> bool: | ||||
|     def _cmd_hint(self, player_name: str, *item_name: str) -> bool: | ||||
|         """Send out a hint for a player's item to their team""" | ||||
|         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | ||||
|         if usable: | ||||
|             team, slot = self.ctx.player_name_lookup[seeked_player] | ||||
|             item = " ".join(item) | ||||
|             world = proxy_worlds[self.ctx.games[slot]] | ||||
|             item, usable, response = get_intended_text(item, world.all_item_and_group_names) | ||||
|             item_name = " ".join(item_name) | ||||
|             game = self.ctx.games[slot] | ||||
|             item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game]) | ||||
|             if usable: | ||||
|                 if item in world.item_name_groups: | ||||
|                 if item_name in self.ctx.item_name_groups[game]: | ||||
|                     hints = [] | ||||
|                     for item in world.item_name_groups[item]: | ||||
|                         if item in world.item_name_to_id:  # ensure item has an ID | ||||
|                             hints.extend(collect_hints(self.ctx, team, slot, item)) | ||||
|                     for item_name_from_group in self.ctx.item_name_groups[game][item_name]: | ||||
|                         if item_name_from_group in self.ctx.item_names_for_game(game):  # ensure item has an ID | ||||
|                             hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) | ||||
|                 else:  # item name | ||||
|                     hints = collect_hints(self.ctx, team, slot, item) | ||||
|                     hints = collect_hints(self.ctx, team, slot, item_name) | ||||
| 
 | ||||
|                 if hints: | ||||
|                     notify_hints(self.ctx, team, hints) | ||||
|  | @ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor): | |||
|             self.output(response) | ||||
|             return False | ||||
| 
 | ||||
|     def _cmd_hint_location(self, player_name: str, *location: str) -> bool: | ||||
|     def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: | ||||
|         """Send out a hint for a player's location to their team""" | ||||
|         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | ||||
|         if usable: | ||||
|             team, slot = self.ctx.player_name_lookup[seeked_player] | ||||
|             item = " ".join(location) | ||||
|             world = proxy_worlds[self.ctx.games[slot]] | ||||
|             item, usable, response = get_intended_text(item, world.location_names) | ||||
|             location_name = " ".join(location_name) | ||||
|             location_name, usable, response = get_intended_text(location_name, | ||||
|                                                                 self.ctx.location_names_for_game(self.ctx.games[slot])) | ||||
|             if usable: | ||||
|                 hints = collect_hint_location_name(self.ctx, team, slot, item) | ||||
|                 hints = collect_hint_location_name(self.ctx, team, slot, location_name) | ||||
|                 if hints: | ||||
|                     notify_hints(self.ctx, team, hints) | ||||
|                 else: | ||||
|  |  | |||
							
								
								
									
										10
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										10
									
								
								Utils.py
								
								
								
								
							|  | @ -328,16 +328,6 @@ def get_options() -> dict: | |||
|     return get_options.options | ||||
| 
 | ||||
| 
 | ||||
| def get_item_name_from_id(code: int) -> str: | ||||
|     from worlds import lookup_any_item_id_to_name | ||||
|     return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})') | ||||
| 
 | ||||
| 
 | ||||
| def get_location_name_from_id(code: int) -> str: | ||||
|     from worlds import lookup_any_location_id_to_name | ||||
|     return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})') | ||||
| 
 | ||||
| 
 | ||||
| def persistent_store(category: str, key: typing.Any, value: typing.Any): | ||||
|     path = user_path("_persistent_storage.yaml") | ||||
|     storage: dict = persistent_load() | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ import Utils | |||
| 
 | ||||
| Utils.local_path.cached_path = os.path.dirname(__file__) | ||||
| 
 | ||||
| from WebHostLib import app as raw_app | ||||
| from WebHostLib import register, app as raw_app | ||||
| from waitress import serve | ||||
| 
 | ||||
| from WebHostLib.models import db | ||||
|  | @ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen | |||
| from WebHostLib.lttpsprites import update_sprites_lttp | ||||
| from WebHostLib.options import create as create_options_files | ||||
| 
 | ||||
| from worlds.AutoWorld import AutoWorldRegister | ||||
| 
 | ||||
| configpath = os.path.abspath("config.yaml") | ||||
| if not os.path.exists(configpath):  # fall back to config.yaml in home | ||||
|     configpath = os.path.abspath(Utils.user_path('config.yaml')) | ||||
| 
 | ||||
| 
 | ||||
| def get_app(): | ||||
|     register() | ||||
|     app = raw_app | ||||
|     if os.path.exists(configpath): | ||||
|         import yaml | ||||
|  | @ -43,6 +42,7 @@ def get_app(): | |||
| def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: | ||||
|     import json | ||||
|     import shutil | ||||
|     from worlds.AutoWorld import AutoWorldRegister | ||||
|     worlds = {} | ||||
|     data = [] | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|  |  | |||
|  | @ -3,12 +3,11 @@ import uuid | |||
| import base64 | ||||
| import socket | ||||
| 
 | ||||
| import jinja2.exceptions | ||||
| from pony.flask import Pony | ||||
| from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory | ||||
| from flask import Flask | ||||
| from flask_caching import Cache | ||||
| from flask_compress import Compress | ||||
| from worlds.AutoWorld import AutoWorldRegister | ||||
| from werkzeug.routing import BaseConverter | ||||
| 
 | ||||
| from .models import * | ||||
| 
 | ||||
|  | @ -53,8 +52,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg" | |||
| cache = Cache(app) | ||||
| Compress(app) | ||||
| 
 | ||||
| from werkzeug.routing import BaseConverter | ||||
| 
 | ||||
| 
 | ||||
| class B64UUIDConverter(BaseConverter): | ||||
| 
 | ||||
|  | @ -69,173 +66,16 @@ class B64UUIDConverter(BaseConverter): | |||
| app.url_map.converters["suuid"] = B64UUIDConverter | ||||
| app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') | ||||
| 
 | ||||
| # has automatic patch integration | ||||
| import Patch | ||||
| app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types | ||||
| 
 | ||||
| def register(): | ||||
|     """Import submodules, triggering their registering on flask routing. | ||||
|     Note: initializes worlds subsystem.""" | ||||
|     # has automatic patch integration | ||||
|     import Patch | ||||
|     app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types | ||||
| 
 | ||||
| def get_world_theme(game_name: str): | ||||
|     if game_name in AutoWorldRegister.world_types: | ||||
|         return AutoWorldRegister.world_types[game_name].web.theme | ||||
|     return 'grass' | ||||
|     from WebHostLib.customserver import run_server_process | ||||
|     # to trigger app routing picking up on it | ||||
|     from . import tracker, upload, landing, check, generate, downloads, api, stats, misc | ||||
| 
 | ||||
| 
 | ||||
| @app.before_request | ||||
| def register_session(): | ||||
|     session.permanent = True  # technically 31 days after the last visit | ||||
|     if not session.get("_id", None): | ||||
|         session["_id"] = uuid4()  # uniquely identify each session without needing a login | ||||
| 
 | ||||
| 
 | ||||
| @app.errorhandler(404) | ||||
| @app.errorhandler(jinja2.exceptions.TemplateNotFound) | ||||
| def page_not_found(err): | ||||
|     return render_template('404.html'), 404 | ||||
| 
 | ||||
| 
 | ||||
| # Start Playing Page | ||||
| @app.route('/start-playing') | ||||
| def start_playing(): | ||||
|     return render_template(f"startPlaying.html") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/weighted-settings') | ||||
| def weighted_settings(): | ||||
|     return render_template(f"weighted-settings.html") | ||||
| 
 | ||||
| 
 | ||||
| # Player settings pages | ||||
| @app.route('/games/<string:game>/player-settings') | ||||
| def player_settings(game): | ||||
|     return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| # Game Info Pages | ||||
| @app.route('/games/<string:game>/info/<string:lang>') | ||||
| def game_info(game, lang): | ||||
|     return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| # List of supported games | ||||
| @app.route('/games') | ||||
| def games(): | ||||
|     worlds = {} | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             worlds[game] = world | ||||
|     return render_template("supportedGames.html", worlds=worlds) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/tutorial/<string:game>/<string:file>/<string:lang>') | ||||
| def tutorial(game, file, lang): | ||||
|     return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/tutorial/') | ||||
| def tutorial_landing(): | ||||
|     worlds = {} | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             worlds[game] = world | ||||
|     return render_template("tutorialLanding.html") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/faq/<string:lang>/') | ||||
| def faq(lang): | ||||
|     return render_template("faq.html", lang=lang) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/glossary/<string:lang>/') | ||||
| def terms(lang): | ||||
|     return render_template("glossary.html", lang=lang) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/seed/<suuid:seed>') | ||||
| def view_seed(seed: UUID): | ||||
|     seed = Seed.get(id=seed) | ||||
|     if not seed: | ||||
|         abort(404) | ||||
|     return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/new_room/<suuid:seed>') | ||||
| def new_room(seed: UUID): | ||||
|     seed = Seed.get(id=seed) | ||||
|     if not seed: | ||||
|         abort(404) | ||||
|     room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) | ||||
|     commit() | ||||
|     return redirect(url_for("host_room", room=room.id)) | ||||
| 
 | ||||
| 
 | ||||
| def _read_log(path: str): | ||||
|     if os.path.exists(path): | ||||
|         with open(path, encoding="utf-8-sig") as log: | ||||
|             yield from log | ||||
|     else: | ||||
|         yield f"Logfile {path} does not exist. " \ | ||||
|               f"Likely a crash during spinup of multiworld instance or it is still spinning up." | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/log/<suuid:room>') | ||||
| def display_log(room: UUID): | ||||
|     room = Room.get(id=room) | ||||
|     if room is None: | ||||
|         return abort(404) | ||||
|     if room.owner == session["_id"]: | ||||
|         return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") | ||||
|     return "Access Denied", 403 | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/room/<suuid:room>', methods=['GET', 'POST']) | ||||
| def host_room(room: UUID): | ||||
|     room = Room.get(id=room) | ||||
|     if room is None: | ||||
|         return abort(404) | ||||
|     if request.method == "POST": | ||||
|         if room.owner == session["_id"]: | ||||
|             cmd = request.form["cmd"] | ||||
|             if cmd: | ||||
|                 Command(room=room, commandtext=cmd) | ||||
|                 commit() | ||||
| 
 | ||||
|     with db_session: | ||||
|         room.last_activity = datetime.utcnow()  # will trigger a spinup, if it's not already running | ||||
| 
 | ||||
|     return render_template("hostRoom.html", 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") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/datapackage') | ||||
| @cache.cached() | ||||
| def get_datapackge(): | ||||
|     """A pretty print version of /api/datapackage""" | ||||
|     from worlds import network_data_package | ||||
|     import json | ||||
|     return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/index') | ||||
| @app.route('/sitemap') | ||||
| def get_sitemap(): | ||||
|     available_games = [] | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             available_games.append(game) | ||||
|     return render_template("siteMap.html", games=available_games) | ||||
| 
 | ||||
| 
 | ||||
| from WebHostLib.customserver import run_server_process | ||||
| from . import tracker, upload, landing, check, generate, downloads, api, stats  # to trigger app routing picking up on it | ||||
| 
 | ||||
| app.register_blueprint(api.api_endpoints) | ||||
|     app.register_blueprint(api.api_endpoints) | ||||
|  |  | |||
|  | @ -184,7 +184,7 @@ class MultiworldInstance(): | |||
| 
 | ||||
|         logging.info(f"Spinning up {self.room_id}") | ||||
|         process = multiprocessing.Process(group=None, target=run_server_process, | ||||
|                                           args=(self.room_id, self.ponyconfig), | ||||
|                                           args=(self.room_id, self.ponyconfig, get_static_server_data()), | ||||
|                                           name="MultiHost") | ||||
|         process.start() | ||||
|         # bind after start to prevent thread sync issues with guardian. | ||||
|  | @ -238,5 +238,5 @@ def run_guardian(): | |||
| 
 | ||||
| 
 | ||||
| from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed | ||||
| from .customserver import run_server_process | ||||
| from .customserver import run_server_process, get_static_server_data | ||||
| from .generate import gen_game | ||||
|  |  | |||
|  | @ -9,12 +9,13 @@ import time | |||
| import random | ||||
| import pickle | ||||
| import logging | ||||
| import datetime | ||||
| 
 | ||||
| import Utils | ||||
| from .models import * | ||||
| from .models import db_session, Room, select, commit, Command, db | ||||
| 
 | ||||
| from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor | ||||
| from Utils import get_public_ipv4, get_public_ipv6, restricted_loads | ||||
| from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless | ||||
| 
 | ||||
| 
 | ||||
| class CustomClientMessageProcessor(ClientMessageProcessor): | ||||
|  | @ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor): | |||
| import MultiServer | ||||
| 
 | ||||
| MultiServer.client_message_processor = CustomClientMessageProcessor | ||||
| del (MultiServer) | ||||
| del MultiServer | ||||
| 
 | ||||
| 
 | ||||
| class DBCommandProcessor(ServerCommandProcessor): | ||||
|  | @ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor): | |||
| 
 | ||||
| 
 | ||||
| class WebHostContext(Context): | ||||
|     def __init__(self): | ||||
|     def __init__(self, static_server_data: dict): | ||||
|         # static server data is used during _load_game_data to load required data, | ||||
|         # without needing to import worlds system, which takes quite a bit of memory | ||||
|         self.static_server_data = static_server_data | ||||
|         super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2) | ||||
|         del self.static_server_data | ||||
|         self.main_loop = asyncio.get_running_loop() | ||||
|         self.video = {} | ||||
|         self.tags = ["AP", "WebHost"] | ||||
| 
 | ||||
|     def _load_game_data(self): | ||||
|         for key, value in self.static_server_data.items(): | ||||
|             setattr(self, key, value) | ||||
| 
 | ||||
|     def listen_to_db_commands(self): | ||||
|         cmdprocessor = DBCommandProcessor(self) | ||||
| 
 | ||||
|  | @ -107,14 +116,32 @@ def get_random_port(): | |||
|     return random.randint(49152, 65535) | ||||
| 
 | ||||
| 
 | ||||
| def run_server_process(room_id, ponyconfig: dict): | ||||
| @cache_argsless | ||||
| def get_static_server_data() -> dict: | ||||
|     import worlds | ||||
|     data = { | ||||
|         "forced_auto_forfeits": {}, | ||||
|         "non_hintable_names": {}, | ||||
|         "gamespackage": worlds.network_data_package["games"], | ||||
|         "item_name_groups": {world_name: world.item_name_groups for world_name, world in | ||||
|                              worlds.AutoWorldRegister.world_types.items()}, | ||||
|     } | ||||
| 
 | ||||
|     for world_name, world in worlds.AutoWorldRegister.world_types.items(): | ||||
|         data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit | ||||
|         data["non_hintable_names"][world_name] = world.hint_blacklist | ||||
| 
 | ||||
|     return data | ||||
| 
 | ||||
| 
 | ||||
| def run_server_process(room_id, ponyconfig: dict, static_server_data: dict): | ||||
|     # establish DB connection for multidata and multisave | ||||
|     db.bind(**ponyconfig) | ||||
|     db.generate_mapping(check_tables=False) | ||||
| 
 | ||||
|     async def main(): | ||||
|         Utils.init_logging(str(room_id), write_mode="a") | ||||
|         ctx = WebHostContext() | ||||
|         ctx = WebHostContext(static_server_data) | ||||
|         ctx.load(room_id) | ||||
|         ctx.init_save() | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,170 @@ | |||
| import datetime | ||||
| import os | ||||
| 
 | ||||
| import jinja2.exceptions | ||||
| from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory | ||||
| 
 | ||||
| from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 | ||||
| from worlds.AutoWorld import AutoWorldRegister | ||||
| from . import app, cache | ||||
| 
 | ||||
| 
 | ||||
| def get_world_theme(game_name: str): | ||||
|     if game_name in AutoWorldRegister.world_types: | ||||
|         return AutoWorldRegister.world_types[game_name].web.theme | ||||
|     return 'grass' | ||||
| 
 | ||||
| 
 | ||||
| @app.before_request | ||||
| def register_session(): | ||||
|     session.permanent = True  # technically 31 days after the last visit | ||||
|     if not session.get("_id", None): | ||||
|         session["_id"] = uuid4()  # uniquely identify each session without needing a login | ||||
| 
 | ||||
| 
 | ||||
| @app.errorhandler(404) | ||||
| @app.errorhandler(jinja2.exceptions.TemplateNotFound) | ||||
| def page_not_found(err): | ||||
|     return render_template('404.html'), 404 | ||||
| 
 | ||||
| 
 | ||||
| # Start Playing Page | ||||
| @app.route('/start-playing') | ||||
| def start_playing(): | ||||
|     return render_template(f"startPlaying.html") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/weighted-settings') | ||||
| def weighted_settings(): | ||||
|     return render_template(f"weighted-settings.html") | ||||
| 
 | ||||
| 
 | ||||
| # Player settings pages | ||||
| @app.route('/games/<string:game>/player-settings') | ||||
| def player_settings(game): | ||||
|     return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| # Game Info Pages | ||||
| @app.route('/games/<string:game>/info/<string:lang>') | ||||
| def game_info(game, lang): | ||||
|     return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| # List of supported games | ||||
| @app.route('/games') | ||||
| def games(): | ||||
|     worlds = {} | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             worlds[game] = world | ||||
|     return render_template("supportedGames.html", worlds=worlds) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/tutorial/<string:game>/<string:file>/<string:lang>') | ||||
| def tutorial(game, file, lang): | ||||
|     return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/tutorial/') | ||||
| def tutorial_landing(): | ||||
|     worlds = {} | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             worlds[game] = world | ||||
|     return render_template("tutorialLanding.html") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/faq/<string:lang>/') | ||||
| def faq(lang): | ||||
|     return render_template("faq.html", lang=lang) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/glossary/<string:lang>/') | ||||
| def terms(lang): | ||||
|     return render_template("glossary.html", lang=lang) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/seed/<suuid:seed>') | ||||
| def view_seed(seed: UUID): | ||||
|     seed = Seed.get(id=seed) | ||||
|     if not seed: | ||||
|         abort(404) | ||||
|     return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/new_room/<suuid:seed>') | ||||
| def new_room(seed: UUID): | ||||
|     seed = Seed.get(id=seed) | ||||
|     if not seed: | ||||
|         abort(404) | ||||
|     room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) | ||||
|     commit() | ||||
|     return redirect(url_for("host_room", room=room.id)) | ||||
| 
 | ||||
| 
 | ||||
| def _read_log(path: str): | ||||
|     if os.path.exists(path): | ||||
|         with open(path, encoding="utf-8-sig") as log: | ||||
|             yield from log | ||||
|     else: | ||||
|         yield f"Logfile {path} does not exist. " \ | ||||
|               f"Likely a crash during spinup of multiworld instance or it is still spinning up." | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/log/<suuid:room>') | ||||
| def display_log(room: UUID): | ||||
|     room = Room.get(id=room) | ||||
|     if room is None: | ||||
|         return abort(404) | ||||
|     if room.owner == session["_id"]: | ||||
|         return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") | ||||
|     return "Access Denied", 403 | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/room/<suuid:room>', methods=['GET', 'POST']) | ||||
| def host_room(room: UUID): | ||||
|     room = Room.get(id=room) | ||||
|     if room is None: | ||||
|         return abort(404) | ||||
|     if request.method == "POST": | ||||
|         if room.owner == session["_id"]: | ||||
|             cmd = request.form["cmd"] | ||||
|             if cmd: | ||||
|                 Command(room=room, commandtext=cmd) | ||||
|                 commit() | ||||
| 
 | ||||
|     with db_session: | ||||
|         room.last_activity = datetime.utcnow()  # will trigger a spinup, if it's not already running | ||||
| 
 | ||||
|     return render_template("hostRoom.html", 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") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/datapackage') | ||||
| @cache.cached() | ||||
| def get_datapackge(): | ||||
|     """A pretty print version of /api/datapackage""" | ||||
|     from worlds import network_data_package | ||||
|     import json | ||||
|     return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/index') | ||||
| @app.route('/sitemap') | ||||
| def get_sitemap(): | ||||
|     available_games = [] | ||||
|     for game, world in AutoWorldRegister.world_types.items(): | ||||
|         if not world.hidden: | ||||
|             available_games.append(game) | ||||
|     return render_template("siteMap.html", games=available_games) | ||||
|  | @ -11,7 +11,7 @@ from worlds.alttp import Items | |||
| from WebHostLib import app, cache, Room | ||||
| from Utils import restricted_loads | ||||
| from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name | ||||
| from MultiServer import get_item_name_from_id, Context | ||||
| from MultiServer import Context | ||||
| from NetUtils import SlotType | ||||
| 
 | ||||
| alttp_icons = { | ||||
|  | @ -1021,7 +1021,7 @@ def getTracker(tracker: UUID): | |||
|     for (team, player), data in multisave.get("video", []): | ||||
|         video[(team, player)] = data | ||||
| 
 | ||||
|     return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, | ||||
|     return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, | ||||
|                            lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, | ||||
|                            tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, | ||||
|                            multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, | ||||
|  |  | |||
|  | @ -45,7 +45,6 @@ class MeritousWorld(World): | |||
|     item_name_groups = item_groups | ||||
| 
 | ||||
|     data_version = 2 | ||||
|     forced_auto_forfeit = False | ||||
| 
 | ||||
|     # NOTE: Remember to change this before this game goes live | ||||
|     required_client_version = (0, 2, 4) | ||||
|  |  | |||
|  | @ -35,9 +35,7 @@ class SM64World(World): | |||
|     location_name_to_id = location_table | ||||
| 
 | ||||
|     data_version = 6 | ||||
|     required_client_version = (0,3,0) | ||||
|      | ||||
|     forced_auto_forfeit = False | ||||
|     required_client_version = (0, 3, 0) | ||||
| 
 | ||||
|     area_connections: typing.Dict[int, int] | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,7 +35,6 @@ class V6World(World): | |||
|     location_name_to_id = location_table | ||||
| 
 | ||||
|     data_version = 1 | ||||
|     forced_auto_forfeit = False | ||||
| 
 | ||||
|     area_connections: typing.Dict[int, int] | ||||
|     area_cost_map: typing.Dict[int,int] | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue