mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
3 Commits
fcccbfca65
...
e49ba2ff6f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e49ba2ff6f | ||
|
|
61d5120f66 | ||
|
|
ff5402c410 |
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
import typing
|
import typing
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
@@ -15,6 +16,9 @@ from CommonClient import CommonContext, server_loop, \
|
|||||||
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||||
from Utils import async_start
|
from Utils import async_start
|
||||||
|
|
||||||
|
# Heartbeat for position sharing via bounces, in seconds
|
||||||
|
UNDERTALE_STATUS_INTERVAL = 30.0
|
||||||
|
UNDERTALE_ONLINE_TIMEOUT = 60.0
|
||||||
|
|
||||||
class UndertaleCommandProcessor(ClientCommandProcessor):
|
class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||||
def __init__(self, ctx):
|
def __init__(self, ctx):
|
||||||
@@ -109,6 +113,11 @@ class UndertaleContext(CommonContext):
|
|||||||
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
self.completed_routes = {"pacifist": 0, "genocide": 0, "neutral": 0}
|
||||||
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
# self.save_game_folder: files go in this path to pass data between us and the actual game
|
||||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
self.last_sent_position: typing.Optional[tuple] = None
|
||||||
|
self.last_room: typing.Optional[str] = None
|
||||||
|
self.last_status_write: float = 0.0
|
||||||
|
self.other_undertale_status: dict[int, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
with open(Utils.user_path("Undertale", "data.win"), "rb") as f:
|
||||||
@@ -219,6 +228,9 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
await ctx.send_msgs([{"cmd": "SetNotify", "keys": [str(ctx.slot)+" RoutesDone neutral",
|
||||||
str(ctx.slot)+" RoutesDone pacifist",
|
str(ctx.slot)+" RoutesDone pacifist",
|
||||||
str(ctx.slot)+" RoutesDone genocide"]}])
|
str(ctx.slot)+" RoutesDone genocide"]}])
|
||||||
|
if any(info.game == "Undertale" and slot != ctx.slot
|
||||||
|
for slot, info in ctx.slot_info.items()):
|
||||||
|
ctx.set_notify("undertale_room_status")
|
||||||
if args["slot_data"]["only_flakes"]:
|
if args["slot_data"]["only_flakes"]:
|
||||||
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, "GenoNoChest.flag"), "w") as f:
|
||||||
f.close()
|
f.close()
|
||||||
@@ -263,6 +275,12 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
if str(ctx.slot)+" RoutesDone pacifist" in args["keys"]:
|
||||||
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
if args["keys"][str(ctx.slot) + " RoutesDone pacifist"] is not None:
|
||||||
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
ctx.completed_routes["pacifist"] = args["keys"][str(ctx.slot)+" RoutesDone pacifist"]
|
||||||
|
if "undertale_room_status" in args["keys"] and args["keys"]["undertale_room_status"]:
|
||||||
|
status = args["keys"]["undertale_room_status"]
|
||||||
|
ctx.other_undertale_status = {
|
||||||
|
int(key): val for key, val in status.items()
|
||||||
|
if int(key) != ctx.slot
|
||||||
|
}
|
||||||
elif cmd == "SetReply":
|
elif cmd == "SetReply":
|
||||||
if args["value"] is not None:
|
if args["value"] is not None:
|
||||||
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
if str(ctx.slot)+" RoutesDone pacifist" == args["key"]:
|
||||||
@@ -271,17 +289,19 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
ctx.completed_routes["genocide"] = args["value"]
|
ctx.completed_routes["genocide"] = args["value"]
|
||||||
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
elif str(ctx.slot)+" RoutesDone neutral" == args["key"]:
|
||||||
ctx.completed_routes["neutral"] = args["value"]
|
ctx.completed_routes["neutral"] = args["value"]
|
||||||
|
if args.get("key") == "undertale_room_status" and args.get("value"):
|
||||||
|
ctx.other_undertale_status = {
|
||||||
|
int(key): val for key, val in args["value"].items()
|
||||||
|
if int(key) != ctx.slot
|
||||||
|
}
|
||||||
elif cmd == "ReceivedItems":
|
elif cmd == "ReceivedItems":
|
||||||
start_index = args["index"]
|
start_index = args["index"]
|
||||||
|
|
||||||
if start_index == 0:
|
if start_index == 0:
|
||||||
ctx.items_received = []
|
ctx.items_received = []
|
||||||
elif start_index != len(ctx.items_received):
|
elif start_index != len(ctx.items_received):
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
await ctx.check_locations(ctx.locations_checked)
|
||||||
if ctx.locations_checked:
|
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||||
sync_msg.append({"cmd": "LocationChecks",
|
|
||||||
"locations": list(ctx.locations_checked)})
|
|
||||||
await ctx.send_msgs(sync_msg)
|
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
counter = -1
|
counter = -1
|
||||||
placedWeapon = 0
|
placedWeapon = 0
|
||||||
@@ -368,9 +388,8 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
elif cmd == "Bounced":
|
elif cmd == "Bounced":
|
||||||
tags = args.get("tags", [])
|
data = args.get("data", {})
|
||||||
if "Online" in tags:
|
if "x" in data and "room" in data:
|
||||||
data = args.get("data", {})
|
|
||||||
if data["player"] != ctx.slot and data["player"] is not None:
|
if data["player"] != ctx.slot and data["player"] is not None:
|
||||||
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
filename = f"FRISK" + str(data["player"]) + ".playerspot"
|
||||||
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
with open(os.path.join(ctx.save_game_folder, filename), "w") as f:
|
||||||
@@ -381,21 +400,63 @@ async def process_undertale_cmd(ctx: UndertaleContext, cmd: str, args: dict):
|
|||||||
|
|
||||||
async def multi_watcher(ctx: UndertaleContext):
|
async def multi_watcher(ctx: UndertaleContext):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
path = ctx.save_game_folder
|
if "Online" in ctx.tags and any(
|
||||||
for root, dirs, files in os.walk(path):
|
info.game == "Undertale" and slot != ctx.slot
|
||||||
for file in files:
|
for slot, info in ctx.slot_info.items()):
|
||||||
if "spots.mine" in file and "Online" in ctx.tags:
|
now = time.time()
|
||||||
with open(os.path.join(root, file), "r") as mine:
|
path = ctx.save_game_folder
|
||||||
this_x = mine.readline()
|
for root, dirs, files in os.walk(path):
|
||||||
this_y = mine.readline()
|
for file in files:
|
||||||
this_room = mine.readline()
|
if "spots.mine" in file:
|
||||||
this_sprite = mine.readline()
|
with open(os.path.join(root, file), "r") as mine:
|
||||||
this_frame = mine.readline()
|
this_x = mine.readline()
|
||||||
mine.close()
|
this_y = mine.readline()
|
||||||
message = [{"cmd": "Bounce", "tags": ["Online"],
|
this_room = mine.readline()
|
||||||
"data": {"player": ctx.slot, "x": this_x, "y": this_y, "room": this_room,
|
this_sprite = mine.readline()
|
||||||
"spr": this_sprite, "frm": this_frame}}]
|
this_frame = mine.readline()
|
||||||
await ctx.send_msgs(message)
|
|
||||||
|
if this_room != ctx.last_room or \
|
||||||
|
now - ctx.last_status_write >= UNDERTALE_STATUS_INTERVAL:
|
||||||
|
ctx.last_room = this_room
|
||||||
|
ctx.last_status_write = now
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "Set",
|
||||||
|
"key": "undertale_room_status",
|
||||||
|
"default": {},
|
||||||
|
"want_reply": False,
|
||||||
|
"operations": [{"operation": "update",
|
||||||
|
"value": {str(ctx.slot): {"room": this_room,
|
||||||
|
"time": now}}}]
|
||||||
|
}])
|
||||||
|
|
||||||
|
# If player was visible but timed out (heartbeat) or left the room, remove them.
|
||||||
|
for slot, entry in ctx.other_undertale_status.items():
|
||||||
|
if entry.get("room") != this_room or \
|
||||||
|
now - entry.get("time", now) > UNDERTALE_ONLINE_TIMEOUT:
|
||||||
|
playerspot = os.path.join(ctx.save_game_folder,
|
||||||
|
f"FRISK{slot}.playerspot")
|
||||||
|
if os.path.exists(playerspot):
|
||||||
|
os.remove(playerspot)
|
||||||
|
|
||||||
|
current_position = (this_x, this_y, this_room, this_sprite, this_frame)
|
||||||
|
if current_position == ctx.last_sent_position:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Empty status dict = no data yet → send to bootstrap.
|
||||||
|
online_in_room = any(
|
||||||
|
entry.get("room") == this_room and
|
||||||
|
now - entry.get("time", now) <= UNDERTALE_ONLINE_TIMEOUT
|
||||||
|
for entry in ctx.other_undertale_status.values()
|
||||||
|
)
|
||||||
|
if ctx.other_undertale_status and not online_in_room:
|
||||||
|
continue
|
||||||
|
|
||||||
|
message = [{"cmd": "Bounce", "games": ["Undertale"],
|
||||||
|
"data": {"player": ctx.slot, "x": this_x, "y": this_y,
|
||||||
|
"room": this_room, "spr": this_sprite,
|
||||||
|
"frm": this_frame}}]
|
||||||
|
await ctx.send_msgs(message)
|
||||||
|
ctx.last_sent_position = current_position
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
@@ -409,10 +470,9 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for file in files:
|
for file in files:
|
||||||
if ".item" in file:
|
if ".item" in file:
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(os.path.join(root, file))
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
await ctx.check_locations(ctx.locations_checked)
|
||||||
if ctx.locations_checked:
|
await ctx.send_msgs([{"cmd": "Sync"}])
|
||||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
|
||||||
await ctx.send_msgs(sync_msg)
|
|
||||||
ctx.syncing = False
|
ctx.syncing = False
|
||||||
if ctx.got_deathlink:
|
if ctx.got_deathlink:
|
||||||
ctx.got_deathlink = False
|
ctx.got_deathlink = False
|
||||||
@@ -447,7 +507,7 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for l in lines:
|
for l in lines:
|
||||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||||
finally:
|
finally:
|
||||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": sending}])
|
await ctx.check_locations(sending)
|
||||||
if "victory" in file and str(ctx.route) in file:
|
if "victory" in file and str(ctx.route) in file:
|
||||||
victory = True
|
victory = True
|
||||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
|
|||||||
4
Utils.py
4
Utils.py
@@ -23,6 +23,7 @@ from time import sleep
|
|||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||||
from yaml import load, load_all, dump
|
from yaml import load, load_all, dump
|
||||||
from pathspec import PathSpec, GitIgnoreSpec
|
from pathspec import PathSpec, GitIgnoreSpec
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||||
@@ -315,6 +316,7 @@ def get_public_ipv6() -> str:
|
|||||||
return ip
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||||
def get_options() -> Settings:
|
def get_options() -> Settings:
|
||||||
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
deprecate("Utils.get_options() is deprecated. Use the settings API instead.")
|
||||||
return get_settings()
|
return get_settings()
|
||||||
@@ -1003,6 +1005,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
|||||||
|
|
||||||
|
|
||||||
def deprecate(message: str, add_stacklevels: int = 0):
|
def deprecate(message: str, add_stacklevels: int = 0):
|
||||||
|
"""also use typing_extensions.deprecated wherever you use this"""
|
||||||
if __debug__:
|
if __debug__:
|
||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||||
@@ -1067,6 +1070,7 @@ def _extend_freeze_support() -> None:
|
|||||||
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
multiprocessing.freeze_support = multiprocessing.spawn.freeze_support = _freeze_support if is_frozen() else _noop
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Use multiprocessing.freeze_support() instead")
|
||||||
def freeze_support() -> None:
|
def freeze_support() -> None:
|
||||||
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
"""This now only calls multiprocessing.freeze_support since we are patching freeze_support on module load."""
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
|||||||
Reference in New Issue
Block a user