feature: nade QoL enhancements - #401
Merged
Merged
Conversation
"Constant issues with server making, and getting a connection", and the reporter had already narrowed it correctly: it does not happen over IP:PORT, and it does not happen on dedicated or draft servers. get_match_server_info prefers servers.steam_relay over host:port whenever it is set. That column is only ever written by the 5stack match plugin's ping and by the dedicated-server ping cron -- and a practice pod runs with INSTALL_5STACK_PLUGIN=false, so it never pings. The server row outlives the pod, and reserving it for a new match never cleared the field, so each practice session inherited whatever relay account the row's last occupant had registered. Dedicated servers were fine because their cron keeps the value honest, which is exactly the split that was observed. Booting an on-demand server now clears it, so the connect string falls back to host:port until something reports a real one. The two reservation paths for already-running dedicated servers are deliberately untouched -- their relay is current, not stale. Also adds a practice-side event trigger on utility_lineups: the plugin caches its library and only re-reads it when told to, so a lineup written on the website stayed invisible in game until somebody typed .reload. This is a separate trigger from the search one because that one is not allowed to fire on the geometry columns, and geometry is what a server standing someone on a throw needs to have right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTocnM98q1YiSRN4Z73REh
The other half of the connection fix. Clearing the previous tenant's relay id stops a practice pod handing out a stranger's address, but nothing then ever writes a real one -- the match plugin's ping is what maintains that column everywhere else, and a practice pod does not run the match plugin. So practice was permanently on host:port. It rides on the occupancy heartbeat rather than getting a beat of its own: same fact about the same server on the same schedule, already authenticated by the server key. Deliberately NOT the match plugin's ping endpoint, which also writes plugin_version and plugin_runtime -- those decide how the panel talks to a server, and the utility plugin's version is not the answer to that question. A null is "nothing to say", never "clear it": a pod still registering with the relay would otherwise flap the address out from under someone mid-join. The stale value is cleared when the row is reserved, which is the one moment it is known to be wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTocnM98q1YiSRN4Z73REh
… replaced Nothing could change a lineup that already existed. The only UPDATE on the table set name/description/visibility from the panel's own action, and the plugin had ingest and delete and nothing between them -- so fixing a throw in game meant deleting it and recording it again, which loses the id and with it every scored attempt, favourite and collection entry hanging off it. Authorised on the LINEUP's author, not just the server key. The key proves which server is asking and can never prove which player, and the plugin holding it is the same plugin every player on that pod is talking to -- without the author check anybody on a practice server could rewrite anybody else's lineup and the plugin would have no way to refuse. Text goes through the same sanitizer as ingest, because a name typed in game is echoed straight back into chat and this is a second front door onto input that path already hardens. Geometry is separate and all-or-nothing. A lineup describes one throw as where you stand, where you look and where it lands; a partial write leaves a row describing a throw nobody ever made. Moving it clears the recorded flight and drops confidence to low, because the path that was measured described the old coordinates and a hand-moved point is not something a server watched happen. The rule worth arguing about is whose record gets thrown away. The author's own is theirs to invalidate: past a move further than the success radius their attempts measured a different throw, so they go. Another player's is NOT -- somebody who has drilled a lineup fifty times did not agree to have that turned into a hit rate against a throw they have never made. When anyone else has practised it the move is refused and the caller is pointed at a fork, which is the answer the panel has always given for exactly this reason: a different throw is a new lineup, not an edit to somebody else's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTocnM98q1YiSRN4Z73REh
The edit route answered every refusal with 400, including the two that are refusals rather than malformed requests: editing a lineup you do not own, and moving one somebody else has practised. A plugin told 400 for either would report it to the player as its own bug rather than as the answer it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTocnM98q1YiSRN4Z73REh
The two 403s from PATCH /utility/:id need different words in game -- one is "that is not yours", the other is "others have practised this, save a copy instead" -- and the only way to tell them apart was to match on our prose. The plugin was looking for the substring "fork", which couples it to wording nobody would think twice about rewording. The reason now travels as a code beside the sentence: not_author, already_practised. Pinned by tests, because it is a contract another codebase reads rather than an implementation detail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JTocnM98q1YiSRN4Z73REh
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pairs with web#568 and game-server#177. Mostly feedback from live use of #396.
Map callouts. New
public.map_calloutstable (one row per name, normalised map name) plusUtilityCalloutsService, so a throw names itself — "Mid Smoke from T Spawn" — instead of being called after the map. Seeded from the published extract by a dailySyncMapCalloutsjob and asyncMapCalloutsoperator action;POST /utility/calloutslets a practice server fill in workshop maps the extract doesn't cover, andcdnalways beatsplugin. Same resolution algorithm as web and the plugins: XY containment first, Z as a tie-break, tightest volume wins.PATCH /utility/:id. Nothing could change an existing lineup, so fixing a throw in game meant deleting and re-recording it — losing the id and every scored attempt hanging off it. Authorised on the lineup's author, not just the server key. Geometry is all-or-nothing, and moving it clears the recorded flight. Moving past the success radius resets the author's own attempts but is refused if anyone else has practised it — they get pointed at a fork. Refusals are 403 with a machine-readablereason(not_author,already_practised) rather than prose the plugin has to substring-match.Practice pods handed out a previous tenant's Steam relay.
servers.steam_relayis only written by the match plugin's ping, and a practice pod doesn't run it — so each session inherited the last occupant's relay address. Booting an on-demand server now clears it, and pods report their own relay on the occupancy heartbeat.Stale in-game library. New event trigger on
utility_lineupspushes a refresh to servers on that map, so a lineup written on the website no longer needs a.reload.Telemetry. A feature no panel has reported yet is listed as not measured instead of being dropped.
Needs
hasura migrateandhasura metadata apply.