mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-15 16:24:52 +09:00
Join messages were stored in exp_legacy's PlayerData, which nothing ever synced. The controller now keeps a players.json datastore of per-player settings keyed by player name, starting with the join message, and saves it on the save and shutdown hooks. Records with nothing set are removed. When a player joins, the instance plugin fetches their record and prints the message to everyone over rcon in the player's colour, so no lua is needed to show it. The in-game /set-join-message and /remove-join-message commands send ipc which the instance forwards to the controller; /set-join-message without a message asks the instance, which prints the stored message back to the player. Greetings is now a feature. Disabling it stops the greeting, the commands, and the printed join messages. The private greeting with the community name and Discord link is shown to every joining player, even those with a join message, since one is a welcome for the player and the other an announcement to everyone else. Two controller permissions cover the web interface: exp_scenario.join_message.set for your own message and .set_other for anyone's, checked against the requesting user the same way exp_discord_auth checks unlinking. The seed grants them to Supporter and Moderator alongside the in-game commands. The user page gets a Scenario section showing the message with an edit button for users who may change it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
4.4 KiB
TypeScript
116 lines
4.4 KiB
TypeScript
import type { InstancePluginContext } from "@clusterio/host";
|
|
import * as lib from "@clusterio/lib";
|
|
import * as messages from "./messages.js";
|
|
import { features, FeatureValue } from "./features.js";
|
|
|
|
/** Sent by the lua side when a player sets or removes their join message, see module/control/greetings.lua */
|
|
export type IpcJoinMessageSet = { player_name: string, message?: string };
|
|
/** Sent by the lua side when a player asks for their join message */
|
|
export type IpcJoinMessageGet = { player_name: string };
|
|
|
|
/** Json as a lua expression for a table, inside a long bracket the json does not contain */
|
|
export function luaJson(value: unknown) {
|
|
const json = JSON.stringify(value);
|
|
let level = "=";
|
|
while (json.includes(`]${level}]`)) {
|
|
level += "=";
|
|
}
|
|
return `helpers.json_to_table[${level}[${json}]${level}]`;
|
|
}
|
|
|
|
/** Sends feature config and player settings to the lua side, see module/features.lua. */
|
|
export default async function (context: InstancePluginContext) {
|
|
const { instance, logger, plugin } = context;
|
|
/** Rcon is up and the lua side can take updates */
|
|
let started = false;
|
|
/** Enabled state of features which need it on this side, by name */
|
|
const featureEnabled = new Map<string, boolean>();
|
|
|
|
/** Subscribe, then send every feature */
|
|
async function syncFeatures() {
|
|
await instance.sendTo("controller", new lib.SubscriptionRequest(
|
|
`exp_scenario:${messages.FeatureUpdatedEvent.name}`, "subscribe", Date.now(),
|
|
));
|
|
const records = await instance.sendTo("controller", new messages.FeatureListRequest());
|
|
await luaSendFeatures(records);
|
|
}
|
|
|
|
/** Every value of each feature is sent, so the lua side sets them all */
|
|
async function luaSendFeatures(records: messages.FeatureRecord[]) {
|
|
if (!records.length) {
|
|
return;
|
|
}
|
|
|
|
const updates = records.map(record => {
|
|
featureEnabled.set(record.id, record.enabled);
|
|
const values: Record<string, FeatureValue> = { enabled: record.enabled };
|
|
for (const field of features.find(feature => feature.name === record.id)?.fields ?? []) {
|
|
values[field.name] = field.name in record.values ? record.values[field.name] : field.default;
|
|
}
|
|
return { name: record.id, values };
|
|
});
|
|
await instance.sendRcon(`/sc exp_scenario.features.receive_update(${luaJson(updates)})`, true);
|
|
}
|
|
|
|
/** Run lua with the player as `p` if they are still in the game, the data is available as `d` */
|
|
async function luaWithPlayer(name: string, data: Record<string, unknown>, code: string) {
|
|
if (instance.status !== "running") {
|
|
return;
|
|
}
|
|
await instance.sendRcon(
|
|
`/sc local d = ${luaJson({ ...data, name })} local p = game.get_player(d.name) if p then ${code} end`, true,
|
|
);
|
|
}
|
|
|
|
async function onPlayerJoin(name: string) {
|
|
const record = await instance.sendTo("controller", new messages.PlayerGetRequest(name));
|
|
if (record.joinMessage && featureEnabled.get("greetings") !== false) {
|
|
await luaWithPlayer(name, { message: record.joinMessage }, "game.print(d.message, { color = p.color })");
|
|
}
|
|
}
|
|
|
|
instance.handle(messages.FeatureUpdatedEvent, async event => {
|
|
if (started) {
|
|
await luaSendFeatures(event.updates.filter(feature => !feature.isDeleted));
|
|
}
|
|
});
|
|
|
|
instance.server.handle("exp_scenario:join_message_set", async (event: IpcJoinMessageSet) => {
|
|
const request = new messages.JoinMessageSetRequest(event.player_name, event.message ?? null);
|
|
await instance.sendTo("controller", request);
|
|
});
|
|
|
|
instance.server.handle("exp_scenario:join_message_get", async (event: IpcJoinMessageGet) => {
|
|
const record = await instance.sendTo("controller", new messages.PlayerGetRequest(event.player_name));
|
|
await luaWithPlayer(
|
|
event.player_name,
|
|
record.joinMessage ? { message: record.joinMessage } : {},
|
|
"p.print(d.message and { \"exp_greetings.message-get\", d.message } or { \"exp_greetings.message-none\" })",
|
|
);
|
|
});
|
|
|
|
instance.hooks.playerEvent.attach(plugin.name, async event => {
|
|
if (event.type === "join") {
|
|
await onPlayerJoin(event.name).catch(err => logger.error(
|
|
`Error showing the join message of ${event.name}:\n${err.stack ?? err.message}`,
|
|
));
|
|
}
|
|
});
|
|
|
|
instance.hooks.start.attach(plugin.name, async () => {
|
|
started = true;
|
|
await syncFeatures();
|
|
});
|
|
|
|
instance.hooks.exit.attach(plugin.name, async () => {
|
|
started = false;
|
|
});
|
|
|
|
// A new session means the controller restarted and forgot the subscription
|
|
instance.hooks.controllerConnectionEvent.attach(plugin.name, async event => {
|
|
if (event === "connect" && started) {
|
|
await syncFeatures();
|
|
}
|
|
});
|
|
}
|