diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 3f78c67b..5a9eb35b 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -306,7 +306,12 @@ export default class Instance extends lib.Link { return; } - let name = /^([^ ]+)/.exec(parsed.message)![1]; + // Guard against lines that classify as actions but lack a name + let nameMatch = /^([^ ]+)/.exec(parsed.message); + if (!nameMatch) { + return; + } + let name = nameMatch[1]; // Detect player leave and join if (parsed.action === "JOIN") { @@ -322,7 +327,9 @@ export default class Instance extends lib.Link { // Detect banned and admin state change, whitelist changes are not logged if (parsed.action === "BAN") { - const reason = /\. Reason: (.+)\.$/.exec(parsed.message)![1]; + // Ban lines without a ". Reason: X." tail default to no reason + const reasonMatch = /\. Reason: (.+)\.$/.exec(parsed.message); + const reason = reasonMatch ? reasonMatch[1] : ""; this._recordUserUpdate(parsed.action, name, reason !== "unspecified" ? reason : ""); } else if (["UNBANNED", "PROMOTE", "DEMOTE"].includes(parsed.action)) { this._recordUserUpdate(parsed.action as "UNBANNED" | "PROMOTE" | "DEMOTE", name); @@ -348,8 +355,11 @@ export default class Instance extends lib.Link { return; } - const name = /^([^ ]+)/.exec(parsed.message)![1]; - this._recordUserUpdate(parsed.action as "PROMOTE" | "DEMOTE", name); + const nameMatch = /^([^ ]+)/.exec(parsed.message); + if (!nameMatch) { + return; + } + this._recordUserUpdate(parsed.action as "PROMOTE" | "DEMOTE", nameMatch[1]); }); } diff --git a/test/host/Instance.js b/test/host/Instance.js index 73e11c1a..e42c02c2 100644 --- a/test/host/Instance.js +++ b/test/host/Instance.js @@ -328,4 +328,37 @@ describe("class Instance", function() { } }); }); + + describe("_watchServerLogActions()", function() { + beforeEach(function() { + instance.config.set("factorio.sync_banlist", "bidirectional"); + instance._watchServerLogActions(); + }); + afterEach(function() { + clearInterval(instance._playerCheckInterval); + }); + function emitAction(action, message) { + instance.server.emit("output", { type: "action", action, message }, ""); + } + it("should record a normal JOIN action", function() { + emitAction("JOIN", "player joined the game"); + assert(instance.playersOnline.has("player"), "player was not added"); + }); + it("should parse the reason of a normal BAN action", function() { + emitAction("BAN", "player was banned by . Reason: griefing."); + assert.deepEqual( + connector.sentMessages[0], + new lib.MessageEvent( + 1, src, addr("allInstances"), "InstanceBanlistUpdateEvent", + new lib.InstanceBanlistUpdateEvent("player", true, "griefing"), + ), + ); + }); + it("should not throw on a BAN action without a reason tail", function() { + assert.doesNotThrow(() => emitAction("BAN", "evil")); + }); + it("should not throw on an action whose message starts with a space", function() { + assert.doesNotThrow(() => emitAction("JOIN", " ")); + }); + }); });