mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Cleanup, web interface now shows images (but corrupted)
This commit is contained in:
1 parent
6cbd3229f6
commit
e9a4627118
7 files changed
+207
-296
No files matched your search
+80
-134
@@ -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<string>();
|
||||
const forces = new Set<string>();
|
||||
|
||||
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<string, Set<PixelUpdate>>();
|
||||
|
||||
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<string, Set<PixelUpdate>>();
|
||||
|
||||
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")
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
+23
-41
@@ -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}`);
|
||||
|
||||
+18
-80
@@ -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<typeof ChartDataSchema>;
|
||||
|
||||
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<typeof TileDataEvent.jsonSchema>) {
|
||||
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<typeof RefreshTileDataRequest.jsonSchema>) {
|
||||
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<typeof GetTileDataRequest.jsonSchema>) {
|
||||
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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<Instance[]>([]);
|
||||
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);
|
||||
@@ -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() {
|
||||
<Card
|
||||
title="Factorio Instance Minimap"
|
||||
extra={
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<Select
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select instance"
|
||||
@@ -88,14 +103,31 @@ export default function MinimapPage() {
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={refreshTileData}
|
||||
loading={loading}
|
||||
disabled={!selectedInstance}
|
||||
<Select
|
||||
style={{ width: 150 }}
|
||||
placeholder="Select surface"
|
||||
value={selectedSurface}
|
||||
onChange={setSelectedSurface}
|
||||
>
|
||||
Refresh Map
|
||||
</Button>
|
||||
{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>
|
||||
|
||||
<Button onClick={loadInstances} loading={loading}>
|
||||
Reload Instances
|
||||
</Button>
|
||||
@@ -112,20 +144,13 @@ export default function MinimapPage() {
|
||||
minZoom={7}
|
||||
crs={L.CRS.Simple}
|
||||
>
|
||||
{/* Terrain layer */}
|
||||
{/* Chart layer with surface and force data */}
|
||||
<TileLayer
|
||||
url={`${window.location.origin}/api/minimap/tiles/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
|
||||
url={`${window.location.origin}/api/minimap/chart/${selectedSurface}/${selectedForce}/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
|
||||
maxNativeZoom={10}
|
||||
minNativeZoom={7}
|
||||
minNativeZoom={10}
|
||||
opacity={1}
|
||||
/>
|
||||
{/* Entity layer */}
|
||||
<TileLayer
|
||||
url={`${window.location.origin}/api/minimap/entities/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
|
||||
maxNativeZoom={10}
|
||||
minNativeZoom={7}
|
||||
opacity={0.8}
|
||||
/>
|
||||
</MapContainer>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user