diff --git a/plugins/minimap/controller.ts b/plugins/minimap/controller.ts index 575f65be..bdbebd4c 100644 --- a/plugins/minimap/controller.ts +++ b/plugins/minimap/controller.ts @@ -98,6 +98,7 @@ export class ControllerPlugin extends BaseControllerPlugin { await fs.ensureDir(this.recipeTilesPath); await fs.ensureDir(this.playerPositionsPath); + // Handle incoming events from instances this.controller.handle(TileDataEvent, this.handleTileDataEvent.bind(this)); this.controller.handle(ChartTagDataEvent, this.handleChartTagDataEvent.bind(this)); this.controller.handle(GetRawTileRequest, this.handleGetRawTileRequest.bind(this)); @@ -107,6 +108,12 @@ export class ControllerPlugin extends BaseControllerPlugin { this.controller.handle(PlayerPositionEvent, this.handlePlayerPositionEvent.bind(this)); this.controller.handle(GetPlayerPathRequest, this.handleGetPlayerPathRequest.bind(this)); + // Register events as subscribable for web clients + this.controller.subscriptions.handle(TileDataEvent); + this.controller.subscriptions.handle(ChartTagDataEvent); + this.controller.subscriptions.handle(RecipeDataEvent); + this.controller.subscriptions.handle(PlayerPositionEvent); + // Set up HTTP routes for serving tiles this.setupTileRoutes(); @@ -750,11 +757,10 @@ export class ControllerPlugin extends BaseControllerPlugin { // Queue for batch saving this.chartTagQueues.get(fileKey)!.push(enrichedTagData); - // Send live update to connected web clients const updateEvent = new ChartTagDataEvent(instance_id, tag_data); - // Broadcast to web clients - this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), updateEvent); + // Broadcast to subscribed web clients only + this.controller.subscriptions.broadcast(updateEvent); } catch (err) { this.logger.error(`Error processing chart tag data: ${err}`); @@ -780,7 +786,6 @@ export class ControllerPlugin extends BaseControllerPlugin { } this.chunkSavingQueue.get(tileName)!.set(chunkName, { data: chunk, tick }); - // Send live update to connected web clients const updateEvent = new TileDataEvent( instance_id, surface, @@ -791,8 +796,8 @@ export class ControllerPlugin extends BaseControllerPlugin { chunk ); - // Broadcast to web clients - this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), updateEvent); + // Broadcast to subscribed web clients only + this.controller.subscriptions.broadcast(updateEvent); } catch (err) { this.logger.error(`Error processing tile data: ${err}`); @@ -1009,8 +1014,8 @@ export class ControllerPlugin extends BaseControllerPlugin { } this.recipeSavingQueue.get(tileKey)!.push(...buffersToWrite); - // broadcast live update - this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), event); + // Broadcast live update to subscribed web clients only + this.controller.subscriptions.broadcast(event); } catch (err) { this.logger.error(`Error processing recipe data: ${err}`); @@ -1078,11 +1083,10 @@ export class ControllerPlugin extends BaseControllerPlugin { // Queue for batch saving this.playerPositionQueues.get(fileKey)!.push(enrichedPlayerData); - // Send live update to connected web clients const updateEvent = new PlayerPositionEvent(instance_id, player_data); - // Broadcast to web clients - this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), updateEvent); + // Broadcast to subscribed web clients only + this.controller.subscriptions.broadcast(updateEvent); } catch (err) { this.logger.error(`Error processing player position data: ${err}`); diff --git a/plugins/minimap/web/index.tsx b/plugins/minimap/web/index.tsx index d6f35b8d..2f8c2315 100644 --- a/plugins/minimap/web/index.tsx +++ b/plugins/minimap/web/index.tsx @@ -1,14 +1,19 @@ import React from "react"; import { BaseWebPlugin } from "@clusterio/web_ui"; +import * as lib from "@clusterio/lib"; import CanvasMinimapPage from "./CanvasMinimapPage"; import { TileDataEvent, ChartTagDataEvent, RecipeDataEvent, PlayerPositionEvent } from "../messages"; export class WebPlugin extends BaseWebPlugin { + // Callback arrays for minimap updates tileUpdateCallbacks: Array<(event: TileDataEvent) => void> = []; chartTagUpdateCallbacks: Array<(event: ChartTagDataEvent) => void> = []; recipeUpdateCallbacks: Array<(event: RecipeDataEvent) => void> = []; playerPositionUpdateCallbacks: Array<(event: PlayerPositionEvent) => void> = []; + // Track subscription state + private subscribed = false; + async init() { this.pages = [ { @@ -19,13 +24,58 @@ export class WebPlugin extends BaseWebPlugin { }, ]; - // Handle tile update events from the controller + // Handle minimap events from the controller this.control.handle(TileDataEvent, this.handleTileDataEvent.bind(this)); this.control.handle(ChartTagDataEvent, this.handleChartTagDataEvent.bind(this)); this.control.handle(RecipeDataEvent, this.handleRecipeDataEvent.bind(this)); this.control.handle(PlayerPositionEvent, this.handlePlayerPositionEvent.bind(this)); } + onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { + if (event === "connect") { + this.updateSubscriptions(); + } + } + + private async updateSubscriptions() { + if (!this.control.connector.connected) { + return; + } + + const shouldSubscribe = this.hasActiveCallbacks(); + + if (shouldSubscribe && !this.subscribed) { + // Subscribe to all minimap events + try { + await this.control.send(new lib.SubscriptionRequest("minimap:TileDataEvent", true)); + await this.control.send(new lib.SubscriptionRequest("minimap:ChartTagDataEvent", true)); + await this.control.send(new lib.SubscriptionRequest("minimap:RecipeDataEvent", true)); + await this.control.send(new lib.SubscriptionRequest("minimap:PlayerPositionEvent", true)); + this.subscribed = true; + } catch (err) { + this.logger.error(`Failed to subscribe to minimap events: ${err}`); + } + } else if (!shouldSubscribe && this.subscribed) { + // Unsubscribe from all minimap events + try { + await this.control.send(new lib.SubscriptionRequest("minimap:TileDataEvent", false)); + await this.control.send(new lib.SubscriptionRequest("minimap:ChartTagDataEvent", false)); + await this.control.send(new lib.SubscriptionRequest("minimap:RecipeDataEvent", false)); + await this.control.send(new lib.SubscriptionRequest("minimap:PlayerPositionEvent", false)); + this.subscribed = false; + } catch (err) { + this.logger.error(`Failed to unsubscribe from minimap events: ${err}`); + } + } + } + + private hasActiveCallbacks(): boolean { + return this.tileUpdateCallbacks.length > 0 + || this.chartTagUpdateCallbacks.length > 0 + || this.recipeUpdateCallbacks.length > 0 + || this.playerPositionUpdateCallbacks.length > 0; + } + async handleTileDataEvent(event: TileDataEvent) { // Notify all registered callbacks for (let callback of this.tileUpdateCallbacks) { @@ -53,6 +103,7 @@ export class WebPlugin extends BaseWebPlugin { onTileUpdate(callback: (event: TileDataEvent) => void) { this.tileUpdateCallbacks.push(callback); + this.updateSubscriptions(); } offTileUpdate(callback: (event: TileDataEvent) => void) { @@ -61,10 +112,12 @@ export class WebPlugin extends BaseWebPlugin { throw new Error("callback is not registered"); } this.tileUpdateCallbacks.splice(index, 1); + this.updateSubscriptions(); } onChartTagUpdate(callback: (event: ChartTagDataEvent) => void) { this.chartTagUpdateCallbacks.push(callback); + this.updateSubscriptions(); } offChartTagUpdate(callback: (event: ChartTagDataEvent) => void) { @@ -73,10 +126,12 @@ export class WebPlugin extends BaseWebPlugin { throw new Error("callback is not registered"); } this.chartTagUpdateCallbacks.splice(index, 1); + this.updateSubscriptions(); } onRecipeUpdate(callback: (event: RecipeDataEvent) => void) { this.recipeUpdateCallbacks.push(callback); + this.updateSubscriptions(); } offRecipeUpdate(callback: (event: RecipeDataEvent) => void) { @@ -85,10 +140,12 @@ export class WebPlugin extends BaseWebPlugin { throw new Error("callback is not registered"); } this.recipeUpdateCallbacks.splice(index, 1); + this.updateSubscriptions(); } onPlayerPositionUpdate(callback: (event: PlayerPositionEvent) => void) { this.playerPositionUpdateCallbacks.push(callback); + this.updateSubscriptions(); } offPlayerPositionUpdate(callback: (event: PlayerPositionEvent) => void) { @@ -97,5 +154,6 @@ export class WebPlugin extends BaseWebPlugin { throw new Error("callback is not registered"); } this.playerPositionUpdateCallbacks.splice(index, 1); + this.updateSubscriptions(); } }