From 2c46c48ba9b8cc738777dca2dd153fe55044560d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 12 Dec 2022 00:30:43 +0100 Subject: [PATCH] WebHost: reduce tracker refresh delay (#1290) --- MultiServer.py | 17 +++++- WebHostLib/static/assets/tracker.js | 14 ++++- WebHostLib/templates/genericTracker.html | 2 +- WebHostLib/templates/macros.html | 2 +- WebHostLib/templates/tracker.html | 4 +- WebHostLib/tracker.py | 73 +++++++++++++++--------- 6 files changed, 76 insertions(+), 36 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index ca352281..1bbbed69 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -16,6 +16,7 @@ import pickle import itertools import time import operator +import hashlib import ModuleUpdate @@ -58,6 +59,12 @@ modify_functions = { } +def get_saving_second(seed_name: str, interval: int = 60) -> int: + # save at expected times so other systems using savegame can expect it + # represents the target second of the auto_save_interval at which to save + return int(hashlib.sha256(seed_name.encode()).hexdigest(), 16) % interval + + class Client(Endpoint): version = Version(0, 0, 0) tags: typing.List[str] = [] @@ -463,10 +470,16 @@ class Context: def _start_async_saving(self): if not self.auto_saver_thread: def save_regularly(): - import time + # time.time() is platform dependent, so using the expensive datetime method instead + def get_datetime_second(): + now = datetime.datetime.now() + return now.second + now.microsecond * 0.000001 + + second = get_saving_second(self.seed_name, self.auto_save_interval) while not self.exit_event.is_set(): try: - time.sleep(self.auto_save_interval) + next_wakeup = (second - get_datetime_second()) % self.auto_save_interval + time.sleep(max(1.0, next_wakeup)) if self.save_dirty: logging.debug("Saving via thread.") self._save() diff --git a/WebHostLib/static/assets/tracker.js b/WebHostLib/static/assets/tracker.js index 0eeda35a..22f6f72f 100644 --- a/WebHostLib/static/assets/tracker.js +++ b/WebHostLib/static/assets/tracker.js @@ -75,10 +75,18 @@ window.addEventListener('load', () => { console.info(tables.search()); tables.draw(); }); + const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); + const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3; + + function getSleepTimeSeconds(){ + // -40 % 60 is -40, which is absolutely wrong and should burn + var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60; + return sleepSeconds || 60; + } const update = () => { const target = $("
"); - const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker'); + console.log("Updating Tracker..."); target.load("/tracker/" + tracker, function (response, status) { if (status === "success") { target.find(".table").each(function (i, new_table) { @@ -97,9 +105,9 @@ window.addEventListener('load', () => { console.log(response); } }) + setTimeout(update, getSleepTimeSeconds()*1000); } - - setInterval(update, 30000); + setTimeout(update, getSleepTimeSeconds()*1000); window.addEventListener('resize', () => { adjustTableHeight(); diff --git a/WebHostLib/templates/genericTracker.html b/WebHostLib/templates/genericTracker.html index fa070ea6..508c084e 100644 --- a/WebHostLib/templates/genericTracker.html +++ b/WebHostLib/templates/genericTracker.html @@ -9,7 +9,7 @@ {% block body %} {% include 'header/dirtHeader.html' %} -
+
This tracker will automatically update itself periodically. diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 307e6b7f..ba6f33a9 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -50,7 +50,7 @@ No file to download for this game. {% endif %} - Tracker + Tracker {% endfor %} diff --git a/WebHostLib/templates/tracker.html b/WebHostLib/templates/tracker.html index 4b97d67e..0f6fe12f 100644 --- a/WebHostLib/templates/tracker.html +++ b/WebHostLib/templates/tracker.html @@ -44,7 +44,7 @@ {%- for player, items in players.items() -%} - {{ loop.index }} {%- if (team, loop.index) in video -%} {%- if video[(team, loop.index)][0] == "Twitch" -%} @@ -121,7 +121,7 @@ {%- for player, checks in players.items() -%} - {{ loop.index }} {{ player_names[(team, loop.index)]|e }} {%- for area in ordered_areas -%} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index dd823c8e..08db5429 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -7,7 +7,7 @@ from uuid import UUID from flask import render_template from werkzeug.exceptions import abort -from MultiServer import Context +from MultiServer import Context, get_saving_second from NetUtils import SlotType from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name @@ -280,16 +280,25 @@ def get_static_room_data(room: Room): player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber]) for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} - + saving_second = get_saving_second(multidata["seed_name"]) result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \ - multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups + multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second _multidata_cache[room.seed.id] = result return result @app.route('/tracker///') -@cache.memoize(timeout=60) # multisave is currently created at most every minute -def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): +def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False): + key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}" + tracker_page = cache.get(key) + if tracker_page: + return tracker_page + timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic) + cache.set(key, tracker_page, timeout) + return tracker_page + + +def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool): # Team and player must be positive and greater than zero if tracked_team < 0 or tracked_player < 1: abort(404) @@ -300,7 +309,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want # Collect seed information and pare it down to a single player locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \ - precollected_items, games, slot_data, groups = get_static_room_data(room) + precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room) player_name = names[tracked_team][tracked_player - 1] location_to_area = player_location_to_area[tracked_player] inventory = collections.Counter() @@ -338,16 +347,18 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want checks_done["Total"] += 1 specific_tracker = game_specific_trackers.get(games[tracked_player], None) if specific_tracker and not want_generic: - return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done, slot_data[tracked_player]) + tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, + seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second) else: - return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, - seed_checks_in_area, checks_done) + tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name, + seed_checks_in_area, checks_done, saving_second) + + return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker @app.route('/generic_tracker///') def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int): - return getPlayerTracker(tracker, tracked_team, tracked_player, True) + return get_player_tracker(tracker, tracked_team, tracked_player, True) def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], @@ -414,7 +425,8 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, + saving_second: int) -> str: icons = { "Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png", @@ -516,14 +528,15 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D inventory=inventory, icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}, - player=player, team=team, room=room, player_name=playerName, + player=player, team=team, room=room, player_name=playerName, saving_second = saving_second, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, + saving_second: int) -> str: icons = { "Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png", @@ -725,7 +738,8 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str: + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], + slot_data: Dict[str, Any], saving_second: int) -> str: icons = { "Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png", @@ -831,7 +845,8 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, + saving_second: int) -> str: icons = { "Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png", @@ -930,8 +945,9 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations **display_data) def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], - inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str: + inventory: Counter, team: int, player: int, playerName: str, + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], + slot_data: Dict, saving_second: int) -> str: SC2WOL_LOC_ID_OFFSET = 1000 SC2WOL_ITEM_ID_OFFSET = 1000 @@ -1173,37 +1189,40 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, **display_data) + def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], inventory: Counter, team: int, player: int, playerName: str, - seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str: + seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], + saving_second: int) -> str: checked_locations = multisave.get("location_checks", {}).get((team, player), set()) player_received_items = {} if multisave.get('version', 0) > 0: - # add numbering to all items but starter_inventory ordered_items = multisave.get('received_items', {}).get((team, player, True), []) else: ordered_items = multisave.get('received_items', {}).get((team, player), []) + # add numbering to all items but starter_inventory for order_index, networkItem in enumerate(ordered_items, start=1): player_received_items[networkItem.item] = order_index return render_template("genericTracker.html", - inventory=inventory, - player=player, team=team, room=room, player_name=playerName, - checked_locations=checked_locations, - not_checked_locations=set(locations[player]) - checked_locations, - received_items=player_received_items) + inventory=inventory, + player=player, team=team, room=room, player_name=playerName, + checked_locations=checked_locations, + not_checked_locations=set(locations[player]) - checked_locations, + received_items=player_received_items, + saving_second=saving_second) @app.route('/tracker/') -@cache.memoize(timeout=60) # multisave is currently created at most every minute +@cache.memoize(timeout=1) # multisave is currently created at most every minute def getTracker(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 = get_static_room_data(room) + precollected_items, games, slot_data, groups, saving_second = 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)}