From 2955f23d356fb6ac8d1848768d588a438e421002 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 25 Feb 2024 00:02:02 +0100 Subject: [PATCH 1/3] MultiServer: make !hint prefer early sphere --- Main.py | 12 ++++++++++++ MultiServer.py | 20 +++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Main.py b/Main.py index f1d2f63692d6..736815ac4289 100644 --- a/Main.py +++ b/Main.py @@ -397,6 +397,17 @@ def precollect_hint(location): checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} + # get spheres -> filter address==None -> skip empty + spheres: List[Dict[int, Set[int]]] = [] + for sphere in multiworld.get_spheres(): + current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) + for sphere_location in sphere: + if type(sphere_location.address) is int: + current_sphere[sphere_location.player].add(sphere_location.address) + + if current_sphere: + spheres.append(dict(current_sphere)) + multidata = { "slot_data": slot_data, "slot_info": slot_info, @@ -411,6 +422,7 @@ def precollect_hint(location): "tags": ["AP"], "minimum_versions": minimum_versions, "seed_name": multiworld.seed_name, + "spheres": spheres, "datapackage": data_package, } AutoWorld.call_all(multiworld, "modify_multidata", multidata) diff --git a/MultiServer.py b/MultiServer.py index 15ed22d715e8..2d209c66f9fc 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -175,6 +175,7 @@ class Context: all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]] + spheres: typing.List[typing.Dict[int, typing.Set[int]]] def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", @@ -236,6 +237,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) self.read_data = {} + self.spheres = [] # init empty to satisfy linter, I suppose self.gamespackage = {} @@ -464,6 +466,9 @@ def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.A for game_name, data in self.location_name_groups.items(): self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] + # sorted access spheres + self.spheres = decoded_obj.get("spheres", []) + # saving def save(self, now=False) -> bool: @@ -621,6 +626,16 @@ def get_rechecked_hints(self, team: int, slot: int): self.recheck_hints(team, slot) return self.hints[team, slot] + def get_sphere(self, player: int, location_id: int) -> int: + """Get sphere of a location, -1 if spheres are not available.""" + if self.spheres: + for i, sphere in enumerate(self.spheres): + if location_id in sphere.get(player, set()): + return i + raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. " + f"Location or player may not exist.") + return -1 + def get_players_package(self): return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] @@ -1504,6 +1519,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: self.ctx.random.shuffle(not_found_hints) # By popular vote, make hints prefer non-local placements not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) + # By another popular vote, prefer early sphere + not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location), + reverse=True) hints = found_hints while can_pay > 0: @@ -1513,9 +1531,9 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool: hints.append(hint) can_pay -= 1 self.ctx.hints_used[self.client.team, self.client.slot] += 1 - points_available = get_client_points(self.ctx, self.client) if not_found_hints: + points_available = get_client_points(self.ctx, self.client) if hints and cost and int((points_available // cost) == 0): self.output( f"There may be more hintables, however, you cannot afford to pay for any more. " From 4c606561bfff9a724cec8439fb04c19c72212214 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 7 Mar 2024 21:08:40 +0100 Subject: [PATCH 2/3] Update MultiServer.py Co-authored-by: Doug Hoskisson --- MultiServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MultiServer.py b/MultiServer.py index 2d209c66f9fc..c4f47e05399e 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -176,6 +176,7 @@ class Context: all_location_and_group_names: typing.Dict[str, typing.Set[str]] non_hintable_names: typing.Dict[str, typing.Set[str]] spheres: typing.List[typing.Dict[int, typing.Set[int]]] + """ each sphere is { player: { location_id, ... } } """ def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", From d5c11b8efb29bf7842e9b1e62f192203757f5825 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 27 May 2024 18:42:42 +0200 Subject: [PATCH 3/3] WebHost: introduce the Sphere Tracker --- MultiServer.py | 1 - WebHostLib/templates/hostRoom.html | 3 +- WebHostLib/templates/multispheretracker.html | 72 ++++++++++++++++++++ WebHostLib/tracker.py | 25 +++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 WebHostLib/templates/multispheretracker.html diff --git a/MultiServer.py b/MultiServer.py index 4fb03732d811..c80230c53ed5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -179,7 +179,6 @@ class Context: """ each sphere is { player: { location_id, ... } } """ logger: logging.Logger - def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 2981c41452f0..2bbfe4ad0169 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -24,7 +24,8 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker + and a Sphere Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/multispheretracker.html b/WebHostLib/templates/multispheretracker.html new file mode 100644 index 000000000000..a86697498396 --- /dev/null +++ b/WebHostLib/templates/multispheretracker.html @@ -0,0 +1,72 @@ +{% extends "tablepage.html" %} +{% block head %} + {{ super() }} + Multiworld Sphere Tracker + + +{% endblock %} + +{% block body %} + {% include "header/dirtHeader.html" %} + +
+
+ + +
+ {% if tracker_data.get_spheres() %} + This tracker lists already found locations by their logical access sphere. + It ignores items that cannot be sent + and will therefore differ from the sphere numbers in the spoiler playthrough. + This tracker will automatically update itself periodically. + {% else %} + This Multiworld has no Sphere data, likely due to being too old, cannot display data. + {% endif %} +
+
+ +
+ {%- for team, players in tracker_data.get_all_players().items() %} +
+ + + + + {#- Mimicking hint table header for familiarity. #} + + + + + + + + + {%- for sphere in tracker_data.get_spheres() %} + {%- set current_sphere = loop.index %} + {%- for player, sphere_location_ids in sphere.items() %} + {%- set checked_locations = tracker_data.get_player_checked_locations(team, player) %} + {%- set finder_game = tracker_data.get_player_game(team, player) %} + {%- set player_location_data = tracker_data.get_player_locations(team, player) %} + {%- for location_id in sphere_location_ids.intersection(checked_locations) %} + + {%- set item_id, receiver, item_flags = player_location_data[location_id] %} + {%- set receiver_game = tracker_data.get_player_game(team, receiver) %} + + + + + + + + {%- endfor %} + + {%- endfor %} + {%- endfor %} + +
SphereFinderReceiverItemLocationGame
{{ current_sphere }}{{ tracker_data.get_player_name(team, player) }}{{ tracker_data.get_player_name(team, receiver) }}{{ tracker_data.item_id_to_name[receiver_game][item_id] }}{{ tracker_data.location_id_to_name[finder_game][location_id] }}{{ finder_game }}
+
+ + {%- endfor -%} +
+
+{% endblock %} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index fd233da131e7..71ee9c7fcafe 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -291,6 +291,11 @@ def get_room_videos(self) -> Dict[TeamPlayer, Tuple[str, str]]: return video_feeds + @_cache_results + def get_spheres(self) -> List[List[int]]: + """ each sphere is { player: { location_id, ... } } """ + return self._multidata.get("spheres", []) + @app.route("/tracker///") def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, generic: bool = False) -> str: @@ -414,6 +419,26 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker ) +def render_generic_multiworld_sphere_tracker(tracker_data: TrackerData) -> str: + return render_template( + "multispheretracker.html", + room=tracker_data.room, + tracker_data=tracker_data, + ) + + +@app.route("/sphere_tracker/") +@cache.memoize(timeout=TRACKER_CACHE_TIMEOUT_IN_SECONDS) +def get_multiworld_sphere_tracker(tracker: UUID): + # Room must exist. + room = Room.get(tracker=tracker) + if not room: + abort(404) + + tracker_data = TrackerData(room) + return render_generic_multiworld_sphere_tracker(tracker_data) + + # TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to # live in their respective world folders.