mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2026-11-11 15:54:52 +09:00
exp_reports, exp_roles and exp_groups wrapped the json they send to Lua in a fixed [=[ ]=] long bracket. A report reason, role name or group name containing ]=] closed it early and the rest ran as Lua through /sc. Any player can make a report, so any player could run code on the server. Use the next level the json does not contain, like the other plugins do. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import type { InstancePluginContext } from "@clusterio/host";
|
|
import * as lib from "@clusterio/lib";
|
|
import * as messages from "./messages.js";
|
|
|
|
/** Sent by the lua side when roles are changed in game. */
|
|
export type IpcAssignmentUpdate = {
|
|
name: string,
|
|
assign: number[] | undefined,
|
|
unassign: number[] | undefined,
|
|
};
|
|
|
|
export default async function (context: InstancePluginContext) {
|
|
const { instance, logger, plugin } = context;
|
|
const syncMode = () => instance.config.get("exp_roles.sync_mode");
|
|
|
|
async function luaSend(receiver: string, json: any) {
|
|
const text = JSON.stringify(json);
|
|
// Role names and tags can contain anything, so pick a long bracket level the json does not close
|
|
let level = "=";
|
|
while (text.includes(`]${level}]`)) {
|
|
level += "=";
|
|
}
|
|
await instance.sendRcon(
|
|
`/sc exp_roles.${receiver}(helpers.json_to_table[${level}[${text}]${level}])`, true
|
|
);
|
|
}
|
|
|
|
instance.handle(messages.RoleUpdatedEvent, async event => {
|
|
if (syncMode() !== "disabled") {
|
|
await luaSend("receive_role_updates", event.updates.map(role => role.toJSON()));
|
|
}
|
|
});
|
|
|
|
instance.handle(messages.AssignmentUpdatedEvent, async event => {
|
|
if (syncMode() !== "disabled") {
|
|
await luaSend("receive_assignment_updates", event.updates.map(a => a.toJSON()));
|
|
}
|
|
});
|
|
|
|
instance.server.handle("exp_roles:assignment_update", async (event: IpcAssignmentUpdate) => {
|
|
if (syncMode() !== "bidirectional") {
|
|
return;
|
|
}
|
|
|
|
const assign = event.assign ?? [];
|
|
try {
|
|
await instance.sendTo("controller", new messages.AssignmentUpdateRequest(
|
|
event.name, assign, event.unassign ?? [],
|
|
));
|
|
} catch (err: any) {
|
|
// The roles were already applied in game, so they have to be taken
|
|
// back off again now that the controller has refused them
|
|
logger.warn(`Role change for ${event.name} was rejected: ${err.message}`);
|
|
if (assign.length) {
|
|
await luaSend("reject_assignment", { name: event.name, role_ids: assign });
|
|
}
|
|
}
|
|
});
|
|
|
|
instance.hooks.instanceConfigFieldChanged.attach(plugin.name, async (field, curr) => {
|
|
if (field === "exp_roles.sync_mode") {
|
|
await luaSend("set_emit_events", curr === "bidirectional");
|
|
}
|
|
});
|
|
|
|
instance.hooks.start.attach(plugin.name, async () => {
|
|
if (syncMode() === "disabled") {
|
|
return;
|
|
}
|
|
|
|
// Date.now() is used because the lua state is initialised from the full
|
|
// list below, so only updates made after this point are of interest
|
|
const subscribedAtMs = Date.now();
|
|
await instance.sendTo("controller", new lib.SubscriptionRequest(
|
|
`exp_roles:${messages.RoleUpdatedEvent.name}`, true, subscribedAtMs
|
|
));
|
|
await instance.sendTo("controller", new lib.SubscriptionRequest(
|
|
`exp_roles:${messages.AssignmentUpdatedEvent.name}`, true, subscribedAtMs
|
|
));
|
|
|
|
const [roles, assignments] = await Promise.all([
|
|
instance.sendTo("controller", new messages.RoleListRequest()),
|
|
instance.sendTo("controller", new messages.AssignmentListRequest()),
|
|
]);
|
|
|
|
if (!roles.some(role => role.isDefault)) {
|
|
logger.warn("No default role is set on the controller, players will hold no roles in game");
|
|
}
|
|
|
|
await luaSend("initialise", {
|
|
...messages.encodeRolesForLua(roles),
|
|
assignments: assignments.map(assignment => assignment.toJSON()),
|
|
});
|
|
await luaSend("set_emit_events", syncMode() === "bidirectional");
|
|
});
|
|
}
|