Fix(undertale): prevent massive bounce msg spam for position updates (#5990)

* fix(undertale): prevent massive bounce msg spam for position updates

* make sure player is removed on leaving / timing out

* do not check for tags: online, as bounce evaluation is or'd
This commit is contained in:
Chris W
2026-02-28 23:56:28 +01:00
committed by GitHub
parent fcccbfca65
commit ff5402c410

View File

@@ -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,6 +289,11 @@ 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"]
@@ -368,9 +391,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 +403,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)