mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
minimap: address small review feedback
This commit is contained in:
1 parent
923316b44d
commit
57b50048bc
7 files changed
+80
-250
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user