Skip to content
Draft
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
110 changes: 110 additions & 0 deletions modules/reboot-warnings/module.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"name": "test-reboot-warnings",
"description": "Broadcasts fixed countdown warning messages before scheduled server reboots. Each warning has its own cronjob so admins can schedule exact 60/30/15/5/1 minute notices without needing game console commands.",
"version": "latest",
"supportedGames": ["all"],
"config": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"warning60": {
"$ref": "#/definitions/warning",
"default": {
"enabled": true,
"message": "Server reboot in 60 minutes. Please plan accordingly."
}
},
"warning30": {
"$ref": "#/definitions/warning",
"default": {
"enabled": true,
"message": "Server reboot in 30 minutes."
}
},
"warning15": {
"$ref": "#/definitions/warning",
"default": {
"enabled": true,
"message": "Server reboot in 15 minutes. Please get somewhere safe."
}
},
"warning5": {
"$ref": "#/definitions/warning",
"default": {
"enabled": true,
"message": "Server reboot in 5 minutes. Log out soon to avoid losing progress."
}
},
"warning1": {
"$ref": "#/definitions/warning",
"default": {
"enabled": true,
"message": "Server reboot in 1 minute. Log out now."
}
},
"skipWhenNoPlayersOnline": {
"type": "boolean",
"default": true,
"description": "When enabled, warning cronjobs skip without sending if no players are online."
}
},
"definitions": {
"warning": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Whether this warning cronjob should send a message when triggered."
},
"message": {
"type": "string",
"minLength": 0,
"maxLength": 500,
"description": "Message to broadcast. Supports {minutesRemaining}, {serverName}, and {playerCount}. Leave blank to skip this warning."
}
},
"required": ["enabled", "message"],
"additionalProperties": false
}
},
"required": [],
"additionalProperties": false
},
"uiSchema": {},
"permissions": [],
"commands": {},
"hooks": {},
"cronJobs": {
"warning60": {
"temporalValue": "0 3 * * *",
"description": "Broadcasts the 60-minute reboot warning. Edit this cron schedule to match your server reboot time.",
"function": "src/cronjobs/warning60/index.js"
},
"warning30": {
"temporalValue": "30 3 * * *",
"description": "Broadcasts the 30-minute reboot warning. Edit this cron schedule to match your server reboot time.",
"function": "src/cronjobs/warning30/index.js"
},
"warning15": {
"temporalValue": "45 3 * * *",
"description": "Broadcasts the 15-minute reboot warning. Edit this cron schedule to match your server reboot time.",
"function": "src/cronjobs/warning15/index.js"
},
"warning5": {
"temporalValue": "55 3 * * *",
"description": "Broadcasts the 5-minute reboot warning. Edit this cron schedule to match your server reboot time.",
"function": "src/cronjobs/warning5/index.js"
},
"warning1": {
"temporalValue": "59 3 * * *",
"description": "Broadcasts the 1-minute reboot warning. Edit this cron schedule to match your server reboot time.",
"function": "src/cronjobs/warning1/index.js"
}
},
"functions": {
"reboot-warnings-helpers": {
"function": "src/functions/reboot-warnings-helpers.js"
}
}
}
3 changes: 3 additions & 0 deletions modules/reboot-warnings/src/cronjobs/warning1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sendRebootWarning } from './reboot-warnings-helpers.js';

await sendRebootWarning('warning1');
3 changes: 3 additions & 0 deletions modules/reboot-warnings/src/cronjobs/warning15/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sendRebootWarning } from './reboot-warnings-helpers.js';

await sendRebootWarning('warning15');
3 changes: 3 additions & 0 deletions modules/reboot-warnings/src/cronjobs/warning30/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sendRebootWarning } from './reboot-warnings-helpers.js';

await sendRebootWarning('warning30');
3 changes: 3 additions & 0 deletions modules/reboot-warnings/src/cronjobs/warning5/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sendRebootWarning } from './reboot-warnings-helpers.js';

