diff --git a/plugins/minimap/controller.ts b/plugins/minimap/controller.ts index a7de4858..36b72c68 100644 --- a/plugins/minimap/controller.ts +++ b/plugins/minimap/controller.ts @@ -830,6 +830,9 @@ export class ControllerPlugin extends BaseControllerPlugin { await this.loadTagContentFromFile(file); } } catch (err) { + if (isENOENT(err)) { + return; + } this.logger.error(`Error loading existing tag content: ${err}`); } } diff --git a/plugins/minimap/instance.ts b/plugins/minimap/instance.ts index 651db7aa..91bf37bf 100644 --- a/plugins/minimap/instance.ts +++ b/plugins/minimap/instance.ts @@ -132,20 +132,24 @@ export class InstancePlugin extends BaseInstancePlugin { } async handleChartTagDataFromLua(data: ChartTagDataIpc) { - // Validate the chart tag data - if (!data || typeof data.tag_number !== "number") { - this.logger.error("Invalid chart tag data received"); - return; + try { + // Validate the chart tag data + if (!data || typeof data.tag_number !== "number") { + this.logger.error("Invalid chart tag data received"); + return; + } + + // Create chart tag data event + const chartTagEvent = new ChartTagDataEvent( + this.instance.config.get("instance.id"), + data as ChartTagData + ); + + // Send to controller + this.instance.sendTo("controller", chartTagEvent); + } catch (err) { + this.logger.error(`Failed to process chart tag data from Lua: ${err}`); } - - // Create chart tag data event - const chartTagEvent = new ChartTagDataEvent( - this.instance.config.get("instance.id"), - data as ChartTagData - ); - - // Send to controller - this.instance.sendTo("controller", chartTagEvent); } async handleRecipeDataFromLua(data: RecipeDataIpc) { @@ -222,28 +226,24 @@ export class InstancePlugin extends BaseInstancePlugin { } async onStart() { - // Initialize the Lua module when the instance starts + // Ensure minimap storage exists in case the module wasn't loaded on startup try { - await this.sendRcon("/sc if not storage.minimap then storage.minimap = {} end"); - - // Set up periodic tile updates every 30 seconds - await this.sendRcon(`/sc - if not storage.minimap.update_timer then - storage.minimap.update_timer = 0 - storage.minimap.update_interval = 1800 -- 30 seconds at 60 UPS + await this.sendRcon(`/sc + if not storage.minimap then + storage.minimap = { + enabled = true, + chunk_update_queue = {}, + recipe_cache = {}, + player_positions = {}, + } end + if storage.minimap.enabled == nil then storage.minimap.enabled = true end + if not storage.minimap.chunk_update_queue then storage.minimap.chunk_update_queue = {} end + if not storage.minimap.recipe_cache then storage.minimap.recipe_cache = {} end + if not storage.minimap.player_positions then storage.minimap.player_positions = {} end `); - - this.logger.info("Minimap plugin initialized on instance"); } catch (err) { this.logger.error(`Failed to initialize minimap on instance: ${err}`); } } - - async onOutput(output: lib.ParsedFactorioOutput) { - // Handle any special output parsing if needed for tile data - if (output.type === "action" && output.action === "CUSTOM") { - // Could handle custom factorio output here if the Lua module sends special messages - } - } } diff --git a/plugins/minimap/module/minimap.lua b/plugins/minimap/module/minimap.lua index 78818646..0b97cf9e 100644 --- a/plugins/minimap/module/minimap.lua +++ b/plugins/minimap/module/minimap.lua @@ -1,7 +1,7 @@ local clusterio_api = require("modules/clusterio/api") local compat = require("modules/clusterio/compat") -minimap = {} +local minimap = {} -- Convert RGB565 to RGB888 values local function rgb565_to_rgb888(rgb565_value) @@ -79,14 +79,14 @@ local function process_player_positions() -- Check if position changed significantly or timeout elapsed local should_update = false - if has_moved_significantly(last_position and last_position.position or nil, position) then - should_update = true - elseif last_position and (current_sec - last_position.last_update_sec) >= 5 then - -- Send update every 5 seconds even if not moving (for timeout) - should_update = true - elseif not last_position then + if not last_position then -- First position for this player should_update = true + elseif has_moved_significantly(last_position.position, position) then + should_update = true + elseif (current_sec - last_position.last_update_sec) >= 5 then + -- Send update every 5 seconds even if not moving (for timeout) + should_update = true end if should_update then diff --git a/plugins/minimap/utils/player-utils.ts b/plugins/minimap/utils/player-utils.ts index d70ef797..aeffceca 100644 --- a/plugins/minimap/utils/player-utils.ts +++ b/plugins/minimap/utils/player-utils.ts @@ -229,32 +229,39 @@ export function parseAndDeduplicatePlayerPositions(buf: BinaryData): Map(); + const playerNameById = new Map(); + for (const [name, id] of parsed.playerSessions) { + playerNameById.set(id, name); + } // Process all records in order for (const record of parsed.positions) { - if (record.type === 0 - && record.sec !== undefined - && record.xTiles !== undefined - && record.yTiles !== undefined + if ( + record.type === 0 + && record.sec !== undefined + && record.xTiles !== undefined + && record.yTiles !== undefined ) { // Position record - convert tile coordinates to world coordinates - const name = [...parsed.playerSessions.entries()].find(([, id]) => id === record.playerId)?.[0]; - if (name) { - if (!playerTimelines.has(record.playerId)) { - playerTimelines.set(record.playerId, []); - } - - // Convert from tiles to world coordinates (tiles are 32x32 world coordinates) - const x = record.xTiles * 32; - const y = record.yTiles * 32; - - playerTimelines.get(record.playerId)!.push({ - name, - x, - y, - sec: record.sec, - }); + const name = playerNameById.get(record.playerId); + if (!name) { + continue; } + + if (!playerTimelines.has(record.playerId)) { + playerTimelines.set(record.playerId, []); + } + + // Convert from tiles to world coordinates (tiles are 32x32 world coordinates) + const x = record.xTiles * 32; + const y = record.yTiles * 32; + + playerTimelines.get(record.playerId)!.push({ + name, + x, + y, + sec: record.sec, + }); } } diff --git a/plugins/minimap/web/CanvasMinimapPage.tsx b/plugins/minimap/web/CanvasMinimapPage.tsx index 0d9bf7f8..07af8355 100644 --- a/plugins/minimap/web/CanvasMinimapPage.tsx +++ b/plugins/minimap/web/CanvasMinimapPage.tsx @@ -544,19 +544,19 @@ export default function CanvasMinimapPage({ }; - // Throttled zoom display update to avoid excessive re-renders - const throttledZoomUpdate = useRef(); + // Debounced zoom display update to avoid excessive re-renders + const debouncedZoomUpdate = useRef(); // Helper to update view state without React re-render const updateViewState = (updates: Partial) => { Object.assign(viewStateRef.current, updates); - // Update display zoom if zoom changed (throttled) + // Update display zoom if zoom changed (debounced) if (updates.zoomLevel !== undefined) { - if (throttledZoomUpdate.current) { - clearTimeout(throttledZoomUpdate.current); + if (debouncedZoomUpdate.current) { + clearTimeout(debouncedZoomUpdate.current); } - throttledZoomUpdate.current = setTimeout(() => { + debouncedZoomUpdate.current = setTimeout(() => { setDisplayZoom(viewStateRef.current.zoomLevel); }, 100); // Update display every 100ms max } @@ -1544,19 +1544,7 @@ export default function CanvasMinimapPage({ // Start loading the tile in the background (but don't wait for it) // Only attempt to load if data source is ready if (currentState.isReady) { - getVirtualTile(tileX, tileY).then(() => { - // When tile loads, trigger a re-render - // But only if we're still looking at the same area - const latestState = currentStateRef.current; - if (latestState.isReady) { - // Use a small delay to batch multiple tile loads - setTimeout(() => { - if (animationFrameRef.current) { - // Render will happen on next animation frame anyway - } - }, 16); - } - }); + void getVirtualTile(tileX, tileY).catch(() => {}); } }; diff --git a/plugins/minimap/web/MinimapPage.tsx b/plugins/minimap/web/MinimapPage.tsx deleted file mode 100644 index 31d7c20f..00000000 --- a/plugins/minimap/web/MinimapPage.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useState, useEffect, useContext } from "react"; -import { Row, Col, Button, Card, Space, Select } from "antd"; -import { MapContainer, TileLayer } from "react-leaflet"; -import L from "leaflet"; -import { ControlContext, useInstances } from "@clusterio/web_ui"; - -// Import Leaflet CSS -import "leaflet/dist/leaflet.css"; - -interface SurfaceForceData { - surfaces: string[]; - forces: string[]; -} - -export default function MinimapPage() { - const [instances] = useInstances(); - const [selectedInstance, setSelectedInstance] = useState(null); - const [selectedSurface, setSelectedSurface] = useState("nauvis"); - const [selectedForce, setSelectedForce] = useState("player"); - const [surfaceForceData, setSurfaceForceData] = useState({ surfaces: [], forces: [] }); - const [refreshTiles, setRefreshTiles] = useState(0); - const [loading, setLoading] = useState(false); - const control = useContext(ControlContext); - - // Set default instance when instances become available - useEffect(() => { - if (instances.size > 0 && !selectedInstance) { - const firstInstance = instances.values().next().value; - if (firstInstance) { - setSelectedInstance(firstInstance.id); - } - } - }, [instances, selectedInstance]); - - // Load surface and force data on component mount - useEffect(() => { - loadSurfaceForceData(); - }, []); - - // Keep subscription filters in sync with selection even on the legacy page - useEffect(() => { - const plugin = control.plugins.get("minimap") as import("./index").WebPlugin; - if (!plugin) { return; } - plugin.setInstanceSurfaceFilter(selectedInstance, selectedSurface || null); - }, [control, selectedInstance, selectedSurface]); - - const loadSurfaceForceData = async () => { - const response = await fetch(`${window.location.origin}/api/minimap/surfaces`); - const data = await response.json(); - setSurfaceForceData(data); - - // Set default selections if available - if (data.surfaces.length > 0 && !selectedSurface) { - setSelectedSurface(data.surfaces.includes("nauvis") ? "nauvis" : data.surfaces[0]); - } - if (data.forces.length > 0 && !selectedForce) { - setSelectedForce(data.forces.includes("player") ? "player" : data.forces[0]); - } - }; - - // Use default map bounds centered around origin - const mapBounds = [ - [-512 / 256, -512 / 256], - [512 / 256, 512 / 256], - ] as L.LatLngBoundsExpression; - - return ( -
- - - - - {surfaceForceData.surfaces.map(surface => ( - - {surface} - - ))} - - - - } - > - {selectedInstance && mapBounds ? ( -
- - - {/* Chart layer with surface and force data */} - - -
- ) : ( -
-
-

No Instance Selected

-

Select a running instance to view its minimap

-
-
- )} -
- -
-
- ); -} diff --git a/plugins/minimap/web/dataSources/SingleInstanceDataSource.ts b/plugins/minimap/web/dataSources/SingleInstanceDataSource.ts index c73b32f2..de5a4287 100644 --- a/plugins/minimap/web/dataSources/SingleInstanceDataSource.ts +++ b/plugins/minimap/web/dataSources/SingleInstanceDataSource.ts @@ -46,6 +46,11 @@ export class SingleInstanceDataSource implements MinimapDataSource { private force = "player"; private activeInstances: number[] = []; + /** + * Data source for the canvas renderer that reads minimap data for a single selected instance. + * + * This bridges selection state to the minimap web plugin and keeps subscription filters in sync. + */ constructor(control: Control) { this.control = control; this.plugin = control.plugins.get("minimap") as MinimapWebPlugin | undefined ?? null; @@ -182,6 +187,7 @@ export class SingleInstanceDataSource implements MinimapDataSource { return; } const filters = [{ instanceId: this.instanceId, surface: this.surface }]; + // Support both the newer multi-filter API and the legacy single-filter API. if (this.plugin.setInstanceSurfaceFilters) { this.plugin.setInstanceSurfaceFilters(filters); } else if (this.plugin.setInstanceSurfaceFilter) {