mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 23:54:52 +09:00
232 lines
9.5 KiB
JavaScript
232 lines
9.5 KiB
JavaScript
import t from "tap";
|
|
import * as lib from "@clusterio/lib";
|
|
import { Instance } from "@clusterio/host";
|
|
import entrypoint from "../dist/node/instance.js";
|
|
import { plugin as pluginDeclaration } from "../dist/node/index.js";
|
|
import * as messages from "../dist/node/messages.js";
|
|
import { features } from "../dist/node/features.js";
|
|
|
|
// The instance validates message classes against the link registry
|
|
for (const Message of pluginDeclaration.messages) {
|
|
lib.Link.register(Message);
|
|
}
|
|
|
|
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
|
|
|
|
class TestConnector extends lib.BaseConnector {
|
|
constructor() {
|
|
super(lib.Address.fromShorthand({ instanceId: 1 }), lib.Address.fromShorthand({ hostId: 1 }));
|
|
this.valid = true;
|
|
this.connected = true;
|
|
this.hasSession = true;
|
|
}
|
|
|
|
send() {}
|
|
}
|
|
|
|
/** Run the plugin on a real instance with spies on what leaves it, returns the event handler it registered. */
|
|
async function startPlugin(
|
|
t2, features = [new messages.FeatureRecord("death_markers", false, { show_map_markers: false })], players = [],
|
|
) {
|
|
const instanceConfig = new lib.InstanceConfig("host");
|
|
instanceConfig.set("instance.id", 1);
|
|
instanceConfig.set("instance.name", "test");
|
|
|
|
const instance = new Instance(
|
|
{ assignGamePort: () => 1 }, new TestConnector(), t2.testdir(), "factorioDir", instanceConfig
|
|
);
|
|
|
|
const state = { sent: [], rcons: [] };
|
|
const ipcHandlers = new Map();
|
|
instance.server = {
|
|
handle: (channel, handler) => ipcHandlers.set(channel, handler),
|
|
sendRcon: async command => {
|
|
state.rcons.push(command);
|
|
return "";
|
|
},
|
|
};
|
|
instance.sendTo = async (dst, request) => {
|
|
state.sent.push(request);
|
|
if (request instanceof messages.PlayerGetRequest) {
|
|
const found = players.find(record => record.id === request.playerName);
|
|
return found ?? new messages.PlayerRecord(request.playerName);
|
|
}
|
|
return request instanceof messages.FeatureListRequest ? features : undefined;
|
|
};
|
|
|
|
const handlers = new Map();
|
|
instance.handle = (Event, handler) => handlers.set(Event, handler);
|
|
|
|
await entrypoint({ plugin: { name: "exp_scenario" }, instance, host: {}, logger });
|
|
const hooks = instance.hooks;
|
|
return {
|
|
state,
|
|
start: () => hooks.start.invoke(),
|
|
exit: () => hooks.exit.invoke(),
|
|
connectionEvent: event => hooks.controllerConnectionEvent.invoke(event),
|
|
featureUpdated: handlers.get(messages.FeatureUpdatedEvent),
|
|
ipc: (channel, event) => ipcHandlers.get(channel)(event),
|
|
playerEvent: event => hooks.playerEvent.invoke(event),
|
|
setStatus: status => { instance._status = status; },
|
|
};
|
|
}
|
|
|
|
/** The payload of a recorded rcon command. */
|
|
function decodeRcon(command) {
|
|
const match = command.match(/^\/sc exp_scenario\.features\.receive_update\(helpers\.json_to_table\[(=+)\[(.*)\]\1\]\)$/s);
|
|
return JSON.parse(match[2]);
|
|
}
|
|
|
|
/** The player data and code of a recorded rcon command made by luaWithPlayer. */
|
|
function decodePlayerRcon(command) {
|
|
const match = command.match(
|
|
/^\/sc local d = helpers\.json_to_table\[(=+)\[(.*)\]\1\] local p = game\.get_player\(d\.name\) if p then (.*) end$/s,
|
|
);
|
|
return { data: JSON.parse(match[2]), code: match[3] };
|
|
}
|
|
|
|
t.test("instance plugin", t2 => {
|
|
t2.test("start subscribes and sends every feature to lua", async t3 => {
|
|
const { start, state } = await startPlugin(t3);
|
|
await start();
|
|
|
|
t3.ok(state.sent[0] instanceof lib.SubscriptionRequest, "subscribes to updates first");
|
|
t3.ok(state.sent[1] instanceof messages.FeatureListRequest, "then lists the features");
|
|
const [update] = decodeRcon(state.rcons[0]);
|
|
const deathMarkers = features.find(feature => feature.name === "death_markers");
|
|
t3.equal(update.name, "death_markers");
|
|
t3.strictSame([update.values.enabled, update.values.show_map_markers], [false, false], "stored values are sent");
|
|
t3.equal(update.values.collect_corpses, true, "the rest are filled with their defaults");
|
|
t3.equal(Object.keys(update.values).length, deathMarkers.fields.length + 1, "every field and enabled");
|
|
});
|
|
|
|
t2.test("FeatureUpdatedEvent only sends while the game is up", async t3 => {
|
|
const { start, exit, featureUpdated, state } = await startPlugin(t3);
|
|
const event = new messages.FeatureUpdatedEvent([
|
|
new messages.FeatureRecord("afk_kick", true, { afk_minutes: 5 }),
|
|
new messages.FeatureRecord("removed", true, {}, 0, true),
|
|
]);
|
|
|
|
await featureUpdated(event);
|
|
t3.strictSame(state.rcons, [], "nothing is sent before start");
|
|
|
|
await start();
|
|
state.rcons.length = 0;
|
|
await featureUpdated(event);
|
|
const updates = decodeRcon(state.rcons[0]);
|
|
t3.strictSame(updates.map(update => update.name), ["afk_kick"], "deleted features are left out");
|
|
t3.strictSame([updates[0].values.enabled, updates[0].values.afk_minutes], [true, 5]);
|
|
|
|
await exit();
|
|
state.rcons.length = 0;
|
|
await featureUpdated(event);
|
|
t3.strictSame(state.rcons, [], "nothing is sent after exit");
|
|
});
|
|
|
|
t2.test("a new controller connection syncs again after the controller restarts", async t3 => {
|
|
const { start, connectionEvent, state } = await startPlugin(t3);
|
|
await connectionEvent("connect");
|
|
t3.strictSame(state.sent, [], "nothing happens before start");
|
|
|
|
await start();
|
|
state.sent.length = 0;
|
|
state.rcons.length = 0;
|
|
await connectionEvent("resume");
|
|
t3.strictSame(state.sent, [], "a resumed session keeps its subscription");
|
|
|
|
await connectionEvent("connect");
|
|
t3.ok(state.sent[0] instanceof lib.SubscriptionRequest, "subscribes again");
|
|
t3.equal(state.rcons.length, 1, "and sends every feature");
|
|
});
|
|
|
|
t2.test("feature updates pick a long bracket the values do not close", async t3 => {
|
|
const { start, featureUpdated, state } = await startPlugin(t3, []);
|
|
await start();
|
|
const name = "a]=]b]==]c";
|
|
await featureUpdated(new messages.FeatureUpdatedEvent([
|
|
new messages.FeatureRecord("station_auto_name", true, { station_name: name }),
|
|
]));
|
|
t3.match(state.rcons[0], "json_to_table[===[", "uses a level not found in the json");
|
|
t3.strictSame(decodeRcon(state.rcons[0])[0].values.station_name, name);
|
|
});
|
|
|
|
t2.end();
|
|
});
|
|
|
|
t.test("player settings", t2 => {
|
|
const alice = new messages.PlayerRecord("Alice", "Alice has arrived ]=] ]==]");
|
|
|
|
t2.test("a joining player's message is printed to everyone", async t3 => {
|
|
const { start, playerEvent, setStatus, state } = await startPlugin(t3, [], [alice]);
|
|
await start();
|
|
setStatus("running");
|
|
state.rcons.length = 0;
|
|
|
|
await playerEvent({ type: "join", name: "Alice" });
|
|
t3.equal(state.rcons.length, 1);
|
|
const { data, code } = decodePlayerRcon(state.rcons[0]);
|
|
t3.strictSame(data, { message: alice.joinMessage, name: "Alice" }, "the message survives the long bracket");
|
|
t3.equal(code, "game.print(d.message, { color = p.color })");
|
|
|
|
await playerEvent({ type: "join", name: "Bob" });
|
|
await playerEvent({ type: "leave", name: "Alice" });
|
|
t3.equal(state.rcons.length, 1, "players without a message and leaves print nothing");
|
|
});
|
|
|
|
t2.test("nothing is printed while the instance is not running or greetings are disabled", async t3 => {
|
|
const { start, playerEvent, setStatus, featureUpdated, state } = await startPlugin(t3, [], [alice]);
|
|
await start();
|
|
state.rcons.length = 0;
|
|
await playerEvent({ type: "join", name: "Alice" });
|
|
t3.strictSame(state.rcons, [], "not running");
|
|
|
|
setStatus("running");
|
|
await featureUpdated(new messages.FeatureUpdatedEvent([new messages.FeatureRecord("greetings", false)]));
|
|
state.rcons.length = 0;
|
|
await playerEvent({ type: "join", name: "Alice" });
|
|
t3.strictSame(state.rcons, [], "greetings disabled");
|
|
});
|
|
|
|
t2.test("ipc from the set and remove commands is forwarded to the controller", async t3 => {
|
|
const { ipc, state } = await startPlugin(t3);
|
|
await ipc("exp_scenario:join_message_set", { player_name: "Alice", message: "Hi" });
|
|
await ipc("exp_scenario:join_message_set", { player_name: "Alice" });
|
|
const sent = state.sent.filter(request => request instanceof messages.JoinMessageSetRequest);
|
|
t3.strictSame(sent.map(request => [request.playerName, request.message]), [["Alice", "Hi"], ["Alice", null]]);
|
|
});
|
|
|
|
t2.test("a joining player's stored toolbar is sent to the lua side", async t3 => {
|
|
const bob = new messages.PlayerRecord("Bob", undefined, "eNqrVkrLz1eyUlBKSixSqgUAGQwD8g==");
|
|
const { start, playerEvent, setStatus, state } = await startPlugin(t3, [], [bob]);
|
|
await start();
|
|
setStatus("running");
|
|
state.rcons.length = 0;
|
|
|
|
await playerEvent({ type: "join", name: "Bob" });
|
|
t3.equal(state.rcons.length, 1, "only the toolbar, Bob has no join message");
|
|
const { data, code } = decodePlayerRcon(state.rcons[0]);
|
|
t3.strictSame(data, { toolbar: bob.toolbar, name: "Bob" });
|
|
t3.equal(code, "exp_scenario.player_settings.receive_toolbar(d.name, d.toolbar)");
|
|
});
|
|
|
|
t2.test("ipc with a toolbar is forwarded to the controller", async t3 => {
|
|
const { ipc, state } = await startPlugin(t3);
|
|
await ipc("exp_scenario:toolbar", { player_name: "Alice", toolbar: "abc" });
|
|
const [event] = state.sent.filter(request => request instanceof messages.PlayerToolbarEvent);
|
|
t3.strictSame([event.playerName, event.toolbar], ["Alice", "abc"]);
|
|
});
|
|
|
|
t2.test("ipc asking for the message prints it to the player", async t3 => {
|
|
const { ipc, setStatus, state } = await startPlugin(t3, [], [alice]);
|
|
setStatus("running");
|
|
await ipc("exp_scenario:join_message_get", { player_name: "Alice" });
|
|
await ipc("exp_scenario:join_message_get", { player_name: "Bob" });
|
|
const [found, missing] = state.rcons.map(decodePlayerRcon);
|
|
t3.equal(found.data.message, alice.joinMessage);
|
|
t3.equal(missing.data.message, undefined, "no message key, so the lua side prints message-none");
|
|
t3.match(found.code, "exp_greetings.message-none");
|
|
});
|
|
|
|
t2.end();
|
|
});
|