await sendRebootWarning('warning5');
3 changes: 3 additions & 0 deletions modules/reboot-warnings/src/cronjobs/warning60/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { sendRebootWarning } from './reboot-warnings-helpers.js';

await sendRebootWarning('warning60');
118 changes: 118 additions & 0 deletions modules/reboot-warnings/src/functions/reboot-warnings-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { data, takaro } from '@takaro/helpers';

const DEFAULT_WARNINGS = {
warning60: {
minutesRemaining: 60,
message: 'Server reboot in 60 minutes. Please plan accordingly.',
},
warning30: {
minutesRemaining: 30,
message: 'Server reboot in 30 minutes.',
},
warning15: {
minutesRemaining: 15,
message: 'Server reboot in 15 minutes. Please get somewhere safe.',
},
warning5: {
minutesRemaining: 5,
message: 'Server reboot in 5 minutes. Log out soon to avoid losing progress.',
},
warning1: {
minutesRemaining: 1,
message: 'Server reboot in 1 minute. Log out now.',
},
};

const KNOWN_PLACEHOLDERS = new Set(['minutesRemaining', 'serverName', 'playerCount']);

function getWarningConfig(config, warningKey) {
const defaults = DEFAULT_WARNINGS[warningKey];
const userWarning = config?.[warningKey] ?? {};
return {
enabled: userWarning.enabled ?? true,
message: userWarning.message ?? defaults.message,
minutesRemaining: defaults.minutesRemaining,
};
}

async function getOnlinePlayerCount(gameServerId) {
const onlineRes = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
filters: {
gameServerId: [gameServerId],
online: [true],
},
limit: 1,
});
return onlineRes.data.meta.total;
}

async function getServerName(gameServerId) {
try {
const serverRes = await takaro.gameserver.gameServerControllerGetOne(gameServerId);
return serverRes.data.data?.name ?? '';
} catch (err) {
console.error(`reboot-warnings: failed to fetch server name, leaving {serverName} unchanged. Error: ${err}`);
return '';
}
}

function warnUnknownPlaceholders(text) {
const remaining = text.match(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g);
if (!remaining || remaining.length === 0) return;

const unknown = [...new Set(remaining.map((token) => token.slice(1, -1)).filter((name) => !KNOWN_PLACEHOLDERS.has(name)))];
if (unknown.length > 0) {
console.warn(`reboot-warnings: unrecognised placeholders in message text: ${unknown.map((name) => `{${name}}`).join(', ')}`);
}
}

async function renderMessage(rawMessage, { minutesRemaining, playerCount, gameServerId }) {
let message = String(rawMessage ?? '');
message = message.replace(/\{minutesRemaining\}/g, String(minutesRemaining));
message = message.replace(/\{playerCount\}/g, String(playerCount));

if (message.includes('{serverName}')) {
const serverName = await getServerName(gameServerId);
if (serverName) {
message = message.replace(/\{serverName\}/g, serverName);
}
}

warnUnknownPlaceholders(message);
return message;
}

export async function sendRebootWarning(warningKey) {
const { gameServerId, module: mod } = data;
const config = mod.userConfig ?? {};
const warning = getWarningConfig(config, warningKey);

if (!warning.enabled) {
console.log(`reboot-warnings: ${warningKey} disabled, skipping`);
return;
}

if (!warning.message || !warning.message.trim()) {
console.log(`reboot-warnings: ${warningKey} message is blank, skipping`);
return;
}

const playerCount = await getOnlinePlayerCount(gameServerId);
if ((config.skipWhenNoPlayersOnline ?? true) && playerCount === 0) {
console.log(`reboot-warnings: no players online, skipping ${warningKey}`);
return;
}

const message = await renderMessage(warning.message, {
minutesRemaining: warning.minutesRemaining,
playerCount,
gameServerId,
});

console.log(`reboot-warnings: sending ${warningKey} (${warning.minutesRemaining} minutes remaining): ${message}`);
await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
message,
opts: {},
});
console.log(`reboot-warnings: ${warningKey} broadcast complete`);
}
Loading
Loading