Files
factorio-expcluster/exp_scenario/instance.ts
T
bbassieandClaude Fable 5.1 2959d7476b Send every value, register protected entities, fix the pattern offset
- The instance sends every value of a feature, defaults included, so
  update_config is one loop which sets each field and compares. The
  stored table is the last set of values, and _apply is gone.
- values_equal compares table sizes before the contents.
- Protected entities are registered with register_on_object_destroyed
  and forgotten in on_object_destroyed, which covers every way an
  entity can go. The four removal handlers and the validity checks are
  gone. Entities destroyed while the feature is disabled are swept when
  it is enabled again. Saves from before the registrations table get it
  through the Storage on_init callback.
- Spawn area applied the water offset twice and the pattern offset
  never. The pattern tiles now move with the entities above them.
- pruneFeatureValues reads a validation result instead of catching an
  error, and FeaturesPage hands its pending state and unsaved bar to
  helpers.
- Shorter comments throughout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 12:55:08 +00:00

86 lines
2.8 KiB
TypeScript

import type { Instance, InstancePluginContext } from "@clusterio/host";
import * as lib from "@clusterio/lib";
import * as messages from "./messages.js";
import { features, FeatureValue } from "./features.js";
/** Sends feature config to the lua side, see module/features.lua. */
export class InstancePlugin {
instance: Instance;
logger: lib.Logger;
name: string;
/** Rcon is up and the lua side can take updates */
started = false;
constructor(context: InstancePluginContext) {
this.instance = context.instance;
this.logger = context.logger;
this.name = context.plugin.name;
}
async init() {
this.instance.handle(messages.FeatureUpdatedEvent, this.handleFeatureUpdatedEvent.bind(this));
this.instance.hooks.start.attach(this.name, this.onStart.bind(this));
this.instance.hooks.exit.attach(this.name, this.onExit.bind(this));
this.instance.hooks.controllerConnectionEvent.attach(this.name, this.onControllerConnectionEvent.bind(this));
}
async onStart() {
this.started = true;
await this.syncFeatures();
}
async onExit() {
this.started = false;
}
/** A new session means the controller restarted and forgot the subscription. */
async onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") {
if (event === "connect" && this.started) {
await this.syncFeatures();
}
}
/** Subscribe, then send every feature */
async syncFeatures() {
await this.instance.sendTo("controller", new lib.SubscriptionRequest(
`exp_scenario:${messages.FeatureUpdatedEvent.name}`, "subscribe", Date.now(),
));
const features = await this.instance.sendTo("controller", new messages.FeatureListRequest());
await this.luaSendFeatures(features);
}
async handleFeatureUpdatedEvent(event: messages.FeatureUpdatedEvent) {
if (this.started) {
await this.luaSendFeatures(event.updates.filter(feature => !feature.isDeleted));
}
}
/** Every value of each feature is sent, so the lua side sets them all */
async luaSendFeatures(records: messages.FeatureRecord[]) {
if (!records.length) {
return;
}
const updates = records.map(record => {
const values: Record<string, FeatureValue> = { enabled: record.enabled };
for (const field of features.find(feature => feature.name === record.id)?.fields ?? []) {
values[field.name] = field.name in record.values ? record.values[field.name] : field.default;
}
return { name: record.id, values };
});
const json = JSON.stringify(updates);
// Pick a long bracket the json does not contain
let level = "=";
while (json.includes(`]${level}]`)) {
level += "=";
}
await this.instance.sendRcon(
`/sc exp_scenario.features.receive_update(helpers.json_to_table[${level}[${json}]${level}])`, true,
);
}
}
export default async function (context: InstancePluginContext) {
await new InstancePlugin(context).init();
}