diff --git a/.gitignore b/.gitignore index c964b929..4b1cf4a0 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,7 @@ ENV/ env.bak/ venv.bak/ .code-workspace +shell.nix # Spyder project settings .spyderproject diff --git a/WebHostLib/static/assets/lttpMultiTracker.js b/WebHostLib/static/assets/lttpMultiTracker.js new file mode 100644 index 00000000..e9033102 --- /dev/null +++ b/WebHostLib/static/assets/lttpMultiTracker.js @@ -0,0 +1,6 @@ +window.addEventListener('load', () => { + $(".table-wrapper").scrollsync({ + y_sync: true, + x_sync: true + }); +}); diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/multiTrackerCommon.js similarity index 97% rename from WebHostLib/static/assets/tracker.js rename to WebHostLib/static/assets/multiTrackerCommon.js index 1a24b95c..c08590cb 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/multiTrackerCommon.js @@ -110,7 +110,7 @@ window.addEventListener('load', () => { const update = () => { const target = $("
"); console.log("Updating Tracker..."); - target.load("/tracker/" + tracker, function (response, status) { + target.load(location.href, function (response, status) { if (status === "success") { target.find(".table").each(function (i, new_table) { const new_trs = $(new_table).find("tbody>tr"); @@ -137,10 +137,5 @@ window.addEventListener('load', () => { tables.draw(); }); - $(".table-wrapper").scrollsync({ - y_sync: true, - x_sync: true - }); - adjustTableHeight(); }); diff --git a/WebHostLib/static/styles/tracker.css b/WebHostLib/static/styles/tracker.css index e203d9e9..0e00553c 100644 --- a/WebHostLib/static/styles/tracker.css +++ b/WebHostLib/static/styles/tracker.css @@ -119,6 +119,33 @@ img.alttp-sprite { background-color: #d3c97d; } +#tracker-navigation { + display: inline-flex; + background-color: #b0a77d; + margin: 0.5rem; + border-radius: 4px; +} + +.tracker-navigation-button { + display: block; + margin: 4px; + padding-left: 12px; + padding-right: 12px; + border-radius: 4px; + text-align: center; + font-size: 14px; + color: #000; + font-weight: lighter; +} + +.tracker-navigation-button:hover { + background-color: #e2eabb !important; +} + +.tracker-navigation-button.selected { + background-color: rgb(220, 226, 189); +} + @media all and (max-width: 1700px) { table.dataTable thead th.upper-row{ position: -webkit-sticky; diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 1c8f3de2..0a4ca70e 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -14,7 +14,7 @@
{% endif %} {% if room.tracker %} - This room has a Multiworld Tracker enabled. + This room has a Multiworld Tracker enabled.
{% endif %} The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/lttpMultiTracker.html similarity index 97% rename from WebHostLib/templates/tracker.html rename to WebHostLib/templates/lttpMultiTracker.html index 96148e34..a86c6851 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -1,14 +1,16 @@ {% extends 'tablepage.html' %} {% block head %} {{ super() }} - Multiworld Tracker + ALttP Multiworld Tracker - + + {% endblock %} {% block body %} {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %}
diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html new file mode 100644 index 00000000..25eeec8d --- /dev/null +++ b/WebHostLib/templates/multiTracker.html @@ -0,0 +1,87 @@ +{% extends 'tablepage.html' %} +{% block head %} + {{ super() }} + Multiworld Tracker + + +{% endblock %} + +{% block body %} + {% include 'header/dirtHeader.html' %} + {% include 'multiTrackerNavigation.html' %} +
+
+ + + + Multistream + + + Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically. +
+
+ {% for team, players in checks_done.items() %} +
+ + + + + + + + + + + + {%- for player, checks in players.items() -%} + + + + + + {%- if activity_timers[(team, player)] -%} + + {%- else -%} + + {%- endif -%} + + {%- endfor -%} + +
#NameChecks%Last
Activity
{{ loop.index }}{{ player_names[(team, loop.index)]|e }}{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}{{ percent_total_checks_done[team][player] }}{{ activity_timers[(team, player)].total_seconds() }}None
+
+ {% endfor %} + {% for team, hints in hints.items() %} +
+ + + + + + + + + + + + + {%- for hint in hints -%} + + + + + + + + + {%- endfor -%} + +
FinderReceiverItemLocationEntranceFound
{{ long_player_names[team, hint.finding_player] }}{{ long_player_names[team, hint.receiving_player] }}{{ hint.item|item_name }}{{ hint.location|location_name }}{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}{% if hint.found %}✔{% endif %}
+
+ {% endfor %} +
+
+{% endblock %} diff --git a/WebHostLib/templates/multiTrackerNavigation.html b/WebHostLib/templates/multiTrackerNavigation.html new file mode 100644 index 00000000..c6498fc6 --- /dev/null +++ b/WebHostLib/templates/multiTrackerNavigation.html @@ -0,0 +1,12 @@ +{%- if enabled_multiworld_trackers|length > 1 -%} +
+ {% for enabled_tracker in enabled_multiworld_trackers %} + {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} + {%- if enabled_tracker.current -%} + {{ enabled_tracker.name }} + {%- else -%} + {{ enabled_tracker.name }} + {%- endif -%} + {% endfor %} +
+{%- endif -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index f5cddfca..c2a44324 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -210,14 +210,6 @@ del data del item -def attribute_item(inventory, team, recipient, item): - target_item = links.get(item, item) - if item in levels: # non-progressive - inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) - else: - inventory[team][recipient][target_item] += 1 - - def attribute_item_solo(inventory, item): """Adds item to inventory counter, converts everything to progressive.""" target_item = links.get(item, item) @@ -486,7 +478,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D "Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png", "Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png", "Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png", - "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", + "Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png", "Saddle": "https://i.imgur.com/2QtDyR0.png", "Channeling Book": "https://i.imgur.com/J3WsYZw.png", "Silk Touch Book": "https://i.imgur.com/iqERxHQ.png", @@ -494,7 +486,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D } minecraft_location_ids = { - "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, + "Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070, 42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077], "Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021, 42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014], @@ -656,7 +648,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in if base_name == "hookshot": display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level) - if base_name == "wallet": + if base_name == "wallet": display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level) # Determine display for bottles. Show letter if it's obtained, determine bottle count @@ -804,7 +796,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: } timespinner_location_ids = { - "Present": [ + "Present": [ 1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009, 1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019, 1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029, @@ -825,14 +817,14 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: 1337150, 1337151, 1337152, 1337153, 1337154, 1337155, 1337171, 1337172, 1337173, 1337174, 1337175], "Ancient Pyramid": [ - 1337236, + 1337236, 1337246, 1337247, 1337248, 1337249] } if(slot_data["DownloadableItems"]): timespinner_location_ids["Present"] += [ 1337156, 1337157, 1337159, - 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, + 1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169, 1337170] if(slot_data["Cantoran"]): timespinner_location_ids["Past"].append(1337176) @@ -1323,9 +1315,28 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic custom_items=custom_items, custom_locations=custom_locations) +def get_enabled_multiworld_trackers(room: Room, current: str): + enabled = [ + { + "name": "Generic", + "endpoint": "get_multiworld_tracker", + "current": current == "Generic" + } + ] + + if any(slot.game == "A Link to the Past" for slot in room.seed.slots) or current == "A Link to the Past": + enabled.append({ + "name": "A Link to the Past", + "endpoint": "get_LttP_multiworld_tracker", + "current": current == "A Link to the Past"} + ) + + return enabled + + @app.route('/tracker/') -@cache.memoize(timeout=1) # multisave is currently created at most every minute -def getTracker(tracker: UUID): +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_multiworld_tracker(tracker: UUID): room: Room = Room.get(tracker=tracker) if not room: abort(404) @@ -1333,9 +1344,6 @@ def getTracker(tracker: UUID): precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ get_static_room_data(room) - inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups} - for teamnumber, team in enumerate(names)} - checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} @@ -1344,7 +1352,6 @@ def getTracker(tracker: UUID): for playernumber in range(1, len(team) + 1) if playernumber not in groups} for teamnumber, team in enumerate(names)} - hints = {team: set() for team in range(len(names))} if room.multisave: multisave = restricted_loads(room.multisave) @@ -1354,6 +1361,78 @@ def getTracker(tracker: UUID): for (team, slot), slot_hints in multisave["hints"].items(): hints[team] |= set(slot_hints) + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): + if player in groups: + continue + player_locations = locations[player] + checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations) + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + + activity_timers = {} + now = datetime.datetime.utcnow() + for (team, player), timestamp in multisave.get("client_activity_timers", []): + activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp) + + player_names = {} + for team, names in enumerate(names): + for player, name in enumerate(names, 1): + player_names[(team, player)] = name + long_player_names = player_names.copy() + for (team, player), alias in multisave.get("name_aliases", {}).items(): + player_names[(team, player)] = alias + long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})" + + video = {} + for (team, player), data in multisave.get("video", []): + video[(team, player)] = data + + enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "Generic") + + return render_template("multiTracker.html", player_names=player_names, room=room, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, checks_in_area=seed_checks_in_area, + activity_timers=activity_timers, video=video, hints=hints, + long_player_names=long_player_names, enabled_multiworld_trackers=enabled_multiworld_trackers) + + +@app.route('/tracker//lttp') +@cache.memoize(timeout=60) # multisave is currently created at most every minute +def get_LttP_multiworld_tracker(tracker: UUID): + room: Room = Room.get(tracker=tracker) + if not room: + abort(404) + locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ + precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \ + get_static_room_data(room) + + inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if + playernumber not in groups} + for teamnumber, team in enumerate(names)} + + checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations} + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + percent_total_checks_done = {teamnumber: {playernumber: 0 + for playernumber in range(1, len(team) + 1) if playernumber not in groups} + for teamnumber, team in enumerate(names)} + + hints = {team: set() for team in range(len(names))} + if room.multisave: + multisave = restricted_loads(room.multisave) + else: + multisave = {} + if "hints" in multisave: + for (team, slot), slot_hints in multisave["hints"].items(): + hints[team] |= set(slot_hints) + + def attribute_item(team: int, recipient: int, item: int): + nonlocal inventory + target_item = links.get(item, item) + if item in levels: # non-progressive + inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item]) + else: + inventory[team][recipient][target_item] += 1 + for (team, player), locations_checked in multisave.get("location_checks", {}).items(): if player in groups: continue @@ -1361,18 +1440,19 @@ def getTracker(tracker: UUID): if precollected_items: precollected = precollected_items[player] for item_id in precollected: - attribute_item(inventory, team, player, item_id) + attribute_item(team, player, item_id) for location in locations_checked: if location not in player_locations or location not in player_location_to_area[player]: continue - item, recipient, flags = player_locations[location] - - if recipient in names: - attribute_item(inventory, team, recipient, item) - checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] += 1 - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100 + recipients = groups.get(recipient, [recipient]) + for recipient in recipients: + attribute_item(team, recipient, item) + checks_done[team][player][player_location_to_area[player][location]] += 1 + checks_done[team][player]["Total"] += 1 + percent_total_checks_done[team][player] = int( + checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \ + seed_checks_in_area[player]["Total"] else 100 for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: @@ -1414,14 +1494,19 @@ 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=lookup_any_item_id_to_name, + enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past") + + return render_template("lttpMultiTracker.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, percent_total_checks_done=percent_total_checks_done, - ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, activity_timers=activity_timers, + multi_items=multi_items, checks_done=checks_done, + percent_total_checks_done=percent_total_checks_done, + ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, + activity_timers=activity_timers, key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids, video=video, big_key_locations=group_big_key_locations, - hints=hints, long_player_names=long_player_names) + hints=hints, long_player_names=long_player_names, + enabled_multiworld_trackers=enabled_multiworld_trackers) game_specific_trackers: typing.Dict[str, typing.Callable] = {