@aresrpg/aresrpg-sdk
Version:
General SDK to interract with AresRPG
265 lines (217 loc) • 6.98 kB
JavaScript
import { Buffer } from 'buffer'
import spiral from 'spiralloop'
export const CHUNK_SIZE = 64
export function to_chunk_position(position) {
const x = Math.floor(position.x / CHUNK_SIZE)
const z = Math.floor(position.z / CHUNK_SIZE)
return { x, z }
}
export function spiral_array(center, min_distance, max_distance) {
const positions = []
// Determine the size of the spiral needed based on maxDistance
const size = max_distance * 2 + 1
spiral([size, size], function (x, z) {
// Adjust x and z to be relative to the center
const adjusted_x = x - max_distance + center.x
const adjusted_z = z - max_distance + center.z
// Calculate Manhattan distance from the center
const distance =
Math.abs(center.x - adjusted_x) + Math.abs(center.z - adjusted_z)
// Include positions within the specified distance range
if (distance >= min_distance && distance <= max_distance) {
positions.push({ x: adjusted_x, z: adjusted_z })
}
})
return positions
}
export function square_array(center, max_distance) {
const positions = []
for (let x = center.x - max_distance; x <= center.x + max_distance; x++) {
for (let z = center.z - max_distance; z <= center.z + max_distance; z++) {
positions.push({ x, z })
}
}
return positions
}
/**
* @typedef {Object} Box3
* @property {boolean} isBox3
* @property {{x: number, y: number, z: number}} min
* @property {{x: number, y: number, z: number}} max
*/
/**
* @typedef {Object} Vector2
* @property {number} x
* @property {number} z
*/
/**
* @typedef {Object} ChunkMetadata
* @property {string} [chunkKey]
* @property {Box3} bounds
* @property {number} [margin]
* @property {boolean} [isEmpty]
*/
/**
* @typedef {Object} Chunk
* @property {ChunkMetadata} metadata
* @property {Uint16Array} rawdata
*/
/**
* @typedef {Object} ChunkBuffer
* @property {Vector2} pos
* @property {Uint16Array} content
*/
/**
* Optimized run-length encoding using a single TypedArray and minimal allocations
* @param {Uint16Array} raw_data
* @returns {Uint16Array}
*/
function encode_run_length(raw_data) {
if (raw_data.length === 0) return new Uint16Array()
// Pre-allocate worst case size (each value different from previous)
const result = new Uint16Array(raw_data.length * 2)
let write_index = 0
let [current_value] = raw_data
let count = 1
for (let i = 1; i < raw_data.length; i++) {
if (raw_data[i] === current_value && count < 65535) {
count++
continue
}
result[write_index++] = count
result[write_index++] = current_value
current_value = raw_data[i]
count = 1
}
// Write final run
result[write_index++] = count
result[write_index++] = current_value
return new Uint16Array(result.buffer, 0, write_index)
}
/**
* Creates an optimized metadata buffer for the chunks
* @param {Chunk[]} chunks
* @returns {Uint8Array}
*/
function create_metadata_buffer(chunks) {
const metadata = chunks.map(chunk => ({
k: chunk.metadata.chunkKey,
b: { n: chunk.metadata.bounds.min, x: chunk.metadata.bounds.max },
m: chunk.metadata.margin,
l: chunk.rawdata?.length || 0,
}))
return new TextEncoder().encode(JSON.stringify(metadata))
}
/**
* Combines chunk data into a single Uint16Array efficiently
* @param {Chunk[]} chunks
* @param {number} total_length
* @returns {Uint16Array}
*/
function combine_chunks(chunks, total_length) {
const combined = new Uint16Array(total_length)
let offset = 0
for (const chunk of chunks) {
combined.set(chunk.rawdata ?? [], offset)
offset += chunk.rawdata?.length ?? 0
}
return combined
}
/**
* Compresses a column of chunks with optimized memory usage
* @param {Chunk[]} chunks
* @returns {Promise<string>}
*/
export async function compress_chunk_column(chunks) {
if (chunks.length === 0) return ''
// Sum total length in a single pass
const total_length = chunks.reduce(
(sum, chunk) => sum + (chunk.rawdata?.length || 0),
0,
)
// Create metadata and combine chunks
const metadata_buffer = create_metadata_buffer(chunks)
const combined_data = combine_chunks(chunks, total_length)
// Run length encode the combined data
const rle_data = encode_run_length(combined_data)
// Create the final buffer in a single concatenation
const final_buffer = Buffer.concat([
Buffer.from(new Uint32Array([metadata_buffer.length]).buffer),
metadata_buffer,
Buffer.from(rle_data.buffer, 0, rle_data.byteLength),
])
// Compress in one pass
const compressed = await new Response(
new Blob([final_buffer])
.stream()
.pipeThrough(new CompressionStream('gzip')),
).arrayBuffer()
return Buffer.from(compressed).toString('base64')
}
/**
* Optimized run-length decoding using pre-allocated buffer
* @param {Uint16Array} encoded_data
* @returns {Uint16Array}
*/
function decode_run_length(encoded_data) {
if (encoded_data.length === 0) return new Uint16Array()
// Calculate total length in a single pass
let total_length = 0
for (let i = 0; i < encoded_data.length; i += 2) {
total_length += encoded_data[i]
}
// Pre-allocate final array
const result = new Uint16Array(total_length)
let write_index = 0
// Decode in a single pass without intermediate arrays
for (let i = 0; i < encoded_data.length; i += 2) {
const count = encoded_data[i]
const value = encoded_data[i + 1]
result.fill(value, write_index, write_index + count)
write_index += count
}
return result
}
/**
* Decompresses a base64 encoded chunk column back into an array of chunks
* @param {string} compressed_payload
* @returns {Promise<Chunk[]>}
*/
export async function decompress_chunk_column(compressed_payload) {
if (!compressed_payload) return []
// Decompress the gzipped data
const compressed_buffer = Buffer.from(compressed_payload, 'base64')
const decompressed_buffer = await new Response(
new Blob([compressed_buffer])
.stream()
.pipeThrough(new DecompressionStream('gzip')),
).arrayBuffer()
const decompressed_data = new Uint8Array(decompressed_buffer)
// Extract metadata length and content
const [metadata_length] = new Uint32Array(decompressed_buffer, 0, 1)
const metadata_json = new TextDecoder().decode(
decompressed_data.slice(4, 4 + metadata_length),
)
const metadata_list = JSON.parse(metadata_json)
// Get the RLE encoded data
const data_buffer = decompressed_data.slice(4 + metadata_length)
const combined_data = decode_run_length(new Uint16Array(data_buffer.buffer))
// Reconstruct chunks
let offset = 0
return metadata_list.map(meta => {
const chunk_data = combined_data.slice(offset, offset + meta.l)
offset += meta.l
return {
rawdata: chunk_data,
metadata: {
chunkKey: meta.k,
bounds: {
isBox3: true,
min: meta.b.n,
max: meta.b.x,
},
margin: meta.m,
},
}
})
}