From e9a462711873a73aa340cb04bbcd89748a7a00d5 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sat, 12 Jul 2025 12:17:58 +0200 Subject: [PATCH] Cleanup, web interface now shows images (but corrupted) --- plugins/minimap/controller.ts | 214 +++++++++++----------------- plugins/minimap/index.ts | 12 +- plugins/minimap/instance.ts | 64 +++------ plugins/minimap/messages.ts | 98 +++---------- plugins/minimap/module/minimap.lua | 30 +++- plugins/minimap/package.json | 2 + plugins/minimap/web/MinimapPage.tsx | 83 +++++++---- 7 files changed, 207 insertions(+), 296 deletions(-) diff --git a/plugins/minimap/controller.ts b/plugins/minimap/controller.ts index ebfd69fb..7b1aa982 100644 --- a/plugins/minimap/controller.ts +++ b/plugins/minimap/controller.ts @@ -2,9 +2,8 @@ import * as lib from "@clusterio/lib"; import { BaseControllerPlugin } from "@clusterio/controller"; import { TileDataEvent, - RefreshTileDataRequest, - GetTileDataRequest, GetInstanceBoundsRequest, + ChartData, } from "./messages"; import * as fs from "fs-extra"; import * as path from "path"; @@ -31,13 +30,21 @@ export class ControllerPlugin extends BaseControllerPlugin { await fs.ensureDir(this.tilesPath); this.controller.handle(TileDataEvent, this.handleTileDataEvent.bind(this)); - this.controller.handle(RefreshTileDataRequest, this.handleRefreshTileDataRequest.bind(this)); + this.controller.handle(GetInstanceBoundsRequest, this.handleGetInstanceBoundsRequest.bind(this)); // Set up HTTP routes for serving tiles this.setupTileRoutes(); } + // Convert RGB565 to RGB888 values + private rgb565ToRgb888(rgb565Value: number): [number, number, number] { + const r = Math.floor(((rgb565Value >> 11) & 0x1F) * 255 / 31); + const g = Math.floor(((rgb565Value >> 5) & 0x3F) * 255 / 63); + const b = Math.floor((rgb565Value & 0x1F) * 255 / 31); + return [r, g, b]; + } + private setupTileRoutes() { const app = this.controller.app; @@ -53,11 +60,11 @@ export class ControllerPlugin extends BaseControllerPlugin { }).png().toBuffer(); }; - // Serve terrain tiles - app.get("/api/minimap/tiles/:z/:x/:y.png", async (req, res) => { + // Serve chart tiles with surface and force support + app.get("/api/minimap/chart/:surface/:force/:z/:x/:y.png", async (req, res) => { try { - const { z, x, y } = req.params; - const filename = `tiles_z${z}x${x}y${y}.png`; + const { surface, force, z, x, y } = req.params; + const filename = `${surface}_${force}_z${z}x${x}y${y}.png`; const filePath = path.resolve(this.tilesPath, filename); if (await fs.pathExists(filePath)) { @@ -70,135 +77,103 @@ export class ControllerPlugin extends BaseControllerPlugin { res.send(blackImage); } } catch (err) { - this.logger.error(`Error serving tile: ${err}`); + this.logger.error(`Error serving chart tile: ${err}`); const blackImage = await createBlackImage(); res.setHeader("Content-Type", "image/png"); res.send(blackImage); } }); - // Serve entity tiles - app.get("/api/minimap/entities/:z/:x/:y.png", async (req, res) => { + // List available surfaces and forces + app.get("/api/minimap/surfaces", async (req, res) => { try { - const { z, x, y } = req.params; - const filename = `entities_z${z}x${x}y${y}.png`; - const filePath = path.resolve(this.tilesPath, filename); + const files = await fs.readdir(this.tilesPath); + const surfaces = new Set(); + const forces = new Set(); - if (await fs.pathExists(filePath)) { - const file = await fs.readFile(filePath); - res.setHeader("Content-Type", "image/png"); - res.send(file); - } else { - const blackImage = await createBlackImage(); - res.setHeader("Content-Type", "image/png"); - res.send(blackImage); + // Parse filenames to extract surface and force information + for (const file of files) { + const match = file.match(/^([^_]+)_([^_]+)_.*\.png$/); + if (match) { + surfaces.add(match[1]); + forces.add(match[2]); + } } + + res.json({ + surfaces: Array.from(surfaces), + forces: Array.from(forces), + }); } catch (err) { - this.logger.error(`Error serving entity tile: ${err}`); - const blackImage = await createBlackImage(); - res.setHeader("Content-Type", "image/png"); - res.send(blackImage); + this.logger.error(`Error listing surfaces: ${err}`); + res.status(500).json({ error: "Failed to list surfaces" }); } }); } async handleTileDataEvent(event: TileDataEvent) { try { - const { type, data, position, size, instanceId, layer } = event; + const { type, data, position, instanceId } = event; - if (type === "pixels") { - await this.processPixelData(data, instanceId, layer); - } else if (type === "tiles" && position && size) { - await this.processTileData(data, position, size, instanceId, layer); + if (type === "chart") { + await this.processChartData(data as ChartData[], position, instanceId); } } catch (err) { this.logger.error(`Error processing tile data: ${err}`); } } - private async processPixelData(data: string[], instanceId: number, layer: string) { - if (data.length % 3 !== 0) { - this.logger.error(`Invalid pixel data length: ${data.length}`); + private async processChartData(chartData: ChartData[], position: [number, number], instanceId: number) { + if (!position || !instanceId) { + this.logger.error("Chart data requires position and instanceId"); return; } - const updates = new Map>(); - - for (let i = 0; i < data.length; i += 3) { - const x = Math.floor(Number(data[i])); - const y = Math.floor(Number(data[i + 1])); - const colorHex = data[i + 2]; - - if (colorHex.length !== 8) { - continue; // Skip invalid color data - } - - const rgba: [number, number, number, number] = [ - parseInt(colorHex.slice(0, 2), 16), - parseInt(colorHex.slice(2, 4), 16), - parseInt(colorHex.slice(4, 6), 16), - parseInt(colorHex.slice(6, 8), 16), - ]; - - // Calculate which tile this pixel belongs to - const tileX = Math.floor(x / TILE_SIZE) + (x < 0 ? -1 : 0); - const tileY = Math.floor(y / TILE_SIZE) + (y < 0 ? -1 : 0); - const filename = `${layer}z10x${tileX}y${tileY}.png`; - - if (!updates.has(filename)) { - updates.set(filename, new Set()); - } - - updates.get(filename)!.add({ - x: ((x % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, - y: ((y % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, - rgba, - }); - } - - // Apply updates to tiles - for (const [filename, pixels] of updates) { - await this.updateTileImage(filename, pixels); - } - } - - private async processTileData(data: string[], position: [number, number], size: number, instanceId: number, layer: string) { + const size = 32; const [originX, originY] = position; const updates = new Map>(); - for (let i = 0; i < data.length; i++) { - const x = i % size; - const y = Math.floor(i / size); - const colorHex = data[i]; + // Process each surface/force combination + for (const chart of chartData) { + const { surface, force, chart_data } = chart; + + // Process each pixel in the 32x32 chunk + for (let i = 0; i < chart_data.length; i += 2) { + const byte1 = chart_data.charCodeAt(i); + const byte2 = chart_data.charCodeAt(i + 1); + + if (isNaN(byte1) || isNaN(byte2)) { + continue; + } - if (colorHex.length !== 6) { - continue; // Skip invalid color data + const rgb565Value = byte1 + (byte2 << 8); + const [r, g, b] = this.rgb565ToRgb888(rgb565Value); + + // Calculate pixel position within the chunk + const pixelIndex = i / 2; + const x = pixelIndex % size; + const y = Math.floor(pixelIndex / size); + + const worldX = originX + x; + const worldY = originY + y; + + // Calculate which tile this pixel belongs to + const tileX = Math.floor(worldX / TILE_SIZE) + (worldX < 0 ? -1 : 0); + const tileY = Math.floor(worldY / TILE_SIZE) + (worldY < 0 ? -1 : 0); + + // Use surface and force in the filename for differentiation + const filename = `${surface}_${force}_z10x${tileX}y${tileY}.png`; + + if (!updates.has(filename)) { + updates.set(filename, new Set()); + } + + updates.get(filename)!.add({ + x: ((worldX % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, + y: ((worldY % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, + rgba: [r, g, b, 255], // Full alpha for chart data + }); } - - const rgba: [number, number, number, number] = [ - parseInt(colorHex.slice(0, 2), 16), - parseInt(colorHex.slice(2, 4), 16), - parseInt(colorHex.slice(4, 6), 16), - 255, // Full alpha for tile data - ]; - - const worldX = originX + x; - const worldY = originY + y; - - // Calculate which tile this pixel belongs to - const tileX = Math.floor(worldX / TILE_SIZE) + (worldX < 0 ? -1 : 0); - const tileY = Math.floor(worldY / TILE_SIZE) + (worldY < 0 ? -1 : 0); - const filename = `${layer}z10x${tileX}y${tileY}.png`; - - if (!updates.has(filename)) { - updates.set(filename, new Set()); - } - - updates.get(filename)!.add({ - x: ((worldX % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, - y: ((worldY % TILE_SIZE) + TILE_SIZE) % TILE_SIZE, - rgba, - }); } // Apply updates to tiles @@ -275,35 +250,6 @@ export class ControllerPlugin extends BaseControllerPlugin { }).png().toFile(imagePath); } - async handleRefreshTileDataRequest(request: RefreshTileDataRequest): Promise<{ success: boolean; message?: string }> { - try { - const { instanceId, area } = request; - const instance = this.controller.instances.get(instanceId); - - if (!instance) { - return { success: false, message: "Instance not found" }; - } - - if (instance.status !== "running") { - return { success: false, message: "Instance is not running" }; - } - - // Send request to instance to get tile data - const defaultArea = { x1: -512, y1: -512, x2: 512, y2: 512 }; - const tileArea = area || defaultArea; - - await this.controller.sendTo( - { instanceId }, - new GetTileDataRequest(tileArea) - ); - - return { success: true, message: "Tile refresh initiated" }; - } catch (err) { - this.logger.error(`Error refreshing tile data: ${err}`); - return { success: false, message: `Error: ${err}` }; - } - } - async handleGetInstanceBoundsRequest(request: GetInstanceBoundsRequest) { const instances = Array.from(this.controller.instances.values()) .filter(instance => instance.status === "running") diff --git a/plugins/minimap/index.ts b/plugins/minimap/index.ts index 16425798..3a0c2626 100644 --- a/plugins/minimap/index.ts +++ b/plugins/minimap/index.ts @@ -2,8 +2,6 @@ import * as lib from "@clusterio/lib"; import { TileDataEvent, - RefreshTileDataRequest, - GetTileDataRequest, GetInstanceBoundsRequest, } from "./messages"; @@ -15,13 +13,6 @@ lib.definePermission({ grantByDefault: true, }); -lib.definePermission({ - name: "minimap.refresh", - title: "Refresh Minimap", - description: "Refresh the minimap data for Factorio instances.", - grantByDefault: true, -}); - export const plugin: lib.PluginDeclaration = { name: "minimap", title: "Minimap", @@ -32,8 +23,7 @@ export const plugin: lib.PluginDeclaration = { messages: [ TileDataEvent, - RefreshTileDataRequest, - GetTileDataRequest, GetInstanceBoundsRequest, ], + routes: ["/minimap"], }; diff --git a/plugins/minimap/instance.ts b/plugins/minimap/instance.ts index d49c0d44..68e4b1a8 100644 --- a/plugins/minimap/instance.ts +++ b/plugins/minimap/instance.ts @@ -1,13 +1,11 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; -import { TileDataEvent, GetTileDataRequest } from "./messages"; +import { TileDataEvent, ChartData } from "./messages"; interface TileDataIpc { - type: "tiles" | "pixels"; - data: string; - position?: [number, number]; - size?: number; - layer?: string; + type: "chart"; + data: ChartData[]; + position: [number, number]; } export class InstancePlugin extends BaseInstancePlugin { @@ -18,8 +16,6 @@ export class InstancePlugin extends BaseInstancePlugin { throw new Error("minimap plugin requires script commands to be enabled."); } - this.instance.handle(GetTileDataRequest, this.handleGetTileDataRequest.bind(this)); - // Listen for tile data from the Lua module this.instance.server.on("ipc-minimap:tile_data", (data: TileDataIpc) => { this.handleTileDataFromLua(data).catch(err => this.logger.error( @@ -28,43 +24,29 @@ export class InstancePlugin extends BaseInstancePlugin { }); } - async handleGetTileDataRequest(request: GetTileDataRequest): Promise<{ tileData: string[] }> { - const { area } = request; - try { - // Send command to Lua to dump tile data for the specified area - const command = `/sc minimap.dump_mapview({${area.x1}, ${area.y1}}, {${area.x2}, ${area.y2}})`; - const response = await this.sendRcon(command); - - // Parse the response - it should be a semicolon-separated string of hex colors - const tileData = response ? response.split(";").filter(data => data.length > 0) : []; - - return { tileData }; - } catch (err) { - this.logger.error(`Failed to get tile data: ${err}`); - return { tileData: [] }; - } - } - async handleTileDataFromLua(data: TileDataIpc) { try { - // Parse the incoming data - const tileData = data.data.split(";").filter(segment => segment.length > 0); + const { type, data: rawData, position } = data; - if (tileData.length === 0) { - return; + if (type === "chart") { + // Handle new chart data format + const chartData = rawData as ChartData[]; + + if (!chartData || !Array.isArray(chartData)) { + this.logger.error("Invalid chart data received"); + return; + } + + // Send chart data to controller + const event = new TileDataEvent( + "chart", + chartData, + position!, + this.instance.config.get("instance.id") + ); + + return this.instance.sendTo("controller", event); } - - // Send tile data to controller - const event = new TileDataEvent( - data.type, - tileData, - data.position || null, - data.size || null, - this.instance.config.get("instance.id"), - data.layer || "" - ); - - await this.instance.sendTo("controller", event); } catch (err) { this.logger.error(`Failed to process tile data from Lua: ${err}`); diff --git a/plugins/minimap/messages.ts b/plugins/minimap/messages.ts index b9c4225a..7728402e 100644 --- a/plugins/minimap/messages.ts +++ b/plugins/minimap/messages.ts @@ -1,6 +1,15 @@ import { Type, Static } from "@sinclair/typebox"; import * as lib from "@clusterio/lib"; +// Define the chart data structure +export const ChartDataSchema = Type.Object({ + surface: Type.String(), + force: Type.String(), + chart_data: Type.String(), +}); + +export type ChartData = Static; + export class TileDataEvent { declare ["constructor"]: typeof TileDataEvent; static type = "event" as const; @@ -10,101 +19,30 @@ export class TileDataEvent { static permission = null; constructor( - public type: "tiles" | "pixels", - public data: string[], - public position: [number, number] | null, - public size: number | null, - public instanceId: number, - public layer: string = "" + public type: "chart", + public data: ChartData[], + public position: [number, number], + public instanceId: number ) { } static jsonSchema = Type.Object({ - "type": Type.Union([Type.Literal("tiles"), Type.Literal("pixels")]), - "data": Type.Array(Type.String()), - "position": Type.Union([Type.Array(Type.Number(), { minItems: 2, maxItems: 2 }), Type.Null()]), - "size": Type.Union([Type.Number(), Type.Null()]), + "type": Type.Union([Type.Literal("chart")]), + "data": Type.Array(ChartDataSchema), + "position": Type.Array(Type.Number(), { minItems: 2, maxItems: 2 }), "instanceId": Type.Number(), - "layer": Type.String(), }); static fromJSON(json: Static) { return new this( json.type, json.data, - json.position as [number, number] | null, - json.size, - json.instanceId, - json.layer + json.position as [number, number], + json.instanceId, ); } } -export class RefreshTileDataRequest { - declare ["constructor"]: typeof RefreshTileDataRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static plugin = "minimap" as const; - static permission = "minimap.refresh"; - - constructor( - public instanceId: number, - public area?: { x1: number; y1: number; x2: number; y2: number } - ) { - } - - static jsonSchema = Type.Object({ - "instanceId": Type.Number(), - "area": Type.Optional(Type.Object({ - "x1": Type.Number(), - "y1": Type.Number(), - "x2": Type.Number(), - "y2": Type.Number(), - })), - }); - - static fromJSON(json: Static) { - return new this(json.instanceId, json.area); - } - - static Response = lib.plainJson(Type.Object({ - "success": Type.Boolean(), - "message": Type.Optional(Type.String()), - })); -} - -export class GetTileDataRequest { - declare ["constructor"]: typeof GetTileDataRequest; - static type = "request" as const; - static src = "controller" as const; - static dst = "instance" as const; - static plugin = "minimap" as const; - static permission = null; - - constructor( - public area: { x1: number; y1: number; x2: number; y2: number } - ) { - } - - static jsonSchema = Type.Object({ - "area": Type.Object({ - "x1": Type.Number(), - "y1": Type.Number(), - "x2": Type.Number(), - "y2": Type.Number(), - }), - }); - - static fromJSON(json: Static) { - return new this(json.area); - } - - static Response = lib.plainJson(Type.Object({ - "tileData": Type.Array(Type.String()), - })); -} - export class GetInstanceBoundsRequest { declare ["constructor"]: typeof GetInstanceBoundsRequest; static type = "request" as const; diff --git a/plugins/minimap/module/minimap.lua b/plugins/minimap/module/minimap.lua index 33ea2609..4be83234 100644 --- a/plugins/minimap/module/minimap.lua +++ b/plugins/minimap/module/minimap.lua @@ -47,7 +47,6 @@ local function dump_chunk_chart(chunk_position) clusterio_api.send_json("minimap:tile_data", { type = "chart", position = {world_x, world_y}, - size = 32, data = data, }) end @@ -116,14 +115,43 @@ local function on_tile_changed(event) end end +log("minimap.lua loaded") -- Initialize the module local function init() + log("minimap.lua init") if not storage.minimap then storage.minimap = { enabled = true, chunk_update_queue = {} } end + + if not storage.minimap.enabled then + return + end + + -- Remove previous queue + storage.minimap.chunk_update_queue = {} + + -- Queue all existing charted chunks for update + local chunks_queued = 0 + + for _, force in pairs(game.forces) do + for _, surface in pairs(game.surfaces) do + -- Iterate through all generated chunks on this surface + for chunk in surface.get_chunks() do + local chunk_position = {x = chunk.x, y = chunk.y} + + -- Check if this chunk is charted for this force + if force.is_chunk_charted(surface, chunk_position) then + table.insert(storage.minimap.chunk_update_queue, chunk_position) + chunks_queued = chunks_queued + 1 + end + end + end + end + + log("Minimap: Queued " .. chunks_queued .. " chunks for initial map generation") end local function on_tick(event) diff --git a/plugins/minimap/package.json b/plugins/minimap/package.json index b6927ea6..fa094437 100644 --- a/plugins/minimap/package.json +++ b/plugins/minimap/package.json @@ -15,6 +15,8 @@ ], "scripts": { "prepare": "tsc --build && webpack-cli --env production", + "watch": "tsc --build --watch", + "watch-web": "webpack-cli --env development --watch", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { diff --git a/plugins/minimap/web/MinimapPage.tsx b/plugins/minimap/web/MinimapPage.tsx index b0a6a797..802aaf46 100644 --- a/plugins/minimap/web/MinimapPage.tsx +++ b/plugins/minimap/web/MinimapPage.tsx @@ -3,7 +3,10 @@ import { Row, Col, Button, Card, Space, Select } from "antd"; import { MapContainer, TileLayer } from "react-leaflet"; import L from "leaflet"; import { ControlContext } from "@clusterio/web_ui"; -import { GetInstanceBoundsRequest, RefreshTileDataRequest } from "../messages"; +import { GetInstanceBoundsRequest } from "../messages"; + +// Import Leaflet CSS +import "leaflet/dist/leaflet.css"; interface Instance { instanceId: number; @@ -16,9 +19,17 @@ interface Instance { }; } +interface SurfaceForceData { + surfaces: string[]; + forces: string[]; +} + export default function MinimapPage() { const [instances, setInstances] = useState([]); 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); @@ -26,6 +37,7 @@ export default function MinimapPage() { // Load instance bounds on component mount useEffect(() => { loadInstances(); + loadSurfaceForceData(); }, []); const loadInstances = async () => { @@ -44,19 +56,22 @@ export default function MinimapPage() { } }; - const refreshTileData = async () => { - if (!selectedInstance) return; - + const loadSurfaceForceData = async () => { try { - setLoading(true); - await control.send(new RefreshTileDataRequest(selectedInstance)); + const response = await fetch(`${window.location.origin}/api/minimap/surfaces`); + console.log(response); + const data = await response.json(); + setSurfaceForceData(data); - // Force tile refresh by incrementing the refresh parameter - setRefreshTiles(prev => prev + 1); + // 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]); + } } catch (err) { - console.error("Failed to refresh tile data:", err); - } finally { - setLoading(false); + console.error("Failed to load surface/force data:", err); } }; @@ -74,7 +89,7 @@ export default function MinimapPage() { + - + {surfaceForceData.surfaces.map(surface => ( + + {surface} + + ))} + + + @@ -112,20 +144,13 @@ export default function MinimapPage() { minZoom={7} crs={L.CRS.Simple} > - {/* Terrain layer */} + {/* Chart layer with surface and force data */} - {/* Entity layer */} - ) : (