import { on } from 'events' import { performance } from 'perf_hooks' import { aiter } from 'iterator-helper' import { chunk_position, same_chunk } from '../chunk.js' import { position_equal } from '../position.js' import { sort_by_distance2d, square_difference, square_symmetric_difference, } from '../math.js' import { PLAYER_ENTITY_ID } from '../settings.js' import { abortable } from '../iterator.js' import { Action, Context } from '../events.js' function fix_light(chunk) { for (let x = 0; x < 16; x++) { for (let z = 0; z < 16; z++) { for (let y = 0; y < 256; y++) { chunk.setSkyLight({ x, y, z }, 15) chunk.setBlockLight({ x, y, z }, 15) } } } } async function load_chunk({ client, world, x, z }) { performance.mark('load_chunk_start') const chunk = await world.chunks.load(x, z) fix_light(chunk) // TODO: replace this with a proper fix client.write('update_light', { chunkX: x, chunkZ: z, trustEdges: true, skyLightMask: chunk.skyLightMask, blockLightMask: chunk.blockLightMask, emptySkyLightMask: 0, emptyBlockLightMask: 0, data: chunk.dumpLight(), }) client.write('map_chunk', { x, z, groundUp: true, bitMap: chunk.getMask(), biomes: chunk.dumpBiomes(), ignoreOldData: true, heightmaps: { type: 'compound', name: '', value: { MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([256, 256]), }, }, }, // FIXME: fake heightmap chunkData: chunk.dump(), blockEntities: [], }) performance.mark('load_chunk_end') performance.measure('load_chunk', 'load_chunk_start', 'load_chunk_end') } function unload_chunk({ client, x, z }) { client.write('unload_chunk', { chunkX: x, chunkZ: z, }) } function unload_signal({ events, x, z }) { const controller = new AbortController() aiter(on(events, Context.CHUNK_UNLOADED)) .filter(([chunk]) => chunk.x === x && chunk.z === z) .take(0) // TODO: should be 1, seems to be a iterator-helper bug .toArray() .then(() => controller.abort()) return controller.signal } export async function load_chunks(state, { client, events, world, chunks }) { const points = chunks.map(({ x, z }) => ({ x, y: z })) const sorted = sort_by_distance2d( { x: chunk_position(state.position.x), y: chunk_position(state.position.z), }, points ) for (const { x, y } of sorted) { await load_chunk({ client, world, x, z: y }) events.emit(Context.CHUNK_LOADED, { x, z: y, signal: unload_signal({ events, x, z: y }), }) // Loading one chunk is cpu intensive, wait for next tick to avoid // starving the event loop for too long await new Promise(resolve => process.nextTick(resolve)) } } export function unload_chunks(state, { client, events, chunks, world }) { for (const chunk of chunks) { events.emit(Context.CHUNK_UNLOADED, chunk) unload_chunk({ client, world, ...chunk }) } } export default { /** @type {import('../context.js').Reducer} */ reduce(state, { type, payload }) { if (type === Action.TELEPORT) { return { ...state, teleport: payload, } } if (position_equal(state.position, state.teleport)) { return { ...state, teleport: null, } } return state }, /** @type {import('../context.js').Observer} */ observe({ client, events, world, signal }) { aiter(abortable(on(events, Context.STATE, { signal }))) .map(([{ position, view_distance, teleport }]) => ({ position, view_distance, teleport, })) .reduce(async (last_state, state) => { if ( state.teleport !== null && !position_equal(last_state.teleport, state.teleport) ) { const chunk = { x: chunk_position(state.teleport.x), z: chunk_position(state.teleport.z), } client.write('update_view_position', { chunkX: chunk.x, chunkZ: chunk.z, }) await load_chunk({ client, world, x: chunk.x, z: chunk.z }) // TODO kick player on error ? client.write('position', { entityId: PLAYER_ENTITY_ID, yaw: 0, pitch: 0, teleportId: 0, ...state.teleport, onGround: true, }) } if (!same_chunk(last_state.position, state.position)) { const { a: points_to_unload, b: points_to_load } = square_symmetric_difference( { x: chunk_position(last_state.position.x), y: chunk_position(last_state.position.z), }, { x: chunk_position(state.position.x), y: chunk_position(state.position.z), }, state.view_distance ) client.write('update_view_position', { chunkX: chunk_position(state.position.x), chunkZ: chunk_position(state.position.z), }) const to_unload = points_to_unload.map(({ x, y }) => ({ x, z: y })) unload_chunks(state, { client, events, world, chunks: to_unload }) const to_load = points_to_load.map(({ x, y }) => ({ x, z: y })) await load_chunks(state, { client, events, world, chunks: to_load }) // TODO kick player on error ? } if (last_state.view_distance !== state.view_distance) { const chunk_point = { x: chunk_position(state.position.x), y: chunk_position(state.position.z), } const points = square_difference( chunk_point, last_state.view_distance, state.view_distance ) const chunks = points.map(({ x, y }) => ({ x, z: y })) client.write('update_view_distance', { viewDistance: state.view_distance, }) const action = state.view_distance > last_state.view_distance ? load_chunks : unload_chunks await action(state, { client, world, events, chunks }) } return state }) .then(state => { const chunk_point = { x: chunk_position(state.position.x), z: chunk_position(state.position.z), } for (let x = -state.view_distance; x <= state.view_distance; x++) { for (let z = -state.view_distance; z <= state.view_distance; z++) { events.emit(Context.CHUNK_UNLOADED, { x: chunk_point.x + x, z: chunk_point.z + z, }) } } }) }, }