Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hasura/metadata/actions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,12 @@ input ServerPluginInput {
enabled: Boolean!
}

input ServerPluginConfigInput {
slug: String!
cfg: String
config: jsonb
}

type ImportedWorkshopMap {
id: uuid!
name: String!
Expand Down Expand Up @@ -2725,6 +2731,7 @@ type Mutation {
server_id: uuid!
map_rotation: ServerMapRotationInput
plugins: [ServerPluginInput!]
plugin_configs: [ServerPluginConfigInput!]
access: ServerAccessInput
): SuccessOutput
}
Expand Down
3 changes: 2 additions & 1 deletion hasura/metadata/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2058,7 +2058,7 @@ actions:
forward_client_headers: true
permissions:
- role: administrator
comment: Save a community server's rotation, plugins and access, restarting it at most once
comment: Save a community server's rotation, plugins, plugin configs and access, restarting it at most once
- name: moveDedicatedServerToNode
definition:
kind: synchronous
Expand Down Expand Up @@ -2088,6 +2088,7 @@ custom_types:
- name: UtilityPlaybookStepInput
- name: UtilitySightlinePairInput
- name: ServerPluginInput
- name: ServerPluginConfigInput
- name: ServerMapRotationInput
- name: ServerAccessInput
objects:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
table:
name: server_plugin_configs
schema: public
object_relationships:
- name: plugin
using:
foreign_key_constraint_on: plugin_slug
- name: server
using:
foreign_key_constraint_on: server_id
select_permissions:
- role: administrator
permission:
columns:
- server_id
- plugin_slug
- cfg
- config
- updated_at
filter: {}
comment: ""
7 changes: 7 additions & 0 deletions hasura/metadata/databases/default/tables/public_servers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ array_relationships:
table:
name: server_plugins
schema: public
- name: plugin_configs
using:
foreign_key_constraint_on:
column: server_id
table:
name: server_plugin_configs
schema: public
computed_fields:
- name: connection_link
definition:
Expand Down
1 change: 1 addition & 0 deletions hasura/metadata/databases/default/tables/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
- "!include public_server_map_rotation.yaml"
- "!include public_server_migrations.yaml"
- "!include public_server_player_sessions.yaml"
- "!include public_server_plugin_configs.yaml"
- "!include public_server_plugins.yaml"
- "!include public_server_recent_players.yaml"
- "!include public_server_regions.yaml"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS public.server_plugin_configs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- A plugin's cvars and config file set for one community server, layered over
-- what the plugin's own page sets for every server that loads it.
CREATE TABLE IF NOT EXISTS public.server_plugin_configs (
server_id uuid NOT NULL,
plugin_slug text NOT NULL,
cfg text,
config jsonb,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (server_id, plugin_slug),
CONSTRAINT server_plugin_configs_server_fkey FOREIGN KEY (server_id)
REFERENCES public.servers (id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT server_plugin_configs_plugin_fkey FOREIGN KEY (plugin_slug)
REFERENCES public.game_plugins (slug) ON UPDATE CASCADE ON DELETE CASCADE
);
84 changes: 83 additions & 1 deletion src/dedicated-servers/dedicated-server-config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export class DedicatedServerConfigService {
settings: {
mapRotation: { mapIds: Array<string>; shuffle: boolean } | null;
plugins: Array<{ slug: string; enabled: boolean }> | null;
pluginConfigs?: Array<{
slug: string;
cfg: string | null;
config: unknown;
}> | null;
access: {
restricted: boolean;
minRole: string | null;
Expand All @@ -66,11 +71,17 @@ export class DedicatedServerConfigService {
await this.setPlugins(serverId, settings.plugins, { restart: false });
}

if (settings.pluginConfigs) {
await this.setPluginConfigs(serverId, settings.pluginConfigs, {
restart: false,
});
}

if (settings.access) {
await this.setAccess(serverId, settings.access);
}

if (settings.mapRotation || settings.plugins) {
if (settings.mapRotation || settings.plugins || settings.pluginConfigs) {
await this.restart(await this.communityServer(serverId));
}
}
Expand Down Expand Up @@ -161,6 +172,77 @@ export class DedicatedServerConfigService {
}
}

// One server's layer over what the plugin's own page sets for every server.
// Only the entries sent are touched, and one with neither cvars nor a file
// drops the server back to the plugin page's.
public async setPluginConfigs(
serverId: string,
configs: Array<{ slug: string; cfg: string | null; config: unknown }>,
options: { restart?: boolean } = {},
): Promise<void> {
const server = await this.communityServer(serverId);

const plugins = new Map(
(
await this.postgres.query<
Array<{ slug: string; name: string; config_path: string | null }>
>(
`SELECT slug, name, config_path FROM game_plugins
WHERE slug = ANY($1::text[])`,
[configs.map((entry) => entry.slug)],
)
).map((plugin) => [plugin.slug, plugin]),
);

for (const entry of configs) {
const plugin = plugins.get(entry.slug);

if (!plugin) {
throw new BadRequestException(`${entry.slug} is not in the catalog`);
}

if (entry.config != null && !plugin.config_path) {
throw new BadRequestException(`${plugin.name} has no config file`);
}
}

await this.postgres.transaction(async (client) => {
for (const entry of configs) {
const cfg = entry.cfg?.trim() ? entry.cfg : null;
const config = entry.config ?? null;

if (cfg === null && config === null) {
await client.query(
`DELETE FROM server_plugin_configs
WHERE server_id = $1 AND plugin_slug = $2`,
[serverId, entry.slug],
);
continue;
}

await client.query(
`INSERT INTO server_plugin_configs
(server_id, plugin_slug, cfg, config, updated_at)
VALUES ($1, $2, $3, $4, now())
ON CONFLICT (server_id, plugin_slug) DO UPDATE SET
cfg = EXCLUDED.cfg,
config = EXCLUDED.config,
updated_at = now()`,
[
serverId,
entry.slug,
cfg,
config === null ? null : JSON.stringify(config),
],
);
}
});

if (options.restart !== false) {
await this.restart(server);
}
}

// Enforced by the Player Management plugin, which rereads the list on a
// refresh, so unlike the pod settings above this needs no restart and works
// on external servers too.
Expand Down
11 changes: 11 additions & 0 deletions src/dedicated-servers/dedicated-servers.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export class DedicatedServersController {
server_id: string;
map_rotation?: { map_ids: Array<string>; shuffle: boolean } | null;
plugins?: Array<{ slug: string; enabled: boolean }> | null;
plugin_configs?: Array<{
slug: string;
cfg?: string | null;
config?: unknown;
}> | null;
access?: {
restricted: boolean;
min_role?: string | null;
Expand All @@ -194,6 +199,12 @@ export class DedicatedServersController {
}
: null,
plugins: data.plugins ?? null,
pluginConfigs:
data.plugin_configs?.map((entry) => ({
slug: entry.slug,
cfg: entry.cfg ?? null,
config: entry.config ?? null,
})) ?? null,
access: data.access
? {
restricted: data.access.restricted,
Expand Down
24 changes: 23 additions & 1 deletion src/dedicated-servers/dedicated-servers.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,23 @@ describe("DedicatedServersService.withServerCfg", () => {
});
});

// A server's own value for a plugin is the most specific, so it lands last.
it("runs a server's own plugin cvars after the mode's", () => {
expect(
files(
DedicatedServersService.withServerCfg(
mode(),
"Custom" as never,
[{ slug: "deathmatch", cfg: "dm_replenish_health 10" }],
[{ slug: "deathmatch", cfg: "dm_replenish_health 50" }],
),
),
).toEqual({
"cfg/gamemode_deathmatch_server.cfg":
"dm_replenish_health 10\nmp_teammates_are_enemies 1\ndm_replenish_health 50\n",
});
});

it("writes plugin cvars on a server whose mode has none of its own", () => {
expect(
files(
Expand Down Expand Up @@ -732,6 +749,11 @@ describe("DedicatedServersService.setupDedicatedServer", () => {
.mockResolvedValue([
{ slug: "inventory-simulator", cfg: inventoryCfg },
]),
serverCfgLayers: jest
.fn()
.mockResolvedValue([
{ slug: "inventory-simulator", cfg: "invsim_ws_enabled 0" },
]),
environmentFor: GameModesService.prototype.environmentFor,
};

Expand Down Expand Up @@ -792,7 +814,7 @@ describe("DedicatedServersService.setupDedicatedServer", () => {
// only a match execs plugin cvars, so it ran on the plugin's own defaults.
it("gives a community server the cvars of the plugins it loads", async () => {
expect(await setup("Casual")).toEqual({
"cfg/gamemode_casual_server.cfg": `${inventoryCfg}\n`,
"cfg/gamemode_casual_server.cfg": `${inventoryCfg}\ninvsim_ws_enabled 0\n`,
});
});

Expand Down
20 changes: 15 additions & 5 deletions src/dedicated-servers/dedicated-servers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,17 @@ export class DedicatedServersService {

// Ranked and Practice servers host matches, and a match execs each
// plugin's cvars itself.
const community = server.type !== "Ranked" && server.type !== "Practice";

const gameMode = DedicatedServersService.withServerCfg(
resolvedMode,
server.type,
server.type === "Ranked" || server.type === "Practice"
? []
: await this.gameModesService.pluginCfgLayers(resolvedMode),
community
? await this.gameModesService.pluginCfgLayers(resolvedMode)
: [],
community
? await this.gameModesService.serverCfgLayers(serverId, resolvedMode)
: [],
);

const gameModeEnvironment =
Expand Down Expand Up @@ -746,18 +751,23 @@ export class DedicatedServersService {
// A community server has no match, so the cvars a match would exec -- each
// loading plugin's, then the mode's -- ride in as the server config CS2
// execs after the Valve mode's own, on every map load, which is also what
// keeps them across a rotation.
// keeps them across a rotation. The server's own plugin cvars run last.
public static withServerCfg(
mode: ResolvedGameMode | null,
type: e_server_types_enum,
pluginCfgs: Array<{ slug: string; cfg: string }> = [],
serverCfgs: Array<{ slug: string; cfg: string }> = [],
): ResolvedGameMode | null {
const serverCfg = DedicatedServersService.valveModeFor(
type,
mode?.valveMode,
).serverCfg;

const cfg = [...pluginCfgs.map((layer) => layer.cfg), mode?.cfg ?? ""]
const cfg = [
...pluginCfgs.map((layer) => layer.cfg),
mode?.cfg ?? "",
...serverCfgs.map((layer) => layer.cfg),
]
.filter((block) => block.trim())
.map((block) => (block.endsWith("\n") ? block : `${block}\n`))
.join("");
Expand Down
Loading
Loading