mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 05:54:36 +00:00
preprocess map into binary data
This commit is contained in:
+11
-13
@@ -17,7 +17,7 @@ class Client {
|
||||
private hasJoined = false
|
||||
|
||||
private socket: WebSocket | null = null;
|
||||
private terrainMap: Promise<TerrainMap>
|
||||
private terrainMap: TerrainMap
|
||||
private game: ClientGame
|
||||
|
||||
private lobbiesContainer: HTMLElement | null;
|
||||
@@ -115,18 +115,16 @@ class Client {
|
||||
this.lobbiesContainer.appendChild(joiningMessage);
|
||||
}
|
||||
|
||||
this.terrainMap.then((map) => {
|
||||
if (this.game != null) {
|
||||
return;
|
||||
}
|
||||
this.game = createClientGame(getUsername(), new PseudoRandom(Date.now()).nextID(), lobby.id, getConfig(), map);
|
||||
this.game.join();
|
||||
const g = this.game
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
// Your function logic here
|
||||
console.log('Browser is closing');
|
||||
g.stop()
|
||||
});
|
||||
if (this.game != null) {
|
||||
return;
|
||||
}
|
||||
this.game = createClientGame(getUsername(), new PseudoRandom(Date.now()).nextID(), lobby.id, getConfig(), this.terrainMap);
|
||||
this.game.join();
|
||||
const g = this.game
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
// Your function logic here
|
||||
console.log('Browser is closing');
|
||||
g.stop()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class ClientGame {
|
||||
|
||||
this.renderer.initialize()
|
||||
this.input.initialize()
|
||||
this.gs.addExecution(...this.executor.spawnBots(this.config.numBots()))
|
||||
//this.gs.addExecution(...this.executor.spawnBots(this.config.numBots()))
|
||||
|
||||
this.intervalID = setInterval(() => this.tick(), 10);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {Jimp as JimpType, JimpConstructors} from '@jimp/core';
|
||||
import 'jimp';
|
||||
import {TerrainTile} from '../../generated/protos';
|
||||
import {Cell} from './Game';
|
||||
|
||||
declare const Jimp: JimpType & JimpConstructors;
|
||||
|
||||
import binAsString from "!!binary-loader!../../resources/WorldSmall.bin";
|
||||
|
||||
export class TerrainMap {
|
||||
|
||||
constructor(public readonly tiles: Terrain[][]) { }
|
||||
|
||||
terrain(cell: Cell): Terrain {
|
||||
@@ -30,64 +24,56 @@ export enum TerrainType {
|
||||
|
||||
export class Terrain {
|
||||
public shoreline: boolean = false
|
||||
public magnitude: number = 0
|
||||
constructor(public type: TerrainType) { }
|
||||
}
|
||||
|
||||
export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
const imageModule = await import(`../../resources/maps/World.png`);
|
||||
const imageUrl = imageModule.default;
|
||||
const image = await Jimp.read(imageUrl)
|
||||
const {width, height} = image.bitmap;
|
||||
export function loadTerrainMap(): TerrainMap {
|
||||
const fileData = binAsString;
|
||||
|
||||
console.log(`Loaded data length: ${fileData.length} bytes`);
|
||||
|
||||
// Extract width and height from the first 4 bytes
|
||||
const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0);
|
||||
const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2);
|
||||
|
||||
console.log(`Decoded dimensions: ${width}x${height}`);
|
||||
|
||||
// Log the first 100 bytes of data (including the width and height bytes)
|
||||
logBinaryAsAscii(fileData, 100);
|
||||
|
||||
// Check if the data length matches the expected size
|
||||
if (fileData.length != width * height + 4) { // +4 for the width and height bytes
|
||||
throw new Error(`Invalid data: buffer size ${fileData.length} incorrect for ${width}x${height} terrain plus 4 bytes for dimensions.`);
|
||||
}
|
||||
|
||||
const terrain: Terrain[][] = Array(width).fill(null).map(() => Array(height).fill(null));
|
||||
|
||||
image.scan(0, 0, width, height, function (x: number, y: number, idx: number) {
|
||||
const t: TerrainTile = new TerrainTile()
|
||||
const red = this.bitmap.data[idx + 0];
|
||||
// Start from the 5th byte (index 4) when processing terrain data
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const packedByte = fileData.charCodeAt(4 + y * width + x); // +4 to skip dimension bytes
|
||||
const type = (packedByte & 0b10000000) ? TerrainType.Land : TerrainType.Water;
|
||||
const shoreline = !!(packedByte & 0b01000000);
|
||||
const magnitude = packedByte & 0b00111111;
|
||||
|
||||
if (red > 100) {
|
||||
terrain[x][y] = new Terrain(TerrainType.Land)
|
||||
} else {
|
||||
terrain[x][y] = new Terrain(TerrainType.Water);
|
||||
terrain[x][y] = new Terrain(type);
|
||||
terrain[x][y].shoreline = shoreline;
|
||||
terrain[x][y].magnitude = magnitude;
|
||||
}
|
||||
})
|
||||
|
||||
process(terrain)
|
||||
}
|
||||
|
||||
return new TerrainMap(terrain);
|
||||
}
|
||||
|
||||
function process(map: Terrain[][]) {
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
const terrain = map[x][y]
|
||||
const ns = neighbors(x, y, map)
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
if (ns.filter(t => t.type == TerrainType.Water).length > 0) {
|
||||
terrain.shoreline = true
|
||||
}
|
||||
} else {
|
||||
if (ns.filter(t => t.type == TerrainType.Land).length > 0) {
|
||||
terrain.shoreline = true
|
||||
}
|
||||
}
|
||||
function logBinaryAsAscii(data: string, length: number = 8) {
|
||||
console.log('Binary data (1 = set bit, 0 = unset bit):');
|
||||
for (let i = 0; i < Math.min(length, data.length); i++) {
|
||||
let byte = data.charCodeAt(i);
|
||||
let byteString = '';
|
||||
for (let j = 7; j >= 0; j--) {
|
||||
byteString += (byte & (1 << j)) ? '1' : '0';
|
||||
}
|
||||
console.log(`Byte ${i}: ${byteString}`);
|
||||
}
|
||||
}
|
||||
|
||||
function neighbors(x: number, y: number, map: Terrain[][]): Terrain[] {
|
||||
const ns: Terrain[] = []
|
||||
if (x > 0) {
|
||||
ns.push(map[x - 1][y])
|
||||
}
|
||||
if (x < map.length - 1) {
|
||||
ns.push(map[x + 1][y])
|
||||
}
|
||||
if (y > 0) {
|
||||
ns.push(map[x][y - 1])
|
||||
}
|
||||
if (y < map[0].length - 1) {
|
||||
ns.push(map[x][y + 1])
|
||||
}
|
||||
return ns
|
||||
}
|
||||
Vendored
+5
-1
@@ -1,5 +1,5 @@
|
||||
declare module '*.png' {
|
||||
const content: string;
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
@@ -15,3 +15,7 @@ declare module '*.svg' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.bin' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -1,121 +1,182 @@
|
||||
import PImage from 'pureimage';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import fs from 'fs/promises';
|
||||
import {createReadStream, createWriteStream} from 'fs';
|
||||
import zlib from 'zlib';
|
||||
import {promisify} from 'util';
|
||||
import {fileURLToPath} from 'url';
|
||||
|
||||
const deflateRaw = promisify(zlib.deflateRaw);
|
||||
const inflateRaw = promisify(zlib.inflateRaw);
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
interface TerrainMap {
|
||||
width: number;
|
||||
height: number;
|
||||
terrain: TerrainTile[][];
|
||||
|
||||
|
||||
interface Coord {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface TerrainTile {
|
||||
isLand: boolean
|
||||
export class TerrainMap {
|
||||
constructor(public readonly tiles: Terrain[][]) { }
|
||||
|
||||
terrain(coord: Coord): Terrain {
|
||||
return this.tiles[coord.x][coord.y]
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.tiles.length
|
||||
}
|
||||
|
||||
height(): number {
|
||||
return this.tiles[0].length
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadTerrainMap(): Promise<TerrainMap> {
|
||||
try {
|
||||
const imagePath = path.resolve(__dirname, '..', '..', 'resources', 'maps', 'World.png');
|
||||
console.log('Attempting to load image from:', imagePath);
|
||||
export enum TerrainType {
|
||||
Land,
|
||||
Water
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(imagePath);
|
||||
} catch (error) {
|
||||
throw new Error(`Image file not found at ${imagePath}. Please ensure the file exists.`);
|
||||
}
|
||||
export class Terrain {
|
||||
public shoreline: boolean = false
|
||||
public magnitude: number = 0
|
||||
constructor(public type: TerrainType) { }
|
||||
}
|
||||
|
||||
const readStream = createReadStream(imagePath);
|
||||
const img = await PImage.decodePNGFromStream(readStream);
|
||||
export async function loadTerrainMap(): Promise<void> {
|
||||
const imagePath = path.resolve(__dirname, '..', '..', 'resources', 'maps', 'WorldSmall.png');
|
||||
|
||||
console.log('Image loaded successfully');
|
||||
console.log('Image dimensions:', img.width, 'x', img.height);
|
||||
const readStream = createReadStream(imagePath);
|
||||
const img = await PImage.decodePNGFromStream(readStream);
|
||||
|
||||
const terrainMap: TerrainMap = {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
terrain: []
|
||||
};
|
||||
console.log('Image loaded successfully');
|
||||
console.log('Image dimensions:', img.width, 'x', img.height);
|
||||
|
||||
// Iterate through each pixel
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
terrainMap.terrain[x] = [];
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const color = img.getPixelRGBA(x, y);
|
||||
const red = (color >> 24) & 0xff;
|
||||
// Extract the red channel (assuming it represents height)
|
||||
const height = (color >> 24) & 0xff;
|
||||
terrainMap.terrain[x][y] = {
|
||||
isLand: red > 100
|
||||
};
|
||||
const terrain: Terrain[][] = Array(img.width).fill(null).map(() => Array(img.height).fill(null));
|
||||
|
||||
// Iterate through each pixel
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
const color = img.getPixelRGBA(x, y);
|
||||
const red = (color >> 24) & 0xff;
|
||||
|
||||
if (red > 100) {
|
||||
terrain[x][y] = new Terrain(TerrainType.Land)
|
||||
} else {
|
||||
terrain[x][y] = new Terrain(TerrainType.Water);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Terrain data extracted');
|
||||
}
|
||||
|
||||
|
||||
// Serialize the terrain data using MessagePack
|
||||
const msg = JSON.stringify(terrainMap)
|
||||
const compressedData = await deflateRaw(msg);
|
||||
const shorelineWaters = processShore(terrain)
|
||||
processDistToLand(shorelineWaters, terrain)
|
||||
const packed = packTerrain(terrain)
|
||||
const outputPath = path.join(__dirname, '..', '..', 'resources', 'WorldSmall.bin');
|
||||
fs.writeFile(outputPath, packed);
|
||||
}
|
||||
|
||||
function processShore(map: Terrain[][]): Coord[] {
|
||||
const shorelineWaters: Coord[] = []
|
||||
for (let x = 0; x < map.length; x++) {
|
||||
for (let y = 0; y < map[0].length; y++) {
|
||||
const terrain = map[x][y]
|
||||
const ns = neighbors(x, y, map)
|
||||
if (terrain.type == TerrainType.Land) {
|
||||
if (ns.filter(t => t.type == TerrainType.Water).length > 0) {
|
||||
terrain.shoreline = true
|
||||
}
|
||||
} else {
|
||||
if (ns.filter(t => t.type == TerrainType.Land).length > 0) {
|
||||
terrain.shoreline = true
|
||||
shorelineWaters.push({x, y})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return shorelineWaters
|
||||
}
|
||||
|
||||
function processDistToLand(shorelineWaters: Coord[], map: Terrain[][]) {
|
||||
const queue: [Coord, number][] = shorelineWaters.map(coord => [coord, 0]);
|
||||
const visited = new Set<string>();
|
||||
|
||||
// Save the serialized data
|
||||
const outputPath = path.join(__dirname, 'terrain_data.msgpack');
|
||||
fs.writeFile(outputPath, compressedData);
|
||||
console.log('Serialized terrain data saved to:', outputPath);
|
||||
while (queue.length > 0) {
|
||||
const [coord, distance] = queue.shift()!;
|
||||
const key = `${coord.x},${coord.y}`;
|
||||
|
||||
return terrainMap
|
||||
if (visited.has(key)) continue;
|
||||
visited.add(key);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading or processing the terrain map:', error);
|
||||
throw error;
|
||||
const terrain = map[coord.x][coord.y];
|
||||
if (terrain.type === TerrainType.Water) {
|
||||
terrain.magnitude = distance;
|
||||
|
||||
for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
|
||||
const newX = coord.x + dx;
|
||||
const newY = coord.y + dy;
|
||||
|
||||
if (newX >= 0 && newX < map.length && newY >= 0 && newY < map[0].length) {
|
||||
queue.push([{x: newX, y: newY}, distance + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test the function
|
||||
loadTerrainMap().then(terrainData => {
|
||||
console.log('Terrain data loaded');
|
||||
console.log('Terrain data extracted');
|
||||
}).catch(console.error);
|
||||
|
||||
console.log('Processing terrain map...');
|
||||
|
||||
|
||||
async function loadAndDecodeTerrainData(): Promise<TerrainMap> {
|
||||
try {
|
||||
// Construct the path to the MessagePack file
|
||||
const filePath = path.join(__dirname, 'terrain_data.msgpack');
|
||||
|
||||
// Read the file
|
||||
const data = await fs.readFile(filePath);
|
||||
|
||||
const inflated = await inflateRaw(data)
|
||||
const decodedData = JSON.parse(inflated.toString('utf-8')) as TerrainMap
|
||||
|
||||
console.log('Terrain data loaded and decoded successfully');
|
||||
console.log('Dimensions:', decodedData.width, 'x', decodedData.height);
|
||||
console.log('Sample height at (0,0):', decodedData.terrain[0][0]);
|
||||
|
||||
return decodedData;
|
||||
} catch (error) {
|
||||
console.error('Error loading or decoding the terrain data:', error);
|
||||
throw error;
|
||||
function neighbors(x: number, y: number, map: Terrain[][]): Terrain[] {
|
||||
const ns: Terrain[] = []
|
||||
if (x > 0) {
|
||||
ns.push(map[x - 1][y])
|
||||
}
|
||||
if (x < map.length - 1) {
|
||||
ns.push(map[x + 1][y])
|
||||
}
|
||||
if (y > 0) {
|
||||
ns.push(map[x][y - 1])
|
||||
}
|
||||
if (y < map[0].length - 1) {
|
||||
ns.push(map[x][y + 1])
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// Usage example
|
||||
loadAndDecodeTerrainData()
|
||||
.then(terrainData => {
|
||||
// You can now use terrainData in your application
|
||||
console.log('Terrain data ready for use');
|
||||
})
|
||||
.catch(console.error);
|
||||
function packTerrain(map: Terrain[][]): Uint8Array {
|
||||
const width = map.length;
|
||||
const height = map[0].length;
|
||||
const packedData = new Uint8Array(4 + width * height);
|
||||
|
||||
// Add width and height to the first 4 bytes
|
||||
packedData[0] = width & 0xFF;
|
||||
packedData[1] = (width >> 8) & 0xFF;
|
||||
packedData[2] = height & 0xFF;
|
||||
packedData[3] = (height >> 8) & 0xFF;
|
||||
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const terrain = map[x][y];
|
||||
let packedByte = 0;
|
||||
|
||||
if (terrain.type === TerrainType.Land) {
|
||||
packedByte |= 0b10000000;
|
||||
}
|
||||
if (terrain.shoreline) {
|
||||
packedByte |= 0b01000000;
|
||||
}
|
||||
packedByte |= Math.min(terrain.magnitude, 63);
|
||||
|
||||
packedData[4 + y * width + x] = packedByte;
|
||||
}
|
||||
}
|
||||
logBinaryAsBits(packedData)
|
||||
return packedData;
|
||||
}
|
||||
|
||||
function logBinaryAsBits(data: Uint8Array, length: number = 8) {
|
||||
const bits = Array.from(data.slice(0, length))
|
||||
.map(b => b.toString(2).padStart(8, '0'))
|
||||
.join(' ');
|
||||
console.log('Binary data (bits):', bits);
|
||||
}
|
||||
|
||||
await loadTerrainMap()
|
||||
Binary file not shown.
Reference in New Issue
Block a user