Skip to content

feature: plugin directory - #394

Merged
lukepolo merged 8 commits into
mainfrom
feat/game-plugins-and-modes
Aug 19, 2026
Merged

feature: plugin directory#394
lukepolo merged 8 commits into
mainfrom
feat/game-plugins-and-modes

Conversation

@lukepolo

Copy link
Copy Markdown
Contributor

No description provided.

@lukepolo lukepolo changed the title Add game plugin registry, installs and game modes feature: plugin directory Aug 19, 2026
Managed installs now land in the directory operators are already told to
manage plugins in, so a plugin config written on first load appears where
it is expected and a game server needs no extra mount to see it.

A manifest records which files each plugin owns, and setup.sh links the
directory while skipping managed plugins the match mode did not select --
so a ranked match still inherits nothing. Files absent from the manifest
are hand-placed and always link, exactly as before.
Neither was ever read and neither could be set -- the mode form has no
field for them. A mode can only pre-fill match options anyway, since the
options are locked once a match is Live, and match_option_defaults was
untyped jsonb shadowing 26 typed columns where a mistyped key fails
silently.

Adds SQL cover for the part that matters: an unranked match still records
kills and stays off the leaderboard.
Twelve migrations that only ever shipped together, three of which existed
to undo columns added by the others. The squashed version creates the
final schema directly, so a fresh install never grows game_modes.runtime,
map_pool_id or match_option_defaults just to drop them again.

Also covers the ELO boundary further: an unranked match must leave a
rating on exactly the number it was, and the next ranked match must still
move it.
…delines

link_plugins gates a plugin's files on slug@version being on the node's own
disk, but the version was taken from whichever node had reported most
recently. On a partially converged fleet the mode booted with its cvars and
none of its plugins, and nothing said so. Resolution is now scoped to the
server's node and that node's runtime, and game_mode_plugins.required is
enforced instead of stored and ignored -- a mode whose required plugin never
arrived fails the server start rather than quietly running vanilla.

The rest of the review of this branch:

- install state joins game_server_nodes and filters the same way the target
  count does, so a plugin present only on disabled nodes stops reporting
  Installed and a Failed row on a decommissioned node stops pinning it;
- an Auto install resolves releases, not release candidates;
- Pending and Installing rows are stamped with the runtime that node runs,
  and a corrected runtime overwrites a stale one;
- previewGameMode goes through the always-load merge it exists to show, and
  environmentFor is the one place that knows what the pod reads;
- the push-to-node install path, the Reconcile queue and two settings the
  API never read are gone.

A plugin can now declare it cannot work while the framework enforces Valve's
server guidelines, and an administrator opts in per plugin; a server loading
one gets DISABLE_SERVER_GUIDELINES and setup.sh patches the framework config
before it starts. Both halves are required, and it is decided from the
plugins that server actually loads, so opting in for one plugin leaves every
other server compliant.
The catalog is curated and CS2 has more plugins than it lists, so the answer to
"it is not in the directory" was nothing at all. addCustomGamePlugin takes a
release URL -- a repository, a release, or the archive itself -- resolves it to
a concrete download, hashes the archive once, and writes the same rows a sync
writes. Everything downstream is unchanged: modes select it, nodes converge to
it, and each node still verifies its own download against a digest, because the
digest was taken by something that saw the bytes.

A repository resolves to the newest Linux archive of its latest release, the
same choice the registry build makes. The framework is always the operator's
call: nothing in an archive says which one it was built for, and the wrong one
unpacks perfectly and never loads.

game_plugins.source is what keeps the two apart. A sync owns 'registry' rows
and prunes the ones that stopped being published; it will not overwrite,
prune, or take over a 'custom' row, so a plugin added because the catalog
lacked it survives the poll fifteen minutes later -- including the case where
the catalog later publishes that same slug.
@lukepolo
lukepolo merged commit d168231 into main Aug 19, 2026
2 checks passed
@lukepolo
lukepolo deleted the feat/game-plugins-and-modes branch August 19, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant