mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Use encode_string and support multiple instances
This commit is contained in:
1 parent
c62eae0009
commit
5b9f650946
3 files changed
+93
-60
No files matched your search
@@ -7,9 +7,12 @@ import {
|
||||
} from "./messages";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import * as zlib from "zlib";
|
||||
import { promisify } from "util";
|
||||
import sharp from "sharp";
|
||||
|
||||
const CHUNK_SIZE = 32;
|
||||
const inflateAsync = promisify(zlib.inflate);
|
||||
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
@@ -57,9 +60,9 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
};
|
||||
|
||||
// Serve chart tiles with surface and force support - now serving 512x512 tiles
|
||||
app.get("/api/minimap/chart/:surface/:force/:z/:x/:y.png", async (req, res) => {
|
||||
app.get("/api/minimap/chart/:instanceId/:surface/:force/:z/:x/:y.png", async (req, res) => {
|
||||
try {
|
||||
const { surface, force, z, x, y } = req.params;
|
||||
const { instanceId, surface, force, z, x, y } = req.params;
|
||||
const tileX = parseInt(x);
|
||||
const tileY = parseInt(y);
|
||||
|
||||
@@ -67,7 +70,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
const chunksPerTile = 16;
|
||||
const tileSize = 512;
|
||||
|
||||
// Calculate the range of 32x32 chunks that make up this 512x512 tile
|
||||
// Calculate the range of chunk coordinates that make up this 512x512 tile
|
||||
const startChunkX = tileX * chunksPerTile;
|
||||
const startChunkY = tileY * chunksPerTile;
|
||||
const endChunkX = startChunkX + chunksPerTile;
|
||||
@@ -89,7 +92,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) {
|
||||
for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) {
|
||||
const filename = `${surface}_${force}_${chunkX*32}_${chunkY*32}.png`;
|
||||
const filename = `${instanceId}/${surface}/${force}/${chunkX}_${chunkY}.png`;
|
||||
const filePath = path.resolve(this.tilesPath, filename);
|
||||
|
||||
if (await fs.pathExists(filePath)) {
|
||||
@@ -132,20 +135,39 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
// List available surfaces and forces
|
||||
app.get("/api/minimap/surfaces", async (req, res) => {
|
||||
try {
|
||||
const files = await fs.readdir(this.tilesPath);
|
||||
const instances = new Set<string>();
|
||||
const surfaces = new Set<string>();
|
||||
const forces = new Set<string>();
|
||||
|
||||
// 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]);
|
||||
// Traverse the hierarchical directory structure: instanceId/surface/force/
|
||||
const instanceDirs = await fs.readdir(this.tilesPath, { withFileTypes: true });
|
||||
|
||||
for (const instanceDir of instanceDirs) {
|
||||
if (instanceDir.isDirectory()) {
|
||||
instances.add(instanceDir.name);
|
||||
|
||||
const instancePath = path.join(this.tilesPath, instanceDir.name);
|
||||
const surfaceDirs = await fs.readdir(instancePath, { withFileTypes: true });
|
||||
|
||||
for (const surfaceDir of surfaceDirs) {
|
||||
if (surfaceDir.isDirectory()) {
|
||||
surfaces.add(surfaceDir.name);
|
||||
|
||||
const surfacePath = path.join(instancePath, surfaceDir.name);
|
||||
const forceDirs = await fs.readdir(surfacePath, { withFileTypes: true });
|
||||
|
||||
for (const forceDir of forceDirs) {
|
||||
if (forceDir.isDirectory()) {
|
||||
forces.add(forceDir.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
instances: Array.from(instances),
|
||||
surfaces: Array.from(surfaces),
|
||||
forces: Array.from(forces),
|
||||
});
|
||||
@@ -174,14 +196,18 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
const [chunkX, chunkY] = position;
|
||||
const [worldX, worldY] = position;
|
||||
|
||||
// Convert world coordinates to chunk coordinates
|
||||
const chunkX = Math.floor(worldX / 32);
|
||||
const chunkY = Math.floor(worldY / 32);
|
||||
|
||||
// Process each surface/force combination
|
||||
for (const chart of chartData) {
|
||||
const { surface, force, chart_data } = chart;
|
||||
|
||||
// Create filename for this chunk
|
||||
const filename = `${surface}_${force}_${chunkX}_${chunkY}.png`;
|
||||
// Create filename using chunk coordinates
|
||||
const filename = `${instanceId}/${surface}/${force}/${chunkX}_${chunkY}.png`;
|
||||
|
||||
// Process the 32x32 chunk directly
|
||||
await this.saveChunkImage(filename, chart_data);
|
||||
@@ -191,53 +217,52 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
private async saveChunkImage(filename: string, chartData: string) {
|
||||
const imagePath = path.resolve(this.tilesPath, filename);
|
||||
|
||||
// Ensure the directory structure exists
|
||||
await fs.ensureDir(path.dirname(imagePath));
|
||||
|
||||
// Create raw image buffer for 32x32 RGBA
|
||||
const raw = Buffer.alloc(CHUNK_SIZE * CHUNK_SIZE * 4);
|
||||
|
||||
// Convert string to proper binary buffer first
|
||||
const binaryBuffer = Buffer.from(chartData, 'latin1'); // or 'latin1'
|
||||
const dataLength = Math.min(binaryBuffer.length, CHUNK_SIZE * CHUNK_SIZE * 2);
|
||||
|
||||
this.logger.info(`Processing chunk ${filename}, buffer length: ${binaryBuffer.length}, expected: ${CHUNK_SIZE * CHUNK_SIZE * 2}`);
|
||||
|
||||
for (let i = 0; i < dataLength; i += 2) {
|
||||
// Read bytes properly from binary buffer
|
||||
const byte1 = binaryBuffer[i];
|
||||
const byte2 = binaryBuffer[i + 1];
|
||||
|
||||
if (byte1 === undefined || byte2 === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Choose endianness based on flag
|
||||
const rgb565Value = this.useBigEndian ?
|
||||
(byte1 << 8) + byte2 : // Big-endian: byte1 is high byte
|
||||
byte1 + (byte2 << 8); // Little-endian: byte1 is low byte
|
||||
|
||||
const [r, g, b] = this.rgb565ToRgb888(rgb565Value);
|
||||
|
||||
// Debug first few pixels
|
||||
if (i < 10) {
|
||||
this.logger.info(`Pixel ${i/2}: bytes[${byte1.toString(16)}, ${byte2.toString(16)}] -> RGB565: 0x${rgb565Value.toString(16)} -> RGB[${r}, ${g}, ${b}]`);
|
||||
}
|
||||
|
||||
// Calculate pixel position within the chunk
|
||||
const pixelIndex = i / 2;
|
||||
const x = pixelIndex % CHUNK_SIZE;
|
||||
const y = Math.floor(pixelIndex / CHUNK_SIZE);
|
||||
|
||||
// Set pixel in raw buffer (RGBA order - Sharp expects this)
|
||||
const bufferIndex = pixelIndex * 4;
|
||||
if (bufferIndex >= 0 && bufferIndex < raw.length - 3) {
|
||||
raw[bufferIndex] = r; // R
|
||||
raw[bufferIndex + 1] = g; // G
|
||||
raw[bufferIndex + 2] = b; // B
|
||||
raw[bufferIndex + 3] = 255; // A (full alpha)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the chunk image directly
|
||||
try {
|
||||
// Decode base64 to get the deflate-compressed data
|
||||
const compressedBuffer = Buffer.from(chartData, 'base64');
|
||||
|
||||
// Decompress the deflate data to get the original binary chart data
|
||||
const binaryBuffer = await inflateAsync(compressedBuffer);
|
||||
const dataLength = Math.min(binaryBuffer.length, CHUNK_SIZE * CHUNK_SIZE * 2);
|
||||
|
||||
for (let i = 0; i < dataLength; i += 2) {
|
||||
// Read bytes properly from binary buffer
|
||||
const byte1 = binaryBuffer[i];
|
||||
const byte2 = binaryBuffer[i + 1];
|
||||
|
||||
if (byte1 === undefined || byte2 === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Choose endianness based on flag
|
||||
const rgb565Value = this.useBigEndian ?
|
||||
(byte1 << 8) + byte2 : // Big-endian: byte1 is high byte
|
||||
byte1 + (byte2 << 8); // Little-endian: byte1 is low byte
|
||||
|
||||
const [r, g, b] = this.rgb565ToRgb888(rgb565Value);
|
||||
|
||||
// Calculate pixel position within the chunk
|
||||
const pixelIndex = i / 2;
|
||||
const x = pixelIndex % CHUNK_SIZE;
|
||||
const y = Math.floor(pixelIndex / CHUNK_SIZE);
|
||||
|
||||
// Set pixel in raw buffer (RGBA order - Sharp expects this)
|
||||
const bufferIndex = pixelIndex * 4;
|
||||
if (bufferIndex >= 0 && bufferIndex < raw.length - 3) {
|
||||
raw[bufferIndex] = r; // R
|
||||
raw[bufferIndex + 1] = g; // G
|
||||
raw[bufferIndex + 2] = b; // B
|
||||
raw[bufferIndex + 3] = 255; // A (full alpha)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the chunk image directly
|
||||
await sharp(raw, {
|
||||
raw: {
|
||||
width: CHUNK_SIZE,
|
||||
@@ -245,8 +270,14 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
channels: 4,
|
||||
},
|
||||
}).png().toFile(imagePath);
|
||||
|
||||
// Get the final PNG file size for comparison
|
||||
const stats = await fs.stat(imagePath);
|
||||
const pngSize = stats.size;
|
||||
|
||||
this.logger.info(`Chunk ${filename}: raw ${raw.length}, decompressed ${binaryBuffer.length}, compressed ${compressedBuffer.length}, PNG ${pngSize} bytes`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Error saving chunk image ${filename}: ${err}`);
|
||||
this.logger.error(`Error processing chunk image ${filename}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ local function dump_chunk_chart(chunk_position)
|
||||
for _, surface in pairs(surfaces) do
|
||||
local chart_data = force.get_chunk_chart(surface, chunk_position)
|
||||
if chart_data then
|
||||
table.insert(data, {surface = surface.name, force = force.name, chart_data = chart_data})
|
||||
-- Use Factorio's built-in deflate compression and base64 encoding
|
||||
local encoded_data = helpers.encode_string(chart_data)
|
||||
table.insert(data, {surface = surface.name, force = force.name, chart_data = encoded_data})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function MinimapPage() {
|
||||
>
|
||||
{/* Chart layer with surface and force data */}
|
||||
<TileLayer
|
||||
url={`${window.location.origin}/api/minimap/chart/${selectedSurface}/${selectedForce}/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
|
||||
url={`${window.location.origin}/api/minimap/chart/${selectedInstance}/${selectedSurface}/${selectedForce}/{z}/{x}/{y}.png?refresh=${refreshTiles}`}
|
||||
maxNativeZoom={10}
|
||||
minNativeZoom={10}
|
||||
opacity={1}
|
||||
|
||||
Reference in new issue
Block a user