minimap: address small review feedback

This commit is contained in:
Danielv123 committed 2026-02-09 18:54:55 +01:00
1 parent 923316b44d
commit 57b50048bc
7 files changed
+80 -250

No files matched your search

+3
View File
@@ -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}`);
}
}
+30 -30
View File
@@ -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
}
}
}
+7 -7
View File
@@ -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
+27 -20
View File
@@ -229,32 +229,39 @@ export function parseAndDeduplicatePlayerPositions(buf: BinaryData): Map<number,
// Build player timelines from the parsed data
const playerTimelines = new Map<number, ParsedPlayerPos[]>();
const playerNameById = new Map<number, string>();
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,
});
}
}
+7 -19
View File
@@ -544,19 +544,19 @@ export default function CanvasMinimapPage({
};
// Throttled zoom display update to avoid excessive re-renders
const throttledZoomUpdate = useRef<NodeJS.Timeout>();
// Debounced zoom display update to avoid excessive re-renders
const debouncedZoomUpdate = useRef<NodeJS.Timeout>();
// Helper to update view state without React re-render
const updateViewState = (updates: Partial<ViewState>) => {
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(() => {});
}
};
-174
View File
@@ -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<number | null>(null);
const [selectedSurface, setSelectedSurface] = useState<string>("nauvis");
const [selectedForce, setSelectedForce] = useState<string>("player");
const [surfaceForceData, setSurfaceForceData] = useState<SurfaceForceData>({ 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 (
<div style={{ padding: "20px" }}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Card
title="Factorio Instance Minimap"
extra={
<Space wrap>
<Select
style={{ width: 200 }}
placeholder="Select instance"
value={selectedInstance}
onChange={setSelectedInstance}
loading={loading}
options={[...instances.values()].map(instance => ({
value: instance.id,
label: instance.name,
}))}
/>
<Select
style={{ width: 150 }}
placeholder="Select surface"
value={selectedSurface}
onChange={setSelectedSurface}
>
{surfaceForceData.surfaces.map(surface => (
<Select.Option key={surface} value={surface}>
{surface}
</Select.Option>
))}
</Select>
<Select
style={{ width: 150 }}
placeholder="Select force"
value={selectedForce}
onChange={setSelectedForce}
>
{surfaceForceData.forces.map(force => (
<Select.Option key={force} value={force}>
{force}
</Select.Option>
))}
</Select>
</Space>
}
>
{selectedInstance && mapBounds ? (
<div
style={{
height: "700px",
width: "100%",
}}
className="minimap-container"
>
<style>
{`/* Avoid blurry rendering when zooming in */
.minimap-container .leaflet-container .leaflet-overlay-pane svg,
.minimap-container .leaflet-container .leaflet-marker-pane img,
.minimap-container .leaflet-container .leaflet-shadow-pane img,
.minimap-container .leaflet-container .leaflet-tile-pane img,
.minimap-container .leaflet-container img.leaflet-image-layer {
image-rendering: pixelated;
}
`}
</style>
<MapContainer
bounds={mapBounds}
style={{ height: "100%", width: "100%", backgroundColor: "#1a1a1a" }}
attributionControl={false}
maxZoom={18}
minZoom={7}
crs={L.CRS.Simple}
>
{/* Chart layer with surface and force data */}
<TileLayer
// eslint-disable-next-line max-len
url={`${window.location.origin}/api/minimap/chart/${selectedInstance}/${selectedSurface}/${selectedForce}/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
maxNativeZoom={10}
minNativeZoom={10}
opacity={1}
tileSize={512}
/>
</MapContainer>
</div>
) : (
<div
style={{
height: "400px",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#f5f5f5",
border: "1px dashed #d9d9d9",
borderRadius: "6px",
}}
>
<div style={{ textAlign: "center" }}>
<h3>No Instance Selected</h3>
<p>Select a running instance to view its minimap</p>
</div>
</div>
)}
</Card>
</Col>
</Row>
</div>
);
}
@@ -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) {