mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Lint and refactor
This commit is contained in:
1 parent
ae7a1744ad
commit
2bce506d96
7 files changed
+1233
-880
No files matched your search
+149
-102
@@ -22,7 +22,7 @@ import {
|
||||
renderTileToPixels,
|
||||
pixelsToRGBA,
|
||||
parseRecipeTileBinary,
|
||||
ParsedRecipeTile
|
||||
ParsedRecipeTile,
|
||||
} from "./tile-utils";
|
||||
|
||||
const CHUNK_SIZE = 32;
|
||||
@@ -51,7 +51,9 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
icon?: any;
|
||||
last_user?: string;
|
||||
}>>();
|
||||
private lastSeenRecipeContent = new Map<string, Map<string, string | undefined>>(); // tileKey -> posKey -> recipe
|
||||
|
||||
// tileKey -> posKey -> recipe
|
||||
private lastSeenRecipeContent = new Map<string, Map<string, string | undefined>>();
|
||||
private savingTiles: boolean = false;
|
||||
private savingChartTags = new Set<string>();
|
||||
private saveInterval: NodeJS.Timeout | null = null;
|
||||
@@ -109,7 +111,12 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
// Compare two pixel arrays and return changes
|
||||
private comparePixels(oldPixels: Uint16Array, newPixels: Uint16Array, chunkX: number, chunkY: number): PixelChange[] {
|
||||
private comparePixels(
|
||||
oldPixels: Uint16Array,
|
||||
newPixels: Uint16Array,
|
||||
chunkX: number,
|
||||
chunkY: number
|
||||
): PixelChange[] {
|
||||
const changes: PixelChange[] = [];
|
||||
|
||||
for (let y = 0; y < 32; y++) {
|
||||
@@ -146,7 +153,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
const groups = new Map<string, PixelChange[]>();
|
||||
for (const ch of changes) {
|
||||
const key = `${ch.newColor}_${ch.oldColor}`;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
if (!groups.has(key)) { groups.set(key, []); }
|
||||
groups.get(key)!.push(ch);
|
||||
}
|
||||
|
||||
@@ -184,8 +191,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
app.get("/api/minimap/tile/:instanceId/:surface/:force/:z/:x/:y.png", async (req, res) => {
|
||||
try {
|
||||
const { instanceId, surface, force, z, x, y } = req.params;
|
||||
const tileX = parseInt(x);
|
||||
const tileY = parseInt(y);
|
||||
const tileX = parseInt(x, 10);
|
||||
const tileY = parseInt(y, 10);
|
||||
|
||||
const tileName = `${instanceId}_${surface}_${force}_${tileX}_${tileY}.bin`;
|
||||
const tilePath = path.join(this.tilesPath, tileName);
|
||||
@@ -237,13 +244,13 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
// Use renderTileToPixels to get the current state, then convert to image
|
||||
const currentPixels = await renderTileToPixels(tileData);
|
||||
|
||||
// Convert RGB565 pixel data to RGBA buffer for Sharp
|
||||
// Convert RGB565 pixel data to RGBA buffer for Sharp
|
||||
const rgbaData = pixelsToRGBA(currentPixels);
|
||||
const raw = Buffer.from(rgbaData);
|
||||
|
||||
// Create and return the image directly
|
||||
return sharp(raw, {
|
||||
raw: { width: 256, height: 256, channels: 4 }
|
||||
raw: { width: 256, height: 256, channels: 4 },
|
||||
}).png().toBuffer();
|
||||
}
|
||||
|
||||
@@ -285,7 +292,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
async updateTile(existingTile: Buffer, newChunks: Map<string, { data: ChartData, tick: number }>): Promise<Buffer | undefined> {
|
||||
async updateTile(
|
||||
existingTile: Buffer,
|
||||
newChunks: Map<string, { data: ChartData, tick: number }>
|
||||
): Promise<Buffer | undefined> {
|
||||
const existingChunks = new Map<string, { tick: number, data: Buffer }>();
|
||||
|
||||
// Parse existing tile data to find existing chunks
|
||||
@@ -300,49 +310,17 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
offset += 1;
|
||||
|
||||
if (type === 1) { // Chunk
|
||||
if (offset + 7 > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient chunk header data at offset ${offset}`);
|
||||
const parseResult = this.parseChunkData(existingTile, offset, existingChunks);
|
||||
if (!parseResult.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = existingTile.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
const chunkCoordsByte = existingTile.readUInt8(offset);
|
||||
offset += 1;
|
||||
const length = existingTile.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient chunk data at offset ${offset}`);
|
||||
break;
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const chunkName = `${chunkX}_${chunkY}`;
|
||||
const data = existingTile.slice(offset, offset + length);
|
||||
offset += length;
|
||||
existingChunks.set(chunkName, { tick, data });
|
||||
|
||||
offset = parseResult.newOffset;
|
||||
} else if (type === 2) { // Pixels
|
||||
if (offset + 6 > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient pixel header data at offset ${offset}`);
|
||||
const parseResult = this.parsePixelData(existingTile, offset);
|
||||
if (!parseResult.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = existingTile.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
const pixelCount = existingTile.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
const pixelDataLength = pixelCount * 6;
|
||||
|
||||
if (offset + pixelDataLength > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient pixel data at offset ${offset}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip pixel data since we're not rebuilding the file
|
||||
offset += pixelDataLength;
|
||||
offset = parseResult.newOffset;
|
||||
} else {
|
||||
this.logger.error(`Unknown tile data type: ${type} at offset ${offset}`);
|
||||
break;
|
||||
@@ -399,13 +377,71 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Append new data to existing tile (never modify existing data)
|
||||
return Buffer.concat([existingTile, ...appendBuffers]);
|
||||
}
|
||||
|
||||
private parseChunkData(
|
||||
existingTile: Buffer,
|
||||
offset: number,
|
||||
existingChunks: Map<string, { tick: number, data: Buffer }>
|
||||
): { success: boolean, newOffset: number } {
|
||||
if (offset + 7 > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient chunk header data at offset ${offset}`);
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = existingTile.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
const chunkCoordsByte = existingTile.readUInt8(offset);
|
||||
offset += 1;
|
||||
const length = existingTile.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient chunk data at offset ${offset}`);
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const chunkName = `${chunkX}_${chunkY}`;
|
||||
const data = existingTile.slice(offset, offset + length);
|
||||
offset += length;
|
||||
existingChunks.set(chunkName, { tick, data });
|
||||
|
||||
return { success: true, newOffset: offset };
|
||||
}
|
||||
|
||||
private parsePixelData(
|
||||
existingTile: Buffer,
|
||||
offset: number
|
||||
): { success: boolean, newOffset: number } {
|
||||
if (offset + 6 > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient pixel header data at offset ${offset}`);
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = existingTile.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
const pixelCount = existingTile.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
const pixelDataLength = pixelCount * 6;
|
||||
|
||||
if (offset + pixelDataLength > existingTile.length) {
|
||||
this.logger.error(`Invalid tile data: insufficient pixel data at offset ${offset}`);
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
// Skip pixel data since we're not rebuilding the file
|
||||
offset += pixelDataLength;
|
||||
|
||||
return { success: true, newOffset: offset };
|
||||
}
|
||||
|
||||
async loadExistingTagContent() {
|
||||
try {
|
||||
if (!await fs.pathExists(this.chartTagsPath)) {
|
||||
@@ -413,48 +449,59 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
const files = await fs.readdir(this.chartTagsPath);
|
||||
const tagFiles = files.filter(file => file.endsWith('_chart_tags.json'));
|
||||
const tagFiles = files.filter(file => file.endsWith("_chart_tags.json"));
|
||||
|
||||
for (const file of tagFiles) {
|
||||
const fileKey = file.replace('_chart_tags.json', '');
|
||||
const filePath = path.join(this.chartTagsPath, file);
|
||||
|
||||
if (!this.lastSeenTagContent.has(fileKey)) {
|
||||
this.lastSeenTagContent.set(fileKey, new Map());
|
||||
}
|
||||
const seenTags = this.lastSeenTagContent.get(fileKey)!;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const tagData = JSON.parse(line);
|
||||
if (typeof tagData.tag_number === 'number' && !tagData.end_tick) {
|
||||
// Only store the latest content for tags that haven't been deleted
|
||||
seenTags.set(tagData.tag_number, {
|
||||
position: tagData.position,
|
||||
text: tagData.text,
|
||||
icon: tagData.icon,
|
||||
last_user: tagData.last_user
|
||||
});
|
||||
}
|
||||
} catch (parseErr) {
|
||||
this.logger.warn(`Failed to parse chart tag line in ${file}: ${parseErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Loaded ${seenTags.size} existing tag contents from ${file}`);
|
||||
} catch (readErr) {
|
||||
this.logger.warn(`Failed to read chart tag file ${file}: ${readErr}`);
|
||||
}
|
||||
await this.loadTagContentFromFile(file);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(`Error loading existing tag content: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTagContentFromFile(file: string) {
|
||||
const fileKey = file.replace("_chart_tags.json", "");
|
||||
const filePath = path.join(this.chartTagsPath, file);
|
||||
|
||||
if (!this.lastSeenTagContent.has(fileKey)) {
|
||||
this.lastSeenTagContent.set(fileKey, new Map());
|
||||
}
|
||||
const seenTags = this.lastSeenTagContent.get(fileKey)!;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const lines = content.trim().split("\n").filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
this.processTagLine(line, seenTags);
|
||||
}
|
||||
|
||||
this.logger.info(`Loaded ${seenTags.size} existing tag contents from ${file}`);
|
||||
} catch (readErr) {
|
||||
this.logger.warn(`Failed to read chart tag file ${file}: ${readErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
private processTagLine(line: string, seenTags: Map<number, any>) {
|
||||
try {
|
||||
const tagData = JSON.parse(line);
|
||||
const isNumberTag = typeof tagData.tag_number === "number";
|
||||
const notDeleted = !tagData.end_tick;
|
||||
|
||||
if (isNumberTag && notDeleted) {
|
||||
// Only store the latest content for tags that haven't been deleted
|
||||
seenTags.set(tagData.tag_number, {
|
||||
position: tagData.position,
|
||||
text: tagData.text,
|
||||
icon: tagData.icon,
|
||||
last_user: tagData.last_user,
|
||||
});
|
||||
}
|
||||
} catch (parseErr) {
|
||||
this.logger.warn(`Failed to parse chart tag line: ${parseErr}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses existing recipe jsonl files and builds an in-memory map that tracks
|
||||
* the latest active (non-ended) recipe for every position. This allows incoming
|
||||
@@ -496,8 +543,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
private async parseRecipeTileForState(tileKey: string, buffer: Buffer) {
|
||||
// Derive tile coordinates from key (last two underscore-separated parts)
|
||||
const parts = tileKey.split("_");
|
||||
const tileY = parseInt(parts.pop()!); // last
|
||||
const tileX = parseInt(parts.pop()!); // second last
|
||||
const tileY = parseInt(parts.pop()!, 10); // last
|
||||
const tileX = parseInt(parts.pop()!, 10); // second last
|
||||
const parsed: ParsedRecipeTile = parseRecipeTileBinary(tileX, tileY, buffer, null);
|
||||
|
||||
// Populate dictionary mapping (name -> index)
|
||||
@@ -505,7 +552,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
for (const [id, name] of parsed.dictionary) {
|
||||
dict.set(name, id);
|
||||
const currentNext = this.nextRecipeIndex.get(tileKey) ?? 0;
|
||||
if (id + 1 > currentNext) this.nextRecipeIndex.set(tileKey, id + 1);
|
||||
if (id + 1 > currentNext) { this.nextRecipeIndex.set(tileKey, id + 1); }
|
||||
}
|
||||
|
||||
// Populate active recipe cache
|
||||
@@ -583,9 +630,9 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
const filePath = path.join(this.chartTagsPath, `${fileKey}_chart_tags.json`);
|
||||
|
||||
// Convert chart tags to newline-delimited JSON format
|
||||
const jsonLines = tagsToSave.map(tag => JSON.stringify(tag)).join('\n');
|
||||
const jsonLines = tagsToSave.map(tag => JSON.stringify(tag)).join("\n");
|
||||
|
||||
await fs.appendFile(filePath, jsonLines + '\n');
|
||||
await fs.appendFile(filePath, `${jsonLines}\n`);
|
||||
|
||||
// Mark tags as successfully saved
|
||||
if (!this.lastSeenTagContent.has(fileKey)) {
|
||||
@@ -597,7 +644,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
position: tag.position,
|
||||
text: tag.text,
|
||||
icon: tag.icon,
|
||||
last_user: tag.last_user
|
||||
last_user: tag.last_user,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,11 +685,11 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
} else if (lastSeenContent) {
|
||||
// For creation/modification events, check if content has actually changed
|
||||
const contentUnchanged =
|
||||
lastSeenContent.position[0] === tag_data.position[0] &&
|
||||
lastSeenContent.position[1] === tag_data.position[1] &&
|
||||
lastSeenContent.text === tag_data.text &&
|
||||
JSON.stringify(lastSeenContent.icon) === JSON.stringify(tag_data.icon) &&
|
||||
lastSeenContent.last_user === tag_data.last_user;
|
||||
lastSeenContent.position[0] === tag_data.position[0]
|
||||
&& lastSeenContent.position[1] === tag_data.position[1]
|
||||
&& lastSeenContent.text === tag_data.text
|
||||
&& JSON.stringify(lastSeenContent.icon) === JSON.stringify(tag_data.icon)
|
||||
&& lastSeenContent.last_user === tag_data.last_user;
|
||||
|
||||
if (contentUnchanged) {
|
||||
return; // Skip if content hasn't changed
|
||||
@@ -652,7 +699,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
// Add instance_id to the tag data for storage
|
||||
const enrichedTagData = {
|
||||
...tag_data,
|
||||
instance_id
|
||||
instance_id,
|
||||
};
|
||||
|
||||
// Immediately update last seen content to improve deduplication before save completes
|
||||
@@ -663,7 +710,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
position: tag_data.position,
|
||||
text: tag_data.text,
|
||||
icon: tag_data.icon,
|
||||
last_user: tag_data.last_user
|
||||
last_user: tag_data.last_user,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -678,7 +725,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
// Send live update to connected web clients
|
||||
const updateEvent = new ChartTagDataEvent(instance_id, tag_data);
|
||||
|
||||
// Broadcast to web clients (overriding the formal src/dst routing)
|
||||
// Broadcast to web clients
|
||||
this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), updateEvent);
|
||||
|
||||
} catch (err) {
|
||||
@@ -716,7 +763,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
chunk
|
||||
);
|
||||
|
||||
// Broadcast to web clients (overriding the formal src/dst routing)
|
||||
// Broadcast to web clients
|
||||
this.controller.sendTo(new lib.Address(lib.Address.broadcast, lib.Address.control), updateEvent);
|
||||
|
||||
} catch (err) {
|
||||
@@ -737,10 +784,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
try {
|
||||
const tileData = await fs.readFile(tilePath);
|
||||
|
||||
const base64Data = tileData.toString('base64');
|
||||
const base64Data = tileData.toString("base64");
|
||||
|
||||
return {
|
||||
tile_data: base64Data
|
||||
tile_data: base64Data,
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.error(`Error reading tile file: ${err}`);
|
||||
@@ -759,8 +806,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = content.trim().split('\n').filter(line => line.trim());
|
||||
const content = await fs.readFile(filePath, "utf-8");
|
||||
const lines = content.trim().split("\n").filter(line => line.trim());
|
||||
|
||||
const chartTags: ChartTagData[] = [];
|
||||
for (const line of lines) {
|
||||
@@ -852,7 +899,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
buffersToWrite = this.encodeSet(tileKey, recipe_data.start_tick ?? 0, px, py, recipe_data.recipe);
|
||||
}
|
||||
|
||||
if (buffersToWrite.length === 0) return;
|
||||
if (buffersToWrite.length === 0) { return; }
|
||||
|
||||
if (!this.recipeSavingQueue.has(tileKey)) {
|
||||
this.recipeSavingQueue.set(tileKey, []);
|
||||
|
||||
+11
-12
@@ -65,11 +65,11 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
async handleTileDataFromLua(data: TileDataIpc) {
|
||||
try {
|
||||
const { type, data: rawData, position, tick } = data;
|
||||
|
||||
|
||||
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;
|
||||
@@ -86,9 +86,9 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
chart
|
||||
));
|
||||
|
||||
return Promise.all(TileDataEvents.map(event => this.instance.sendTo("controller", event)));
|
||||
TileDataEvents.map(event => this.instance.sendTo("controller", event));
|
||||
}
|
||||
|
||||
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to process tile data from Lua: ${err}`);
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
|
||||
async handleChartTagDataFromLua(data: ChartTagDataIpc) {
|
||||
// Validate the chart tag data
|
||||
if (!data || typeof data.tag_number !== 'number') {
|
||||
if (!data || typeof data.tag_number !== "number") {
|
||||
this.logger.error("Invalid chart tag data received");
|
||||
return;
|
||||
}
|
||||
@@ -108,14 +108,14 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
);
|
||||
|
||||
// Send to controller
|
||||
return this.instance.sendTo("controller", chartTagEvent);
|
||||
this.instance.sendTo("controller", chartTagEvent);
|
||||
}
|
||||
|
||||
async handleRecipeDataFromLua(data: RecipeDataIpc) {
|
||||
try {
|
||||
if (!data) {
|
||||
this.logger.error("Invalid recipe data received")
|
||||
return
|
||||
this.logger.error("Invalid recipe data received");
|
||||
return;
|
||||
}
|
||||
|
||||
const recipeData: RecipeData = {
|
||||
@@ -133,8 +133,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
recipeData
|
||||
);
|
||||
|
||||
return this.instance.sendTo("controller", event);
|
||||
|
||||
this.instance.sendTo("controller", event);
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(`Failed to process recipe data from Lua: ${err}`);
|
||||
}
|
||||
@@ -144,7 +143,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
// Initialize the Lua module when the instance starts
|
||||
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
|
||||
@@ -165,4 +164,4 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
// Could handle custom factorio output here if the Lua module sends special messages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
-65
@@ -35,25 +35,25 @@ export const ChartTagDataSchema = Type.Object({
|
||||
export type ChartTagData = Static<typeof ChartTagDataSchema>;
|
||||
|
||||
export const RecipeDataSchema = Type.Object({
|
||||
start_tick: Type.Optional(Type.Number()),
|
||||
end_tick: Type.Optional(Type.Number()),
|
||||
surface: Type.String(),
|
||||
force: Type.String(),
|
||||
position: Type.Tuple([Type.Number(), Type.Number()]),
|
||||
recipe: Type.Optional(Type.String()),
|
||||
icon: Type.Optional(SignalIDSchema),
|
||||
start_tick: Type.Optional(Type.Number()),
|
||||
end_tick: Type.Optional(Type.Number()),
|
||||
surface: Type.String(),
|
||||
force: Type.String(),
|
||||
position: Type.Tuple([Type.Number(), Type.Number()]),
|
||||
recipe: Type.Optional(Type.String()),
|
||||
icon: Type.Optional(SignalIDSchema),
|
||||
});
|
||||
|
||||
export type RecipeData = Static<typeof RecipeDataSchema>;
|
||||
|
||||
/**
|
||||
* TileDataEvent: Used for both Factorio Instance -> Controller and Controller -> Web Clients
|
||||
*
|
||||
*
|
||||
* Contains compressed RGB565 chart data from Factorio's internal mapping system.
|
||||
* When sent from instance to controller: triggers storage in persistent tile files.
|
||||
* When sent from controller to web clients: enables real-time minimap updates.
|
||||
*
|
||||
* Data flow:
|
||||
*
|
||||
* Data flow:
|
||||
* - Lua module -> Instance plugin -> Controller plugin (for persistence)
|
||||
* - Controller plugin -> Web UI plugin -> Canvas renderer (for live updates)
|
||||
*/
|
||||
@@ -100,12 +100,12 @@ export class TileDataEvent {
|
||||
|
||||
/**
|
||||
* ChartTagDataEvent: Used for both Factorio Instance -> Controller and Controller -> Web Clients
|
||||
*
|
||||
*
|
||||
* Contains chart tag information from Factorio's map tags.
|
||||
* When sent from instance to controller: triggers storage for tag history and timelapse.
|
||||
* When sent from controller to web clients: enables real-time tag updates.
|
||||
*
|
||||
* Data flow:
|
||||
*
|
||||
* Data flow:
|
||||
* - Lua module -> Instance plugin -> Controller plugin (for persistence)
|
||||
* - Controller plugin -> Web UI plugin -> Tag renderer (for live updates)
|
||||
*/
|
||||
@@ -136,34 +136,34 @@ export class ChartTagDataEvent {
|
||||
}
|
||||
|
||||
export class RecipeDataEvent {
|
||||
declare ["constructor"]: typeof RecipeDataEvent;
|
||||
static type = "event" as const;
|
||||
static src = ["instance", "controller"] as const;
|
||||
static dst = ["controller", "control"] as const;
|
||||
static plugin = "minimap" as const;
|
||||
static permission = null;
|
||||
declare ["constructor"]: typeof RecipeDataEvent;
|
||||
static type = "event" as const;
|
||||
static src = ["instance", "controller"] as const;
|
||||
static dst = ["controller", "control"] as const;
|
||||
static plugin = "minimap" as const;
|
||||
static permission = null;
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
public instance_id: number,
|
||||
public recipe_data: RecipeData,
|
||||
) {}
|
||||
) {}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"instance_id": Type.Number(),
|
||||
"recipe_data": RecipeDataSchema,
|
||||
});
|
||||
static jsonSchema = Type.Object({
|
||||
"instance_id": Type.Number(),
|
||||
"recipe_data": RecipeDataSchema,
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof RecipeDataEvent.jsonSchema>) {
|
||||
return new this(
|
||||
json.instance_id,
|
||||
json.recipe_data
|
||||
);
|
||||
}
|
||||
static fromJSON(json: Static<typeof RecipeDataEvent.jsonSchema>) {
|
||||
return new this(
|
||||
json.instance_id,
|
||||
json.recipe_data
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GetRawTileRequest: Request raw tile data from controller storage
|
||||
*
|
||||
*
|
||||
* Returns base64-encoded tile data that can be processed in timelapse mode
|
||||
* or for initial tile loading when canvas minimap starts up.
|
||||
*/
|
||||
@@ -187,7 +187,7 @@ export class GetRawTileRequest {
|
||||
static jsonSchema = Type.Object({
|
||||
"instance_id": Type.Number(),
|
||||
"surface": Type.String(),
|
||||
"force": Type.String(),
|
||||
"force": Type.String(),
|
||||
"tile_x": Type.Number(),
|
||||
"tile_y": Type.Number(),
|
||||
"tick": Type.Optional(Type.Number()),
|
||||
@@ -211,7 +211,7 @@ export class GetRawTileRequest {
|
||||
|
||||
/**
|
||||
* GetChartTagsRequest: Request existing chart tags from controller storage
|
||||
*
|
||||
*
|
||||
* Returns all saved chart tags for a specific instance/surface/force combination.
|
||||
* Used to load existing chart tags when the web UI starts up or selection changes.
|
||||
*/
|
||||
@@ -246,46 +246,46 @@ export class GetChartTagsRequest {
|
||||
static Response = lib.plainJson(Type.Object({
|
||||
"chart_tags": Type.Array(ChartTagDataSchema),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export class GetRawRecipeTileRequest {
|
||||
declare ["constructor"]: typeof GetRawRecipeTileRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "minimap" as const;
|
||||
static permission = "minimap.view";
|
||||
declare ["constructor"]: typeof GetRawRecipeTileRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static plugin = "minimap" as const;
|
||||
static permission = "minimap.view";
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
public instance_id: number,
|
||||
public surface: string,
|
||||
public force: string,
|
||||
public tile_x: number,
|
||||
public tile_y: number,
|
||||
public tick?: number,
|
||||
) {}
|
||||
) {}
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"instance_id": Type.Number(),
|
||||
"surface": Type.String(),
|
||||
"force": Type.String(),
|
||||
"tile_x": Type.Number(),
|
||||
"tile_y": Type.Number(),
|
||||
"tick": Type.Optional(Type.Number()),
|
||||
});
|
||||
static jsonSchema = Type.Object({
|
||||
"instance_id": Type.Number(),
|
||||
"surface": Type.String(),
|
||||
"force": Type.String(),
|
||||
"tile_x": Type.Number(),
|
||||
"tile_y": Type.Number(),
|
||||
"tick": Type.Optional(Type.Number()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof GetRawRecipeTileRequest.jsonSchema>) {
|
||||
return new this(
|
||||
json.instance_id,
|
||||
json.surface,
|
||||
json.force,
|
||||
json.tile_x,
|
||||
json.tile_y,
|
||||
json.tick
|
||||
);
|
||||
}
|
||||
static fromJSON(json: Static<typeof GetRawRecipeTileRequest.jsonSchema>) {
|
||||
return new this(
|
||||
json.instance_id,
|
||||
json.surface,
|
||||
json.force,
|
||||
json.tile_x,
|
||||
json.tile_y,
|
||||
json.tick
|
||||
);
|
||||
}
|
||||
|
||||
static Response = lib.plainJson(Type.Object({
|
||||
"recipe_tile": Type.Union([Type.String(), Type.Null()]),
|
||||
}));
|
||||
}
|
||||
static Response = lib.plainJson(Type.Object({
|
||||
"recipe_tile": Type.Union([Type.String(), Type.Null()]),
|
||||
}));
|
||||
}
|
||||
+455
-236
@@ -1,7 +1,7 @@
|
||||
import * as zlib from "zlib";
|
||||
|
||||
// Environment detection
|
||||
const isNode = typeof Buffer !== 'undefined' && typeof process !== 'undefined' && process.versions?.node;
|
||||
const isNode = typeof Buffer !== "undefined" && typeof process !== "undefined" && process.versions?.node;
|
||||
|
||||
// Cross-platform type for binary data
|
||||
type BinaryData = Buffer | Uint8Array;
|
||||
@@ -49,26 +49,41 @@ async function inflateData(data: BinaryData): Promise<Buffer | Uint8Array> {
|
||||
const { promisify } = await import("util");
|
||||
const inflateAsync = promisify(zlib.inflate);
|
||||
return inflateAsync(data as Buffer);
|
||||
} else {
|
||||
// Browser environment - use sync version (it's actually async-friendly due to polyfill)
|
||||
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
||||
return zlib.inflateSync(buffer);
|
||||
}
|
||||
// Browser environment - use promisified inflate
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
||||
zlib.inflate(buffer, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGB565 to RGB888 values using Factorio's exact method
|
||||
* @param rgb565Value The RGB565 color value to convert
|
||||
* @returns Tuple of RGB values [r, g, b]
|
||||
*/
|
||||
export function rgb565ToRgb888(rgb565Value: number): [number, number, number] {
|
||||
// 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
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render chunk data to get RGB565 pixel array
|
||||
* @param chunkData The compressed chunk data to render
|
||||
* @returns RGB565 pixel array for 32x32 chunk
|
||||
*/
|
||||
export async function renderChunkToPixels(chunkData: BinaryData): Promise<Uint16Array> {
|
||||
try {
|
||||
@@ -89,7 +104,6 @@ export async function renderChunkToPixels(chunkData: BinaryData): Promise<Uint16
|
||||
|
||||
return pixels;
|
||||
} catch (decompressError) {
|
||||
console.error(`Failed to decompress chunk data: ${decompressError}`);
|
||||
// Return black pixels on error
|
||||
return new Uint16Array(32 * 32);
|
||||
}
|
||||
@@ -97,6 +111,10 @@ export async function renderChunkToPixels(chunkData: BinaryData): Promise<Uint16
|
||||
|
||||
/**
|
||||
* Extract a 32x32 chunk area from a 256x256 tile
|
||||
* @param tilePixels The full tile pixel data
|
||||
* @param chunkX Chunk X coordinate within tile
|
||||
* @param chunkY Chunk Y coordinate within tile
|
||||
* @returns RGB565 pixel array for 32x32 chunk
|
||||
*/
|
||||
export function extractChunkFromTile(tilePixels: Uint16Array, chunkX: number, chunkY: number): Uint16Array {
|
||||
const chunkPixels = new Uint16Array(32 * 32);
|
||||
@@ -117,6 +135,9 @@ export function extractChunkFromTile(tilePixels: Uint16Array, chunkX: number, ch
|
||||
/**
|
||||
* Render tile data to RGB565 pixel array (256x256)
|
||||
* Supports rendering up to a specific tick for timelapse functionality
|
||||
* @param tileData The binary tile data to render
|
||||
* @param maxTick Optional maximum tick to render up to
|
||||
* @returns RGB565 pixel array for 256x256 tile
|
||||
*/
|
||||
export async function renderTileToPixels(tileData: BinaryData, maxTick?: number): Promise<Uint16Array> {
|
||||
const currentPixels = new Uint16Array(256 * 256); // 256x256 tile
|
||||
@@ -131,89 +152,19 @@ export async function renderTileToPixels(tileData: BinaryData, maxTick?: number)
|
||||
offset += 1;
|
||||
|
||||
if (type === 1) { // Chunk
|
||||
if (offset + 7 > tileData.length) {
|
||||
const chunkResult = await processChunkData(
|
||||
tileData, offset, currentPixels, maxTick
|
||||
);
|
||||
if (!chunkResult.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
offset += 4;
|
||||
|
||||
// Skip this chunk if it's beyond our target tick
|
||||
if (maxTick !== undefined && tick > maxTick) {
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
offset += length; // Skip chunk data
|
||||
continue;
|
||||
}
|
||||
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > tileData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const chunkData = sliceData(tileData, offset, offset + length);
|
||||
|
||||
try {
|
||||
const chunkPixels = await renderChunkToPixels(chunkData);
|
||||
|
||||
// Update current pixel state
|
||||
for (let y = 0; y < 32; y++) {
|
||||
for (let x = 0; x < 32; x++) {
|
||||
const chunkIndex = y * 32 + x;
|
||||
const tileX = chunkX * 32 + x;
|
||||
const tileY = chunkY * 32 + y;
|
||||
const tileIndex = tileY * 256 + tileX;
|
||||
currentPixels[tileIndex] = chunkPixels[chunkIndex];
|
||||
}
|
||||
}
|
||||
} catch (decompressError) {
|
||||
console.error(`Failed to decompress chunk data: ${decompressError}`);
|
||||
}
|
||||
|
||||
offset += length;
|
||||
offset = chunkResult.newOffset;
|
||||
} else if (type === 2) { // Pixels
|
||||
if (offset + 6 > tileData.length) {
|
||||
const pixelResult = processPixelData(tileData, offset, currentPixels, maxTick);
|
||||
if (!pixelResult.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
offset += 4;
|
||||
const pixelCount = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const newColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const oldColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
const expectedDataLength = pixelCount * 2; // x,y per pixel
|
||||
if (offset + expectedDataLength > tileData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip if beyond target tick
|
||||
if (maxTick !== undefined && tick > maxTick) {
|
||||
offset += expectedDataLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const pixelOffset = offset + i * 2;
|
||||
const x = readUInt8(tileData, pixelOffset);
|
||||
const y = readUInt8(tileData, pixelOffset + 1);
|
||||
if (x < 256 && y < 256) {
|
||||
currentPixels[y * 256 + x] = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
offset += expectedDataLength;
|
||||
offset = pixelResult.newOffset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -222,9 +173,140 @@ export async function renderTileToPixels(tileData: BinaryData, maxTick?: number)
|
||||
return currentPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process chunk data during tile rendering
|
||||
* @param tileData The binary tile data being processed
|
||||
* @param offset Current offset in the tile data
|
||||
* @param currentPixels The pixel array to update
|
||||
* @param maxTick Optional maximum tick to render up to
|
||||
* @returns Processing result with success status and new offset
|
||||
*/
|
||||
async function processChunkData(
|
||||
tileData: BinaryData,
|
||||
offset: number,
|
||||
currentPixels: Uint16Array,
|
||||
maxTick?: number
|
||||
): Promise<{ success: boolean; newOffset: number }> {
|
||||
if (offset + 7 > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
offset += 4;
|
||||
|
||||
// Skip this chunk if it's beyond our target tick
|
||||
if (maxTick !== undefined && tick > maxTick) {
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
offset += length; // Skip chunk data
|
||||
return { success: true, newOffset: offset };
|
||||
}
|
||||
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const chunkData = sliceData(tileData, offset, offset + length);
|
||||
|
||||
try {
|
||||
const chunkPixels = await renderChunkToPixels(chunkData);
|
||||
|
||||
// Update current pixel state
|
||||
updateTilePixelsWithChunk(currentPixels, chunkPixels, chunkX, chunkY);
|
||||
} catch (decompressError) {
|
||||
// Continue processing other chunks on error
|
||||
}
|
||||
|
||||
return { success: true, newOffset: offset + length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pixel data during tile rendering
|
||||
* @param tileData The binary tile data being processed
|
||||
* @param offset Current offset in the tile data
|
||||
* @param currentPixels The pixel array to update
|
||||
* @param maxTick Optional maximum tick to render up to
|
||||
* @returns Processing result with success status and new offset
|
||||
*/
|
||||
function processPixelData(
|
||||
tileData: BinaryData,
|
||||
offset: number,
|
||||
currentPixels: Uint16Array,
|
||||
maxTick?: number
|
||||
): { success: boolean; newOffset: number } {
|
||||
if (offset + 6 > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
offset += 4;
|
||||
const pixelCount = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const newColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const oldColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
const expectedDataLength = pixelCount * 2; // x,y per pixel
|
||||
if (offset + expectedDataLength > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
// Skip if beyond target tick
|
||||
if (maxTick !== undefined && tick > maxTick) {
|
||||
return { success: true, newOffset: offset + expectedDataLength };
|
||||
}
|
||||
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const pixelOffset = offset + i * 2;
|
||||
const x = readUInt8(tileData, pixelOffset);
|
||||
const y = readUInt8(tileData, pixelOffset + 1);
|
||||
if (x < 256 && y < 256) {
|
||||
currentPixels[y * 256 + x] = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, newOffset: offset + expectedDataLength };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tile pixels with chunk data
|
||||
* @param currentPixels The tile pixel array to update
|
||||
* @param chunkPixels The chunk pixel data to apply
|
||||
* @param chunkX Chunk X coordinate within tile
|
||||
* @param chunkY Chunk Y coordinate within tile
|
||||
*/
|
||||
function updateTilePixelsWithChunk(
|
||||
currentPixels: Uint16Array,
|
||||
chunkPixels: Uint16Array,
|
||||
chunkX: number,
|
||||
chunkY: number
|
||||
): void {
|
||||
for (let y = 0; y < 32; y++) {
|
||||
for (let x = 0; x < 32; x++) {
|
||||
const chunkIndex = y * 32 + x;
|
||||
const tileX = chunkX * 32 + x;
|
||||
const tileY = chunkY * 32 + y;
|
||||
const tileIndex = tileY * 256 + tileX;
|
||||
currentPixels[tileIndex] = chunkPixels[chunkIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert RGB565 pixel array to RGBA pixel data for canvas rendering
|
||||
* Returns Uint8ClampedArray that can be used to create ImageData in browser
|
||||
* @param pixels The RGB565 pixel array to convert
|
||||
* @returns RGBA pixel data as Uint8ClampedArray
|
||||
*/
|
||||
export function pixelsToRGBA(pixels: Uint16Array): Uint8ClampedArray {
|
||||
const imageData = new Uint8ClampedArray(256 * 256 * 4);
|
||||
@@ -245,10 +327,12 @@ export function pixelsToRGBA(pixels: Uint16Array): Uint8ClampedArray {
|
||||
|
||||
/**
|
||||
* Convert RGB565 pixel array to ImageData for canvas rendering (browser only)
|
||||
* @param pixels The RGB565 pixel array to convert
|
||||
* @returns ImageData object for canvas rendering
|
||||
*/
|
||||
export function pixelsToImageData(pixels: Uint16Array): any {
|
||||
if (typeof (globalThis as any).ImageData === 'undefined') {
|
||||
throw new Error('ImageData is not available in this environment. Use pixelsToRGBA() instead.');
|
||||
if (typeof (globalThis as any).ImageData === "undefined") {
|
||||
throw new Error("ImageData is not available in this environment. Use pixelsToRGBA() instead.");
|
||||
}
|
||||
const rgbaData = pixelsToRGBA(pixels);
|
||||
return new (globalThis as any).ImageData(rgbaData, 256, 256);
|
||||
@@ -256,6 +340,8 @@ export function pixelsToImageData(pixels: Uint16Array): any {
|
||||
|
||||
/**
|
||||
* Parse tile data and extract all available ticks for timelapse functionality
|
||||
* @param tileData The binary tile data to parse
|
||||
* @returns Array of available ticks sorted in ascending order
|
||||
*/
|
||||
export function extractAvailableTicks(tileData: BinaryData): number[] {
|
||||
const ticks = new Set<number>();
|
||||
@@ -347,6 +433,8 @@ export interface ParsedTileData {
|
||||
|
||||
/**
|
||||
* Parse tile data into structured change records for efficient timelapse navigation
|
||||
* @param tileData The binary tile data to parse
|
||||
* @returns Parsed tile data with chunks, pixel changes, and all ticks
|
||||
*/
|
||||
export function parseTileData(tileData: BinaryData): ParsedTileData {
|
||||
const chunks: ChunkRecord[] = [];
|
||||
@@ -363,65 +451,17 @@ export function parseTileData(tileData: BinaryData): ParsedTileData {
|
||||
offset += 1;
|
||||
|
||||
if (type === 1) { // Chunk
|
||||
if (offset + 7 > tileData.length) {
|
||||
const result = parseChunkRecord(tileData, offset, chunks, ticks);
|
||||
if (!result.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
ticks.add(tick);
|
||||
offset += 4;
|
||||
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > tileData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const data = sliceData(tileData, offset, offset + length);
|
||||
|
||||
chunks.push({
|
||||
tick,
|
||||
chunkX,
|
||||
chunkY,
|
||||
data
|
||||
});
|
||||
|
||||
offset += length;
|
||||
offset = result.newOffset;
|
||||
} else if (type === 2) { // Pixels
|
||||
if (offset + 6 > tileData.length) {
|
||||
const result = parsePixelChangeRecord(tileData, offset, pixelChanges, ticks);
|
||||
if (!result.success) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
ticks.add(tick);
|
||||
offset += 4;
|
||||
const pixelCount = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const newColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const oldColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
const expectedDataLength = pixelCount * 2;
|
||||
if (offset + expectedDataLength > tileData.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const changes: PixelChange[] = [];
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const pixelOffset = offset + i * 2;
|
||||
const x = readUInt8(tileData, pixelOffset);
|
||||
const y = readUInt8(tileData, pixelOffset + 1);
|
||||
changes.push({ x, y, newColor, oldColor });
|
||||
}
|
||||
|
||||
pixelChanges.push({ tick, changes });
|
||||
offset += expectedDataLength;
|
||||
offset = result.newOffset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -430,34 +470,118 @@ export function parseTileData(tileData: BinaryData): ParsedTileData {
|
||||
return {
|
||||
chunks,
|
||||
pixelChanges,
|
||||
allTicks: Array.from(ticks).sort((a, b) => a - b)
|
||||
allTicks: Array.from(ticks).sort((a, b) => a - b),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse chunk record from tile data
|
||||
* @param tileData The binary tile data being processed
|
||||
* @param offset Current offset in the tile data
|
||||
* @param chunks Array to add the parsed chunk to
|
||||
* @param ticks Set to add the chunk tick to
|
||||
* @returns Processing result with success status and new offset
|
||||
*/
|
||||
function parseChunkRecord(
|
||||
tileData: BinaryData,
|
||||
offset: number,
|
||||
chunks: ChunkRecord[],
|
||||
ticks: Set<number>
|
||||
): { success: boolean; newOffset: number } {
|
||||
if (offset + 7 > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
ticks.add(tick);
|
||||
offset += 4;
|
||||
|
||||
const chunkCoordsByte = readUInt8(tileData, offset);
|
||||
offset += 1;
|
||||
const length = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const chunkX = chunkCoordsByte >> 4;
|
||||
const chunkY = chunkCoordsByte & 0x0F;
|
||||
const data = sliceData(tileData, offset, offset + length);
|
||||
|
||||
chunks.push({
|
||||
tick,
|
||||
chunkX,
|
||||
chunkY,
|
||||
data,
|
||||
});
|
||||
|
||||
return { success: true, newOffset: offset + length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse pixel change record from tile data
|
||||
* @param tileData The binary tile data being processed
|
||||
* @param offset Current offset in the tile data
|
||||
* @param pixelChanges Array to add the parsed pixel changes to
|
||||
* @param ticks Set to add the pixel change tick to
|
||||
* @returns Processing result with success status and new offset
|
||||
*/
|
||||
function parsePixelChangeRecord(
|
||||
tileData: BinaryData,
|
||||
offset: number,
|
||||
pixelChanges: PixelChangeRecord[],
|
||||
ticks: Set<number>
|
||||
): { success: boolean; newOffset: number } {
|
||||
if (offset + 6 > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const tick = readUInt32BE(tileData, offset) * 60; // Convert back to actual tick
|
||||
ticks.add(tick);
|
||||
offset += 4;
|
||||
const pixelCount = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const newColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
const oldColor = readUInt16BE(tileData, offset);
|
||||
offset += 2;
|
||||
|
||||
const expectedDataLength = pixelCount * 2;
|
||||
if (offset + expectedDataLength > tileData.length) {
|
||||
return { success: false, newOffset: offset };
|
||||
}
|
||||
|
||||
const changes: PixelChange[] = [];
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
const pixelOffset = offset + i * 2;
|
||||
const x = readUInt8(tileData, pixelOffset);
|
||||
const y = readUInt8(tileData, pixelOffset + 1);
|
||||
changes.push({ x, y, newColor, oldColor });
|
||||
}
|
||||
|
||||
pixelChanges.push({ tick, changes });
|
||||
return { success: true, newOffset: offset + expectedDataLength };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply chunk data to RGB565 pixel array
|
||||
* @param pixels The tile pixel array to update
|
||||
* @param chunkRecord The chunk record to apply
|
||||
*/
|
||||
export async function applyChunkToPixels(pixels: Uint16Array, chunkRecord: ChunkRecord): Promise<void> {
|
||||
try {
|
||||
const chunkPixels = await renderChunkToPixels(chunkRecord.data);
|
||||
|
||||
// Update tile pixels with chunk data
|
||||
for (let y = 0; y < 32; y++) {
|
||||
for (let x = 0; x < 32; x++) {
|
||||
const chunkIndex = y * 32 + x;
|
||||
const tileX = chunkRecord.chunkX * 32 + x;
|
||||
const tileY = chunkRecord.chunkY * 32 + y;
|
||||
const tileIndex = tileY * 256 + tileX;
|
||||
pixels[tileIndex] = chunkPixels[chunkIndex];
|
||||
}
|
||||
}
|
||||
updateTilePixelsWithChunk(pixels, chunkPixels, chunkRecord.chunkX, chunkRecord.chunkY);
|
||||
} catch (err) {
|
||||
console.error(`Failed to apply chunk ${chunkRecord.chunkX},${chunkRecord.chunkY}:`, err);
|
||||
// Error applying chunk - continue without this chunk
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear chunk area to black (for rewinding past chunk creation)
|
||||
* @param pixels The tile pixel array to update
|
||||
* @param chunkRecord The chunk record to clear
|
||||
*/
|
||||
export function clearChunkInPixels(pixels: Uint16Array, chunkRecord: ChunkRecord): void {
|
||||
for (let y = 0; y < 32; y++) {
|
||||
@@ -472,6 +596,8 @@ export function clearChunkInPixels(pixels: Uint16Array, chunkRecord: ChunkRecord
|
||||
|
||||
/**
|
||||
* Apply pixel changes to RGB565 pixel array (forward direction)
|
||||
* @param pixels The tile pixel array to update
|
||||
* @param changes Array of pixel changes to apply
|
||||
*/
|
||||
export function applyPixelChanges(pixels: Uint16Array, changes: PixelChange[]): void {
|
||||
for (const change of changes) {
|
||||
@@ -482,6 +608,8 @@ export function applyPixelChanges(pixels: Uint16Array, changes: PixelChange[]):
|
||||
|
||||
/**
|
||||
* Revert pixel changes from RGB565 pixel array (backward direction)
|
||||
* @param pixels The tile pixel array to update
|
||||
* @param changes Array of pixel changes to revert
|
||||
*/
|
||||
export function revertPixelChanges(pixels: Uint16Array, changes: PixelChange[]): void {
|
||||
for (const change of changes) {
|
||||
@@ -493,6 +621,10 @@ export function revertPixelChanges(pixels: Uint16Array, changes: PixelChange[]):
|
||||
/**
|
||||
* Render tile data incrementally from one tick to another
|
||||
* Much more efficient than re-rendering from scratch
|
||||
* @param parsedData The parsed tile data
|
||||
* @param currentPixels The current pixel state
|
||||
* @param fromTick The starting tick
|
||||
* @param toTick The target tick
|
||||
*/
|
||||
export async function renderTileIncremental(
|
||||
parsedData: ParsedTileData,
|
||||
@@ -509,38 +641,70 @@ export async function renderTileIncremental(
|
||||
const forward = toTick > fromTick;
|
||||
|
||||
if (forward) {
|
||||
// Apply changes forward from fromTick to toTick
|
||||
|
||||
// Apply chunks that occur in this range
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.tick > fromTick && chunk.tick <= toTick) {
|
||||
await applyChunkToPixels(currentPixels, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply pixel changes that occur in this range
|
||||
for (const record of pixelChanges) {
|
||||
if (record.tick > fromTick && record.tick <= toTick) {
|
||||
applyPixelChanges(currentPixels, record.changes);
|
||||
}
|
||||
}
|
||||
await applyChangesForward(chunks, pixelChanges, currentPixels, fromTick, toTick);
|
||||
} else {
|
||||
// Revert changes backward from fromTick to toTick
|
||||
await applyChangesBackward(chunks, pixelChanges, currentPixels, fromTick, toTick);
|
||||
}
|
||||
}
|
||||
|
||||
// Revert pixel changes in reverse order
|
||||
for (let i = pixelChanges.length - 1; i >= 0; i--) {
|
||||
const record = pixelChanges[i];
|
||||
if (record.tick <= fromTick && record.tick > toTick) {
|
||||
revertPixelChanges(currentPixels, record.changes);
|
||||
}
|
||||
/**
|
||||
* Apply changes in forward direction
|
||||
* @param chunks Array of chunk records to process
|
||||
* @param pixelChanges Array of pixel change records to process
|
||||
* @param currentPixels The pixel array to update
|
||||
* @param fromTick The starting tick
|
||||
* @param toTick The target tick
|
||||
*/
|
||||
async function applyChangesForward(
|
||||
chunks: ChunkRecord[],
|
||||
pixelChanges: PixelChangeRecord[],
|
||||
currentPixels: Uint16Array,
|
||||
fromTick: number,
|
||||
toTick: number
|
||||
): Promise<void> {
|
||||
// Apply chunks that occur in this range
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.tick > fromTick && chunk.tick <= toTick) {
|
||||
await applyChunkToPixels(currentPixels, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
// Revert chunks (clear to black if we're going before their creation time)
|
||||
for (let i = chunks.length - 1; i >= 0; i--) {
|
||||
const chunk = chunks[i];
|
||||
if (chunk.tick <= fromTick && chunk.tick > toTick) {
|
||||
clearChunkInPixels(currentPixels, chunk);
|
||||
}
|
||||
// Apply pixel changes that occur in this range
|
||||
for (const record of pixelChanges) {
|
||||
if (record.tick > fromTick && record.tick <= toTick) {
|
||||
applyPixelChanges(currentPixels, record.changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes in backward direction
|
||||
* @param chunks Array of chunk records to process
|
||||
* @param pixelChanges Array of pixel change records to process
|
||||
* @param currentPixels The pixel array to update
|
||||
* @param fromTick The starting tick
|
||||
* @param toTick The target tick
|
||||
*/
|
||||
async function applyChangesBackward(
|
||||
chunks: ChunkRecord[],
|
||||
pixelChanges: PixelChangeRecord[],
|
||||
currentPixels: Uint16Array,
|
||||
fromTick: number,
|
||||
toTick: number
|
||||
): Promise<void> {
|
||||
// Revert pixel changes in reverse order
|
||||
for (let i = pixelChanges.length - 1; i >= 0; i--) {
|
||||
const record = pixelChanges[i];
|
||||
if (record.tick <= fromTick && record.tick > toTick) {
|
||||
revertPixelChanges(currentPixels, record.changes);
|
||||
}
|
||||
}
|
||||
|
||||
// Revert chunks (clear to black if we're going before their creation time)
|
||||
for (let i = chunks.length - 1; i >= 0; i--) {
|
||||
const chunk = chunks[i];
|
||||
if (chunk.tick <= fromTick && chunk.tick > toTick) {
|
||||
clearChunkInPixels(currentPixels, chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,12 +721,17 @@ export interface ParsedRecipeTile {
|
||||
/**
|
||||
* Parse binary recipe tile data and return timeline ticks and active recipes at target tick
|
||||
* @param tileX Tile X coordinate for world position calculation
|
||||
* @param tileY Tile Y coordinate for world position calculation
|
||||
* @param tileY Tile Y coordinate for world position calculation
|
||||
* @param buf Binary recipe tile data
|
||||
* @param targetTick Target tick for filtering (null = latest state)
|
||||
* @returns Parsed recipe data with ticks and active recipes
|
||||
*/
|
||||
export function parseRecipeTileBinary(tileX: number, tileY: number, buf: BinaryData, targetTick: number | null): ParsedRecipeTile {
|
||||
export function parseRecipeTileBinary(
|
||||
tileX: number,
|
||||
tileY: number,
|
||||
buf: BinaryData,
|
||||
targetTick: number | null
|
||||
): ParsedRecipeTile {
|
||||
const ticks: number[] = [];
|
||||
const dict = new Map<number, string>();
|
||||
const active = new Map<string, string>();
|
||||
@@ -574,60 +743,110 @@ export function parseRecipeTileBinary(tileX: number, tileY: number, buf: BinaryD
|
||||
offset += 1;
|
||||
|
||||
if (type === 0) { // Dictionary entry
|
||||
const idx = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
const len = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
const nameBytes = sliceData(buf, offset, offset + len);
|
||||
offset += len;
|
||||
|
||||
// Convert to string using cross-platform method
|
||||
let recipeName: string;
|
||||
if (isNode && nameBytes instanceof Buffer) {
|
||||
recipeName = nameBytes.toString('utf-8');
|
||||
} else {
|
||||
recipeName = new TextDecoder().decode(nameBytes as Uint8Array);
|
||||
}
|
||||
|
||||
dict.set(idx, recipeName);
|
||||
const result = parseDictionaryEntry(buf, offset, dict);
|
||||
offset = result.newOffset;
|
||||
} else if (type === 1 || type === 2) { // Set recipe (1) or Clear recipe (2)
|
||||
const sec = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
const px = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
const py = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
|
||||
let idx: number | undefined;
|
||||
if (type === 1) {
|
||||
idx = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
const tickVal = sec * 60;
|
||||
ticks.push(tickVal);
|
||||
|
||||
if (sec > targetSec) {
|
||||
// Future to target time – we can skip rest if file is chronological
|
||||
// but to be safe just continue without applying
|
||||
continue;
|
||||
}
|
||||
|
||||
const worldX = tileX * 256 + px;
|
||||
const worldY = tileY * 256 + py;
|
||||
const posKey = `${worldX},${worldY}`;
|
||||
|
||||
if (type === 1 && idx !== undefined) {
|
||||
const name = dict.get(idx);
|
||||
if (name) active.set(posKey, name);
|
||||
} else if (type === 2) {
|
||||
active.delete(posKey);
|
||||
}
|
||||
const result = parseRecipeEntry(
|
||||
buf, offset, type, tileX, tileY, ticks, active, targetSec, dict
|
||||
);
|
||||
offset = result.newOffset;
|
||||
} else {
|
||||
console.warn("Unknown recipe record type", type);
|
||||
// Unknown recipe record type - stop parsing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { ticks, activeRecipes: active, dictionary: dict };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dictionary entry from recipe tile
|
||||
* @param buf The binary recipe tile data
|
||||
* @param offset Current offset in the data
|
||||
* @param dict Dictionary map to add the entry to
|
||||
* @returns Processing result with new offset
|
||||
*/
|
||||
function parseDictionaryEntry(
|
||||
buf: BinaryData,
|
||||
offset: number,
|
||||
dict: Map<number, string>
|
||||
): { newOffset: number } {
|
||||
const idx = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
const len = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
const nameBytes = sliceData(buf, offset, offset + len);
|
||||
offset += len;
|
||||
|
||||
// Convert to string using cross-platform method
|
||||
let recipeName: string;
|
||||
if (isNode && nameBytes instanceof Buffer) {
|
||||
recipeName = nameBytes.toString("utf-8");
|
||||
} else {
|
||||
recipeName = new TextDecoder().decode(nameBytes as Uint8Array);
|
||||
}
|
||||
|
||||
dict.set(idx, recipeName);
|
||||
return { newOffset: offset };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse recipe entry from recipe tile
|
||||
* @param buf The binary recipe tile data
|
||||
* @param offset Current offset in the data
|
||||
* @param type Entry type (1 = set recipe, 2 = clear recipe)
|
||||
* @param tileX Tile X coordinate for world position calculation
|
||||
* @param tileY Tile Y coordinate for world position calculation
|
||||
* @param ticks Array to add the entry tick to
|
||||
* @param active Map of active recipes to update
|
||||
* @param targetSec Target second for filtering
|
||||
* @param dict Dictionary map for recipe name lookup
|
||||
* @returns Processing result with new offset
|
||||
*/
|
||||
function parseRecipeEntry(
|
||||
buf: BinaryData,
|
||||
offset: number,
|
||||
type: number,
|
||||
tileX: number,
|
||||
tileY: number,
|
||||
ticks: number[],
|
||||
active: Map<string, string>,
|
||||
targetSec: number,
|
||||
dict: Map<number, string>
|
||||
): { newOffset: number } {
|
||||
const sec = readUInt32BE(buf, offset);
|
||||
offset += 4;
|
||||
const px = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
const py = readUInt8(buf, offset);
|
||||
offset += 1;
|
||||
|
||||
let idx: number | undefined;
|
||||
if (type === 1) {
|
||||
idx = readUInt16BE(buf, offset);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
const tickVal = sec * 60;
|
||||
ticks.push(tickVal);
|
||||
|
||||
if (sec > targetSec) {
|
||||
// Future to target time – skip without applying
|
||||
return { newOffset: offset };
|
||||
}
|
||||
|
||||
const worldX = tileX * 256 + px;
|
||||
const worldY = tileY * 256 + py;
|
||||
const posKey = `${worldX},${worldY}`;
|
||||
|
||||
if (type === 1 && idx !== undefined) {
|
||||
const name = dict.get(idx);
|
||||
if (name) {
|
||||
active.set(posKey, name);
|
||||
}
|
||||
} else if (type === 2) {
|
||||
active.delete(posKey);
|
||||
}
|
||||
|
||||
return { newOffset: offset };
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -38,21 +38,16 @@ export default function MinimapPage() {
|
||||
}, []);
|
||||
|
||||
const loadSurfaceForceData = async () => {
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/api/minimap/surfaces`);
|
||||
console.log(response);
|
||||
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]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load surface/force data:", err);
|
||||
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]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,8 +61,8 @@ export default function MinimapPage() {
|
||||
<div style={{ padding: "20px" }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title="Factorio Instance Minimap"
|
||||
<Card
|
||||
title="Factorio Instance Minimap"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Select
|
||||
@@ -78,7 +73,7 @@ export default function MinimapPage() {
|
||||
loading={loading}
|
||||
options={[...instances.values()].map(instance => ({
|
||||
value: instance.id,
|
||||
label: instance.name
|
||||
label: instance.name,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
@@ -109,10 +104,10 @@ export default function MinimapPage() {
|
||||
}
|
||||
>
|
||||
{selectedInstance && mapBounds ? (
|
||||
<div
|
||||
style={{
|
||||
height: "700px",
|
||||
width: "100%"
|
||||
<div
|
||||
style={{
|
||||
height: "700px",
|
||||
width: "100%",
|
||||
}}
|
||||
className="minimap-container"
|
||||
>
|
||||
@@ -137,6 +132,7 @@ export default function MinimapPage() {
|
||||
>
|
||||
{/* 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}
|
||||
@@ -146,11 +142,11 @@ export default function MinimapPage() {
|
||||
</MapContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: "400px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
<div
|
||||
style={{
|
||||
height: "400px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f5f5f5",
|
||||
border: "1px dashed #d9d9d9",
|
||||
@@ -168,4 +164,4 @@ export default function MinimapPage() {
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,12 @@ export class WebPlugin extends BaseWebPlugin {
|
||||
this.pages = [
|
||||
{
|
||||
path: "/minimap",
|
||||
sidebarName: "Minimap",
|
||||
sidebarName: "Minimap",
|
||||
permission: "minimap.view",
|
||||
content: <CanvasMinimapPage />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
// Handle tile update events from the controller
|
||||
this.control.handle(TileDataEvent, this.handleTileDataEvent.bind(this));
|
||||
this.control.handle(ChartTagDataEvent, this.handleChartTagDataEvent.bind(this));
|
||||
@@ -79,4 +79,4 @@ export class WebPlugin extends BaseWebPlugin {
|
||||
}
|
||||
this.recipeUpdateCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user