mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
The controller and instance plugins are init functions with closures. The controller batches refreshes with setMany and deleteMany, and the instance gates updates on its status instead of a started flag. short_name is now display_name. New always_on_top field sorts servers first in get_servers. The record carries has_password from the game password in factorio.settings, never the password itself, and the description falls back to the one in factorio.settings. A controller welcome is used when the instance has none, and the readme falls back to a default welcome from the locale. Lua gets is_synced for connect.lua. The readme passes an empty detail instead of nil to the join tooltip so the player count stays in __2__, restores the Password status, shows the welcome title as a centered label and shortens long descriptions with the full text as a tooltip. The Lua tests assert the lookups they index, which fixes the lint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import type { ControllerPluginContext, InstanceRecord } from "@clusterio/controller";
|
|
import * as lib from "@clusterio/lib";
|
|
import * as messages from "./messages.js";
|
|
|
|
/** Instance config fields which change the record */
|
|
const recordFields = new Set([
|
|
"instance.name",
|
|
"instance.assigned_host",
|
|
"factorio.mod_pack_id",
|
|
"factorio.settings",
|
|
"exp_server_list.display_name",
|
|
"exp_server_list.description",
|
|
"exp_server_list.welcome",
|
|
"exp_server_list.reset_time",
|
|
"exp_server_list.hidden",
|
|
"exp_server_list.always_on_top",
|
|
]);
|
|
|
|
/** Controller config fields which change every record */
|
|
const controllerFields = new Set([
|
|
"controller.default_mod_pack_id",
|
|
"exp_server_list.welcome",
|
|
]);
|
|
|
|
/**
|
|
* Keeps a record for every instance and broadcasts the ones which change.
|
|
*
|
|
* Records are rebuilt from controller state on start, nothing is saved.
|
|
*/
|
|
export default async function (context: ControllerPluginContext) {
|
|
const { controller, plugin } = context;
|
|
const servers = new lib.SubscribableDatastore<messages.ServerRecord>();
|
|
|
|
function modPackName(instance: InstanceRecord) {
|
|
const id = instance.config.get("factorio.mod_pack_id")
|
|
?? controller.config.get("controller.default_mod_pack_id");
|
|
return id !== null && id !== undefined ? controller.modPacks.get(id)?.name ?? "" : "";
|
|
}
|
|
|
|
function playerCount(instanceId: number) {
|
|
let count = 0;
|
|
for (const user of controller.users.records.values()) {
|
|
if (user.instances.has(instanceId)) {
|
|
count += 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/** Same as the address the web ui shows for the instance */
|
|
function address(instance: InstanceRecord) {
|
|
const hostId = instance.config.get("instance.assigned_host");
|
|
const publicAddress = hostId !== null ? controller.hosts.get(hostId)?.publicAddress : undefined;
|
|
if (!publicAddress) {
|
|
return "";
|
|
}
|
|
return instance.gamePort === undefined ? publicAddress : `${publicAddress}:${instance.gamePort}`;
|
|
}
|
|
|
|
function buildRecord(instance: InstanceRecord) {
|
|
const config = instance.config;
|
|
const settings = config.get("factorio.settings");
|
|
const settingString = (name: string) => (typeof settings[name] === "string" ? settings[name] : "");
|
|
return new messages.ServerRecord(
|
|
instance.id,
|
|
config.get("instance.name"),
|
|
config.get("exp_server_list.display_name") || config.get("instance.name"),
|
|
config.get("exp_server_list.description") || settingString("description"),
|
|
config.get("exp_server_list.welcome") || controller.config.get("exp_server_list.welcome"),
|
|
config.get("exp_server_list.reset_time"),
|
|
config.get("exp_server_list.hidden"),
|
|
config.get("exp_server_list.always_on_top"),
|
|
settingString("game_password") !== "",
|
|
modPackName(instance),
|
|
instance.factorioVersion === "latest" ? "" : instance.factorioVersion ?? "",
|
|
instance.status === "running" ? playerCount(instance.id) : 0,
|
|
instance.status,
|
|
address(instance),
|
|
);
|
|
}
|
|
|
|
/** Rebuild the records of the instances, and store the ones which changed in one update */
|
|
function refresh(instances: Iterable<InstanceRecord>) {
|
|
const changed: messages.ServerRecord[] = [];
|
|
const deleted: messages.ServerRecord[] = [];
|
|
for (const instance of instances) {
|
|
const existing = servers.get(instance.id);
|
|
if (instance.isDeleted) {
|
|
if (existing) {
|
|
deleted.push(existing);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const record = buildRecord(instance);
|
|
if (!existing || !existing.sameAs(record)) {
|
|
changed.push(record);
|
|
}
|
|
}
|
|
|
|
if (changed.length) {
|
|
servers.setMany(changed);
|
|
}
|
|
if (deleted.length) {
|
|
servers.deleteMany(deleted);
|
|
}
|
|
}
|
|
|
|
const refreshAll = () => refresh(controller.instances.values());
|
|
|
|
refreshAll();
|
|
servers.on("update", updates => {
|
|
controller.subscriptions.broadcast(new messages.ServerUpdatesEvent(updates));
|
|
});
|
|
|
|
// The public address of a host arrives after it connects
|
|
controller.hosts.on("update", hosts => {
|
|
const hostIds = new Set(hosts.map(host => host.id));
|
|
refresh([...controller.instances.values()].filter(instance => {
|
|
const hostId = instance.config.get("instance.assigned_host");
|
|
return hostId !== null && hostIds.has(hostId);
|
|
}));
|
|
});
|
|
|
|
controller.subscriptions.handle(messages.ServerUpdatesEvent, async request => {
|
|
const updates = [...servers.values()].filter(server => server.updatedAtMs > request.lastRequestTimeMs);
|
|
return updates.length ? new messages.ServerUpdatesEvent(updates) : null;
|
|
});
|
|
controller.handle(messages.ServerListRequest, async () => [...servers.values()]);
|
|
|
|
const hooks = controller.hooks;
|
|
hooks.instanceStatusChanged.attach(plugin.name, async instance => {
|
|
refresh([instance]);
|
|
});
|
|
hooks.instanceConfigFieldChanged.attach(plugin.name, async (instance, field) => {
|
|
if (recordFields.has(field)) {
|
|
refresh([instance]);
|
|
}
|
|
});
|
|
hooks.controllerConfigFieldChanged.attach(plugin.name, async field => {
|
|
if (controllerFields.has(field)) {
|
|
refreshAll();
|
|
}
|
|
});
|
|
hooks.playerEvent.attach(plugin.name, async (instance, event) => {
|
|
if (event.type === "join" || event.type === "leave") {
|
|
refresh([instance]);
|
|
}
|
|
});
|
|
hooks.modPacksUpdated.attach(plugin.name, async () => {
|
|
refreshAll();
|
|
});
|
|
}
|