mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-12 07:54:52 +09:00
clusterio #1180 made controller.hosts a HostManager. The datastore with set and the update event now lives on controller.hosts.records. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
9.0 KiB
JavaScript
226 lines
9.0 KiB
JavaScript
import t from "tap";
|
|
import * as lib from "@clusterio/lib";
|
|
import { Controller, HostRecord, InstanceRecord } from "@clusterio/controller";
|
|
import entrypoint from "../dist/node/controller.js";
|
|
import { plugin as pluginDeclaration } from "../dist/node/index.js";
|
|
import * as messages from "../dist/node/messages.js";
|
|
|
|
// The plugin's config fields must be defined before an InstanceConfig can set them
|
|
lib.addPluginConfigFields([pluginDeclaration]);
|
|
for (const Message of pluginDeclaration.messages) {
|
|
lib.Link.register(Message);
|
|
}
|
|
|
|
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
|
|
|
/** Run the plugin on a real controller, which is side effect free while not started. */
|
|
async function startPlugin(t2, setup = () => {}) {
|
|
const controllerConfig = new lib.ControllerConfig("controller", {
|
|
"controller.database_directory": t2.testdir(),
|
|
});
|
|
const controller = new Controller(logger, [], controllerConfig);
|
|
setup(controller);
|
|
|
|
const broadcasts = [];
|
|
controller.subscriptions.broadcast = event => {
|
|
if (event instanceof messages.ServerUpdatesEvent) {
|
|
broadcasts.push(event);
|
|
}
|
|
};
|
|
|
|
const handlers = new Map();
|
|
controller.handle = (Class, handler) => handlers.set(Class, handler);
|
|
controller.subscriptions.handle = (Class, handler) => handlers.set(Class, handler);
|
|
|
|
await entrypoint({ controller, logger, plugin: pluginDeclaration, metrics: undefined });
|
|
const list = async () => handlers.get(messages.ServerListRequest)(new messages.ServerListRequest());
|
|
return {
|
|
controller,
|
|
broadcasts,
|
|
list,
|
|
/** The record of one instance */
|
|
get: async id => (await list()).find(server => server.id === id),
|
|
subscribe: since => handlers.get(messages.ServerUpdatesEvent)(
|
|
new lib.SubscriptionRequest("exp_server_list:ServerUpdatesEvent", "subscribe", since),
|
|
),
|
|
};
|
|
}
|
|
|
|
function addHost(controller, id, publicAddress = "") {
|
|
controller.hosts.records.set(new HostRecord(id, `host-${id}`, "2.0.0", new Map(), true, "", publicAddress));
|
|
}
|
|
|
|
function addInstance(controller, id, { hostId = 1, status = "running", gamePort, factorioVersion, fields = {} } = {}) {
|
|
const config = new lib.InstanceConfig("controller", {
|
|
"instance.id": id,
|
|
"instance.name": `instance-${id}`,
|
|
"instance.assigned_host": hostId,
|
|
...fields,
|
|
});
|
|
const record = new InstanceRecord(config, status, gamePort, factorioVersion);
|
|
controller.instances.records.set(record);
|
|
controller.instances.addInstanceHooks(record);
|
|
return record;
|
|
}
|
|
|
|
const join = (controller, name, instanceId) => controller.users.getOrCreateUser(name).notifyJoin(instanceId);
|
|
|
|
t.test("entrypoint attaches to the controller hooks", async t2 => {
|
|
const { controller } = await startPlugin(t2);
|
|
const hooks = [
|
|
"instanceStatusChanged",
|
|
"instanceConfigFieldChanged",
|
|
"controllerConfigFieldChanged",
|
|
"playerEvent",
|
|
"modPacksUpdated",
|
|
];
|
|
for (const hook of hooks) {
|
|
t2.ok([...controller.hooks[hook].attached].includes("exp_server_list"), `attached to ${hook}`);
|
|
}
|
|
});
|
|
|
|
t.test("builds a record for every instance on start", async t2 => {
|
|
const { get } = await startPlugin(t2, controller => {
|
|
addHost(controller, 1, "example.com");
|
|
addInstance(controller, 10, {
|
|
gamePort: 34197,
|
|
factorioVersion: "2.1.14",
|
|
fields: {
|
|
"exp_server_list.description": "Weekly reset",
|
|
"exp_server_list.hidden": true,
|
|
"exp_server_list.always_on_top": true,
|
|
"factorio.settings": { description: "From the settings", game_password: "secret" },
|
|
},
|
|
});
|
|
addInstance(controller, 11, {
|
|
status: "stopped",
|
|
factorioVersion: "latest",
|
|
fields: {
|
|
"exp_server_list.display_name": "S11",
|
|
"factorio.settings": { description: "From the settings", game_password: "" },
|
|
},
|
|
});
|
|
});
|
|
|
|
const server = await get(10);
|
|
t2.equal(server.name, "instance-10");
|
|
t2.equal(server.displayName, "instance-10", "display name falls back to the name");
|
|
t2.equal(server.description, "Weekly reset", "own description wins over the settings");
|
|
t2.equal(server.hidden, true);
|
|
t2.equal(server.alwaysOnTop, true);
|
|
t2.equal(server.hasPassword, true);
|
|
t2.notMatch(JSON.stringify(server.toJSON()), "secret", "the password is not sent");
|
|
t2.equal(server.factorioVersion, "2.1.14");
|
|
t2.equal(server.address, "example.com:34197", "public address of the host and the game port");
|
|
t2.equal(server.status, "running");
|
|
|
|
const stopped = await get(11);
|
|
t2.equal(stopped.displayName, "S11");
|
|
t2.equal(stopped.description, "From the settings", "description falls back to the settings");
|
|
t2.equal(stopped.hasPassword, false, "an empty password is no password");
|
|
t2.equal(stopped.factorioVersion, "", "the latest target is not a version");
|
|
t2.equal(stopped.address, "example.com", "host address alone without a game port");
|
|
});
|
|
|
|
t.test("uses the mod pack of the instance or the default", async t2 => {
|
|
const { get, controller } = await startPlugin(t2, c => {
|
|
addHost(c, 1);
|
|
c.modPacks.setMany(lib.ModPack.defaultModPacks.map(pack => lib.ModPack.fromJSON(pack.toJSON())));
|
|
const packs = [...c.modPacks.values()];
|
|
c.config.set("controller.default_mod_pack_id", packs[0].id);
|
|
addInstance(c, 10);
|
|
addInstance(c, 11, { fields: { "factorio.mod_pack_id": packs[1].id } });
|
|
});
|
|
const packs = [...controller.modPacks.values()];
|
|
t2.equal((await get(10)).modPackName, packs[0].name, "the default mod pack");
|
|
t2.equal((await get(11)).modPackName, packs[1].name, "the instance mod pack");
|
|
t2.equal((await get(10)).address, "", "no address while the host has none");
|
|
});
|
|
|
|
t.test("uses the controller welcome when the instance has none", async t2 => {
|
|
const { get, controller, broadcasts } = await startPlugin(t2, c => {
|
|
addHost(c, 1);
|
|
addInstance(c, 10);
|
|
addInstance(c, 11, { fields: { "exp_server_list.welcome": "Own welcome" } });
|
|
});
|
|
t2.equal((await get(10)).welcome, "", "empty when neither is set");
|
|
|
|
controller.config.set("exp_server_list.welcome", "Cluster welcome");
|
|
await controller.hooks.controllerConfigFieldChanged.invoke("exp_server_list.welcome", "");
|
|
t2.equal((await get(10)).welcome, "Cluster welcome");
|
|
t2.equal((await get(11)).welcome, "Own welcome", "the instance welcome wins");
|
|
t2.strictSame(broadcasts.at(-1).updates.map(server => server.id), [10], "only changed records are sent");
|
|
});
|
|
|
|
t.test("counts players only while running", async t2 => {
|
|
const { get, controller, broadcasts } = await startPlugin(t2, c => {
|
|
addHost(c, 1);
|
|
addInstance(c, 10);
|
|
});
|
|
const instance = controller.instances.get(10);
|
|
|
|
join(controller, "alice", 10);
|
|
join(controller, "bob", 10);
|
|
await controller.hooks.playerEvent.invoke(instance, { type: "join", name: "bob" });
|
|
t2.equal((await get(10)).playerCount, 2);
|
|
t2.equal(broadcasts.at(-1).updates[0].playerCount, 2, "the change is broadcast");
|
|
|
|
instance.status = "stopped";
|
|
await controller.hooks.instanceStatusChanged.invoke(instance, "running");
|
|
t2.equal((await get(10)).playerCount, 0, "players left online by a crash are not counted");
|
|
});
|
|
|
|
t.test("follows config, host and status changes", async t2 => {
|
|
const { get, controller, broadcasts } = await startPlugin(t2, c => {
|
|
addHost(c, 1);
|
|
addHost(c, 2);
|
|
addInstance(c, 10, { gamePort: 34197 });
|
|
addInstance(c, 11, { gamePort: 34198 });
|
|
addInstance(c, 12, { hostId: 2 });
|
|
});
|
|
const instance = controller.instances.getMutable(10);
|
|
|
|
instance.config.set("instance.name", "Weekly");
|
|
t2.equal((await get(10)).name, "Weekly", "renames are picked up");
|
|
|
|
instance.config.set("exp_server_list.welcome", "Hello");
|
|
t2.equal((await get(10)).welcome, "Hello", "plugin fields are picked up");
|
|
|
|
instance.config.set("factorio.settings", { game_password: "secret" });
|
|
t2.equal((await get(10)).hasPassword, true, "server settings are picked up");
|
|
|
|
addHost(controller, 1, "example.com");
|
|
t2.equal((await get(10)).address, "example.com:34197", "the public address once the host sends it");
|
|
t2.strictSame(
|
|
broadcasts.at(-1).updates.map(server => server.id), [10, 11], "the instances on that host in one update",
|
|
);
|
|
|
|
const count = broadcasts.length;
|
|
addHost(controller, 1, "example.com");
|
|
t2.equal(broadcasts.length, count, "nothing is broadcast when nothing changed");
|
|
|
|
// What deleteInstance does once the host has deleted it
|
|
controller.instances.records.delete(instance);
|
|
await controller.hooks.instanceStatusChanged.invoke(instance, "running");
|
|
t2.equal(await get(10), undefined, "deleted instances are removed");
|
|
t2.equal(broadcasts.at(-1).updates[0].isDeleted, true, "and broadcast as deleted");
|
|
});
|
|
|
|
t.test("subscriptions get the servers updated since the last request", async t2 => {
|
|
const { subscribe, list } = await startPlugin(t2, controller => {
|
|
addHost(controller, 1);
|
|
addInstance(controller, 10);
|
|
});
|
|
t2.equal((await subscribe(0)).updates.length, 1);
|
|
t2.equal(await subscribe(Date.now() + 1000), null);
|
|
t2.equal((await list()).length, 1);
|
|
});
|
|
|
|
t.test("ServerRecord round trips through json", async t2 => {
|
|
const record = new messages.ServerRecord(
|
|
1, "a", "b", "c", "d", "e", true, true, true, "Base", "2.1.14", 3, "running", "localhost:1", 5, true,
|
|
);
|
|
t2.same(messages.ServerRecord.fromJSON(record.toJSON()), record);
|
|
t2.ok(record.sameAs(messages.ServerRecord.fromJSON(record.toJSON())));
|
|
});
|