Add player moderation workflows and game integration
CI / checks (push) Successful in 7m27s

This commit is contained in:
2026-10-15 21:33:50 -04:00
parent c91d49c298
commit 8b372fa2c1
50 changed files with 4744 additions and 354 deletions
@@ -5,6 +5,7 @@ import { db } from '../db/index.js';
import type { NativeInstance } from '../backends/native.js';
import type { LaunchRecipePlan } from '../fleet/launch-recipes.js';
import type { PlacementOptions } from '../hosts/capacity.js';
import type { ModerationConfiguration } from '../moderation/configuration.js';
export interface NativeLaunchInput {
game: 't5' | 't6';
@@ -14,6 +15,7 @@ export interface NativeLaunchInput {
serverKey: string;
rconPassword: string;
bankToken: string;
moderation?: ModerationConfiguration;
rebuild: boolean;
safetyOverride?: boolean;
unhealthyRecovery?: boolean;
+11 -5
View File
@@ -12,6 +12,7 @@ import {
import { sha256Buffer } from '../fleet/files.js';
import type { NativeLaunchInput } from './launch-preparation.js';
import { choosePlacement, type PlacementOptions } from '../hosts/capacity.js';
import { isPrivateConfigPath } from '../fleet/private-config.js';
parentPort!.on(
'message',
@@ -79,12 +80,16 @@ parentPort!.on(
'electric_cherry_bank.cfg'
);
if (
recipe.files.some(
(file) =>
posix.join(relDestForKind(file.kind, recipe.game), file.path) === bankPath
recipe.files.some((file) =>
isPrivateConfigPath(
posix.join(relDestForKind(file.kind, recipe.game), file.path),
recipe.game
)
)
)
throw new Error('electric_cherry_bank.cfg is reserved for instance credentials.');
throw new Error(
'Private configuration files are reserved for instance credentials.'
);
const payload = {
...(input.activationId ? { stageId: input.activationId } : {}),
name: instance.server,
@@ -95,13 +100,14 @@ parentPort!.on(
bindAddress: instance.bind_address,
rconPassword: password,
bank: { token: input.bankToken, configPath: bankPath },
...(input.moderation ? { moderation: input.moderation } : {}),
env: {
PLUTAINER_SERVER_NAME: instance.server,
PLUTAINER_PORT: String(instance.port),
PLUTAINER_GAME: recipe.game === 't6' ? 't6zm' : 't5sp',
PLUTAINER_CONFIG_FILE: recipe.configFile,
PLUTAINER_MAP_ROTATE: 'false',
PLUTAINER_EXTRA_ARGS: `+exec electric_cherry_map.cfg +exec electric_cherry_bank.cfg +set tws_server_id ${instance.server} +map_rotate`,
PLUTAINER_EXTRA_ARGS: `+exec electric_cherry_map.cfg +exec electric_cherry_bank.cfg${input.moderation ? ' +exec electric_cherry_moderation.cfg' : ''} +set tws_server_id ${instance.server} +map_rotate`,
PLUTO_SERVER_KEY: input.serverKey
},
files: recipe.files.map((file) => ({
+30 -1
View File
@@ -14,6 +14,11 @@ import { setImmediate as yieldToController } from 'node:timers/promises';
import { choosePlacement, availableHostPort, type PlacementOptions } from '../hosts/capacity.js';
import { randomBytes, randomUUID } from 'node:crypto';
import { bankCredential } from '../auth/bank.js';
import { moderationCredential } from '../moderation/credentials.js';
import {
moderationConfiguration,
moderationConfigureCapability
} from '../moderation/configuration.js';
import { posix } from 'node:path';
import { db } from '../db/index.js';
import { unsettledCancelledCommand } from './cancellation.js';
@@ -1996,6 +2001,7 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
let prepared: PreparedNativeLaunch | undefined;
let preparedKey: string | null = null;
let preparedPassword: string | undefined;
let preparedModeration = false;
try {
if (job.action === 'create' || job.action === 'rebuild') {
if (
@@ -2027,6 +2033,11 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
(files.length ? runtimePasswordForServer(job.server) : undefined) ||
config.rcon.password ||
randomBytes(32).toString('hex');
const rebuilding = job.action === 'rebuild' && !migration;
const generation = instance.generation + (rebuilding ? 1 : 0);
preparedModeration =
!!config.publicUrl &&
!!host.inventory.capabilities?.includes(moderationConfigureCapability);
prepared = await prepareNativeLaunch({
game: prior.game,
mapSlug: prior.mapSlug,
@@ -2035,7 +2046,15 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
serverKey: preparedKey,
rconPassword: preparedPassword,
bankToken: bankCredential(instance.id),
rebuild: job.action === 'rebuild' && !migration,
...(preparedModeration
? {
moderation: moderationConfiguration(
moderationCredential(instance.id, generation),
generation
)
}
: {}),
rebuild: rebuilding,
safetyOverride: overrideSafety,
unhealthyRecovery:
!files.length &&
@@ -2124,6 +2143,16 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
return;
}
if (prepared) {
if (
preparedModeration &&
!liveHost.inventory.capabilities?.includes(moderationConfigureCapability)
) {
hold(
job,
'Host moderation support changed during preparation; preparing the current selection next pass.'
);
return;
}
// Compare the same runtime config bytes while rechecking current promoted files.
const expected =
migration && isRebalanceMigration(job.id)