mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
Improve webUI loading speed with 512x512 tiles, still corrupted looking
This commit is contained in:
1 parent
e9a4627118
commit
c62eae0009
2 files changed
+130
-129
No files matched your search
+129
-129
@@ -9,23 +9,18 @@ import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import sharp from "sharp";
|
||||
|
||||
const TILE_SIZE = 512;
|
||||
const CHUNK_SIZE = 32;
|
||||
|
||||
interface PixelUpdate {
|
||||
x: number;
|
||||
y: number;
|
||||
rgba: [number, number, number, number];
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
private tilesPath: string = "";
|
||||
private fileLocks = new Map<string, Promise<void>[]>();
|
||||
private pendingUpdates = new Map<string, Set<PixelUpdate>>();
|
||||
// Set to true if colors appear inverted - this will try big-endian byte order
|
||||
private useBigEndian: boolean = false;
|
||||
|
||||
async init() {
|
||||
this.tilesPath = path.resolve(
|
||||
this.controller.config.get("controller.database_directory"),
|
||||
"minimap_tiles"
|
||||
"minimap_chunks"
|
||||
);
|
||||
await fs.ensureDir(this.tilesPath);
|
||||
|
||||
@@ -37,50 +32,100 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
this.setupTileRoutes();
|
||||
}
|
||||
|
||||
// Convert RGB565 to RGB888 values
|
||||
// Convert RGB565 to RGB888 values using Factorio's exact method
|
||||
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);
|
||||
// This matches ByteColor(uint16_t rgb5) constructor in Factorio source
|
||||
const r = ((rgb565Value >> 11) & 0x1F) << 3; // 5 bits -> 8 bits
|
||||
const g = ((rgb565Value >> 5) & 0x3F) << 2; // 6 bits -> 8 bits
|
||||
const b = (rgb565Value & 0x1F) << 3; // 5 bits -> 8 bits
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
private setupTileRoutes() {
|
||||
const app = this.controller.app;
|
||||
|
||||
// Create black placeholder image
|
||||
const createBlackImage = async () => {
|
||||
// Create black placeholder image for 512x512 tiles
|
||||
const createBlackTile = async () => {
|
||||
return await sharp({
|
||||
create: {
|
||||
width: 256,
|
||||
height: 256,
|
||||
width: 512,
|
||||
height: 512,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
},
|
||||
}).png().toBuffer();
|
||||
};
|
||||
|
||||
// Serve chart tiles with surface and force support
|
||||
// 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) => {
|
||||
try {
|
||||
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);
|
||||
const tileX = parseInt(x);
|
||||
const tileY = parseInt(y);
|
||||
|
||||
if (await fs.pathExists(filePath)) {
|
||||
const file = await fs.readFile(filePath);
|
||||
// Each 512x512 tile contains 16x16 chunks of 32x32 pixels
|
||||
const chunksPerTile = 16;
|
||||
const tileSize = 512;
|
||||
|
||||
// Calculate the range of 32x32 chunks that make up this 512x512 tile
|
||||
const startChunkX = tileX * chunksPerTile;
|
||||
const startChunkY = tileY * chunksPerTile;
|
||||
const endChunkX = startChunkX + chunksPerTile;
|
||||
const endChunkY = startChunkY + chunksPerTile;
|
||||
|
||||
// Create a 512x512 canvas
|
||||
const canvas = sharp({
|
||||
create: {
|
||||
width: tileSize,
|
||||
height: tileSize,
|
||||
channels: 4,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
// Load all the 32x32 chunks and composite them
|
||||
const compositeImages = [];
|
||||
let chunksFound = 0;
|
||||
|
||||
for (let chunkY = startChunkY; chunkY < endChunkY; chunkY++) {
|
||||
for (let chunkX = startChunkX; chunkX < endChunkX; chunkX++) {
|
||||
const filename = `${surface}_${force}_${chunkX*32}_${chunkY*32}.png`;
|
||||
const filePath = path.resolve(this.tilesPath, filename);
|
||||
|
||||
if (await fs.pathExists(filePath)) {
|
||||
const relativeX = (chunkX - startChunkX) * CHUNK_SIZE;
|
||||
const relativeY = (chunkY - startChunkY) * CHUNK_SIZE;
|
||||
|
||||
compositeImages.push({
|
||||
input: filePath,
|
||||
left: relativeX,
|
||||
top: relativeY,
|
||||
});
|
||||
chunksFound++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(chunksFound > 0) {
|
||||
this.logger.info(`Tile ${tileX},${tileY}: Found ${chunksFound} chunks, chunk range: ${startChunkX}-${endChunkX-1}, ${startChunkY}-${endChunkY-1}`);
|
||||
}
|
||||
|
||||
// If we have any chunks to composite, create the merged image
|
||||
if (compositeImages.length > 0) {
|
||||
const mergedImage = await canvas.composite(compositeImages).png().toBuffer();
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
res.send(file);
|
||||
res.send(mergedImage);
|
||||
} else {
|
||||
const blackImage = await createBlackImage();
|
||||
// No chunks found, return black tile
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
res.send(blackImage);
|
||||
res.send(await createBlackTile());
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Error serving chart tile: ${err}`);
|
||||
const blackImage = await createBlackImage();
|
||||
|
||||
// Return black tile on error
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
res.send(blackImage);
|
||||
res.send(await createBlackTile());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -129,125 +174,80 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
const size = 32;
|
||||
const [originX, originY] = position;
|
||||
const updates = new Map<string, Set<PixelUpdate>>();
|
||||
const [chunkX, chunkY] = position;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates to tiles
|
||||
for (const [filename, pixels] of updates) {
|
||||
await this.updateTileImage(filename, pixels);
|
||||
// Create filename for this chunk
|
||||
const filename = `${surface}_${force}_${chunkX}_${chunkY}.png`;
|
||||
|
||||
// Process the 32x32 chunk directly
|
||||
await this.saveChunkImage(filename, chart_data);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTileImage(filename: string, pixels: Set<PixelUpdate>) {
|
||||
private async saveChunkImage(filename: string, chartData: string) {
|
||||
const imagePath = path.resolve(this.tilesPath, filename);
|
||||
|
||||
// Handle file locking to prevent concurrent writes
|
||||
if (!this.fileLocks.has(imagePath)) {
|
||||
this.fileLocks.set(imagePath, []);
|
||||
}
|
||||
|
||||
const locks = this.fileLocks.get(imagePath)!;
|
||||
if (locks.length > 0) {
|
||||
await Promise.all(locks);
|
||||
}
|
||||
|
||||
const updatePromise = this.performTileUpdate(imagePath, pixels);
|
||||
locks.push(updatePromise);
|
||||
|
||||
try {
|
||||
await updatePromise;
|
||||
} finally {
|
||||
const index = locks.indexOf(updatePromise);
|
||||
if (index > -1) {
|
||||
locks.splice(index, 1);
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (locks.length === 0) {
|
||||
this.fileLocks.delete(imagePath);
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async performTileUpdate(imagePath: string, pixels: Set<PixelUpdate>) {
|
||||
let raw: Buffer;
|
||||
|
||||
// Save the chunk image directly
|
||||
try {
|
||||
// Load existing image or create new one
|
||||
raw = await sharp(imagePath).raw().toBuffer();
|
||||
} catch (err) {
|
||||
// Create new blank tile
|
||||
raw = await sharp({
|
||||
create: {
|
||||
width: TILE_SIZE,
|
||||
height: TILE_SIZE,
|
||||
await sharp(raw, {
|
||||
raw: {
|
||||
width: CHUNK_SIZE,
|
||||
height: CHUNK_SIZE,
|
||||
channels: 4,
|
||||
background: { r: 20, g: 20, b: 20, alpha: 0 },
|
||||
},
|
||||
}).raw().toBuffer();
|
||||
}).png().toFile(imagePath);
|
||||
} catch (err) {
|
||||
this.logger.error(`Error saving chunk image ${filename}: ${err}`);
|
||||
}
|
||||
|
||||
// Apply pixel updates
|
||||
for (const pixel of pixels) {
|
||||
const index = (pixel.y * TILE_SIZE + pixel.x) * 4;
|
||||
if (index >= 0 && index < raw.length - 3) {
|
||||
raw[index] = pixel.rgba[0]; // R
|
||||
raw[index + 1] = pixel.rgba[1]; // G
|
||||
raw[index + 2] = pixel.rgba[2]; // B
|
||||
raw[index + 3] = pixel.rgba[3]; // A
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated image
|
||||
await sharp(raw, {
|
||||
raw: {
|
||||
width: TILE_SIZE,
|
||||
height: TILE_SIZE,
|
||||
channels: 4,
|
||||
},
|
||||
}).png().toFile(imagePath);
|
||||
}
|
||||
|
||||
async handleGetInstanceBoundsRequest(request: GetInstanceBoundsRequest) {
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function MinimapPage() {
|
||||
maxNativeZoom={10}
|
||||
minNativeZoom={10}
|
||||
opacity={1}
|
||||
tileSize={512}
|
||||
/>
|
||||
</MapContainer>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user