Skip to content

Commit a2ffeec

Browse files
committed
Merge branch 'main' into smw-main
2 parents f65d47b + e0be796 commit a2ffeec

40 files changed

+3371
-101
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*_Spoiler.txt
55
*.bmbp
66
*.apbp
7+
*.apl2ac
78
*.apm3
89
*.apmc
910
*.apz5
@@ -135,6 +136,7 @@ venv/
135136
ENV/
136137
env.bak/
137138
venv.bak/
139+
.code-workspace
138140

139141
# Spyder project settings
140142
.spyderproject

MultiServer.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import itertools
1717
import time
1818
import operator
19+
import hashlib
1920

2021
import ModuleUpdate
2122

@@ -58,6 +59,12 @@
5859
}
5960

6061

62+
def get_saving_second(seed_name: str, interval: int = 60) -> int:
63+
# save at expected times so other systems using savegame can expect it
64+
# represents the target second of the auto_save_interval at which to save
65+
return int(hashlib.sha256(seed_name.encode()).hexdigest(), 16) % interval
66+
67+
6168
class Client(Endpoint):
6269
version = Version(0, 0, 0)
6370
tags: typing.List[str] = []
@@ -463,10 +470,16 @@ def init_save(self, enabled: bool = True):
463470
def _start_async_saving(self):
464471
if not self.auto_saver_thread:
465472
def save_regularly():
466-
import time
473+
# time.time() is platform dependent, so using the expensive datetime method instead
474+
def get_datetime_second():
475+
now = datetime.datetime.now()
476+
return now.second + now.microsecond * 0.000001
477+
478+
second = get_saving_second(self.seed_name, self.auto_save_interval)
467479
while not self.exit_event.is_set():
468480
try:
469-
time.sleep(self.auto_save_interval)
481+
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
482+
time.sleep(max(1.0, next_wakeup))
470483
if self.save_dirty:
471484
logging.debug("Saving via thread.")
472485
self._save()

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Currently, the following games are supported:
3333
* Hylics 2
3434
* Overcooked! 2
3535
* Zillion
36+
* Lufia II Ancient Cave
3637

3738
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
3839
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

Utils.py

+3
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,9 @@ def get_default_options() -> OptionsType:
307307
"ffr_options": {
308308
"display_msgs": True,
309309
},
310+
"lufia2ac_options": {
311+
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
312+
},
310313
}
311314
return options
312315

WebHostLib/options.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str:
7171

7272
del file_data
7373

74-
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
74+
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
7575
f.write(res)
7676

7777
# Generate JSON files for player-settings pages

WebHostLib/static/assets/tracker.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,18 @@ window.addEventListener('load', () => {
7575
console.info(tables.search());
7676
tables.draw();
7777
});
78+
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
79+
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
80+
81+
function getSleepTimeSeconds(){
82+
// -40 % 60 is -40, which is absolutely wrong and should burn
83+
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
84+
return sleepSeconds || 60;
85+
}
7886

7987
const update = () => {
8088
const target = $("<div></div>");
81-
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
89+
console.log("Updating Tracker...");
8290
target.load("/tracker/" + tracker, function (response, status) {
8391
if (status === "success") {
8492
target.find(".table").each(function (i, new_table) {
@@ -97,9 +105,9 @@ window.addEventListener('load', () => {
97105
console.log(response);
98106
}
99107
})
108+
setTimeout(update, getSleepTimeSeconds()*1000);
100109
}
101-
102-
setInterval(update, 30000);
110+
setTimeout(update, getSleepTimeSeconds()*1000);
103111

104112
window.addEventListener('resize', () => {
105113
adjustTableHeight();

WebHostLib/templates/genericTracker.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
{% block body %}
1111
{% include 'header/dirtHeader.html' %}
12-
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
12+
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
1313
<div id="tracker-header-bar">
1414
<input placeholder="Search" id="search"/>
1515
<span class="info">This tracker will automatically update itself periodically.</span>

WebHostLib/templates/macros.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
No file to download for this game.
5151
{% endif %}
5252
</td>
53-
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
53+
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
5454
</tr>
5555
{% endfor %}
5656
</tbody>

WebHostLib/templates/sc2wolTracker.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@
137137
</td>
138138
</tr>
139139
<tr>
140-
<td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Ghost" /></td>
141-
<td colspan="2"><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Spectre" /></td>
142-
<td colspan="2"><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Thor" /></td>
140+
<td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
141+
<td colspan="2"><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
142+
<td colspan="2"><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
143143
</tr>
144144
<tr>
145145
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>

WebHostLib/templates/tracker.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<tbody>
4545
{%- for player, items in players.items() -%}
4646
<tr>
47-
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
47+
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
4848
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
4949
{%- if (team, loop.index) in video -%}
5050
{%- if video[(team, loop.index)][0] == "Twitch" -%}
@@ -121,7 +121,7 @@
121121
<tbody>
122122
{%- for player, checks in players.items() -%}
123123
<tr>
124-
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
124+
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
125125
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
126126
<td>{{ player_names[(team, loop.index)]|e }}</td>
127127
{%- for area in ordered_areas -%}

WebHostLib/tracker.py

+48-28
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from flask import render_template
88
from werkzeug.exceptions import abort
99

10-
from MultiServer import Context
10+
from MultiServer import Context, get_saving_second
1111
from NetUtils import SlotType
1212
from Utils import restricted_loads
1313
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):
280280
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
281281
for playernumber in range(1, len(names[0]) + 1)
282282
if playernumber not in groups}
283-
283+
saving_second = get_saving_second(multidata["seed_name"])
284284
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
285-
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups
285+
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second
286286
_multidata_cache[room.seed.id] = result
287287
return result
288288

289289

290290
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
291-
@cache.memoize(timeout=60) # multisave is currently created at most every minute
292-
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
291+
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
292+
key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}"
293+
tracker_page = cache.get(key)
294+
if tracker_page:
295+
return tracker_page
296+
timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic)
297+
cache.set(key, tracker_page, timeout)
298+
return tracker_page
299+
300+
301+
def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool):
293302
# Team and player must be positive and greater than zero
294303
if tracked_team < 0 or tracked_player < 1:
295304
abort(404)
@@ -300,7 +309,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
300309

301310
# Collect seed information and pare it down to a single player
302311
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
303-
precollected_items, games, slot_data, groups = get_static_room_data(room)
312+
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
304313
player_name = names[tracked_team][tracked_player - 1]
305314
location_to_area = player_location_to_area[tracked_player]
306315
inventory = collections.Counter()
@@ -338,21 +347,24 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
338347
checks_done["Total"] += 1
339348
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
340349
if specific_tracker and not want_generic:
341-
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
342-
seed_checks_in_area, checks_done, slot_data[tracked_player])
350+
tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
351+
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
343352
else:
344-
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
345-
seed_checks_in_area, checks_done)
353+
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
354+
seed_checks_in_area, checks_done, saving_second)
355+
356+
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
346357

347358

348359
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
349360
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
350-
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
361+
return get_player_tracker(tracker, tracked_team, tracked_player, True)
351362

352363

353364
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
354365
inventory: Counter, team: int, player: int, player_name: str,
355-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
366+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
367+
saving_second: int) -> str:
356368

357369
# Note the presence of the triforce item
358370
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
@@ -414,7 +426,8 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[
414426

415427
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
416428
inventory: Counter, team: int, player: int, playerName: str,
417-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
429+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
430+
saving_second: int) -> str:
418431

419432
icons = {
420433
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
@@ -516,14 +529,15 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
516529
inventory=inventory, icons=icons,
517530
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
518531
id in lookup_any_item_id_to_name},
519-
player=player, team=team, room=room, player_name=playerName,
532+
player=player, team=team, room=room, player_name=playerName, saving_second = saving_second,
520533
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
521534
**display_data)
522535

523536

524537
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
525538
inventory: Counter, team: int, player: int, playerName: str,
526-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
539+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
540+
saving_second: int) -> str:
527541

528542
icons = {
529543
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
@@ -725,7 +739,8 @@ def lookup_and_trim(id, area):
725739

726740
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
727741
inventory: Counter, team: int, player: int, playerName: str,
728-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
742+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
743+
slot_data: Dict[str, Any], saving_second: int) -> str:
729744

730745
icons = {
731746
"Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
@@ -831,7 +846,8 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
831846

832847
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
833848
inventory: Counter, team: int, player: int, playerName: str,
834-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
849+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
850+
saving_second: int) -> str:
835851

836852
icons = {
837853
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png",
@@ -930,8 +946,9 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
930946
**display_data)
931947

932948
def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
933-
inventory: Counter, team: int, player: int, playerName: str,
934-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
949+
inventory: Counter, team: int, player: int, playerName: str,
950+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
951+
slot_data: Dict, saving_second: int) -> str:
935952

936953
SC2WOL_LOC_ID_OFFSET = 1000
937954
SC2WOL_ITEM_ID_OFFSET = 1000
@@ -1173,37 +1190,40 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
11731190
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
11741191
**display_data)
11751192

1193+
11761194
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
11771195
inventory: Counter, team: int, player: int, playerName: str,
1178-
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
1196+
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
1197+
saving_second: int) -> str:
11791198

11801199
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
11811200
player_received_items = {}
11821201
if multisave.get('version', 0) > 0:
1183-
# add numbering to all items but starter_inventory
11841202
ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
11851203
else:
11861204
ordered_items = multisave.get('received_items', {}).get((team, player), [])
11871205

1206+
# add numbering to all items but starter_inventory
11881207
for order_index, networkItem in enumerate(ordered_items, start=1):
11891208
player_received_items[networkItem.item] = order_index
11901209

11911210
return render_template("genericTracker.html",
1192-
inventory=inventory,
1193-
player=player, team=team, room=room, player_name=playerName,
1194-
checked_locations=checked_locations,
1195-
not_checked_locations=set(locations[player]) - checked_locations,
1196-
received_items=player_received_items)
1211+
inventory=inventory,
1212+
player=player, team=team, room=room, player_name=playerName,
1213+
checked_locations=checked_locations,
1214+
not_checked_locations=set(locations[player]) - checked_locations,
1215+
received_items=player_received_items,
1216+
saving_second=saving_second)
11971217

11981218

11991219
@app.route('/tracker/<suuid:tracker>')
1200-
@cache.memoize(timeout=60) # multisave is currently created at most every minute
1220+
@cache.memoize(timeout=1) # multisave is currently created at most every minute
12011221
def getTracker(tracker: UUID):
12021222
room: Room = Room.get(tracker=tracker)
12031223
if not room:
12041224
abort(404)
12051225
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
1206-
precollected_items, games, slot_data, groups = get_static_room_data(room)
1226+
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
12071227

12081228
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups}
12091229
for teamnumber, team in enumerate(names)}

host.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ sni_options:
9393
lttp_options:
9494
# File name of the v1.0 J rom
9595
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
96+
lufia2ac_options:
97+
# File name of the US rom
98+
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
9699
sm_options:
97100
# File name of the v1.0 J rom
98101
rom_file: "Super Metroid (JU).sfc"

0 commit comments

Comments
 (0)