Files
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

65 lines
4.8 KiB
JavaScript

import { MAX_VIEW_WIDTH, VIEW_HEIGHT, MAX_VIEW_CELLS, MAX_VIEW_COORDINATE, WORLD_MIN_Y, WORLD_MAX_Y } from "./view-distance.js";
const triple=p=>Array.isArray(p)&&p.length===3&&p.every(Number.isSafeInteger);
const sectionId=p=>p.join(',');
export function decodeSection(record) {
if(!triple(record?.section)||record.section.some(v=>Math.abs(v*16)>MAX_VIEW_COORDINATE))throw Error('Invalid section coordinates');
const {palette,runs}=record;
if(!Array.isArray(palette)||!palette.length||palette.length>4096||!palette.every(v=>Number.isInteger(v)&&v>=0&&v<=0xffffffff))throw Error('Invalid section palette');
if(!Array.isArray(runs)||!runs.length||runs.length>8192||runs.length%2)throw Error('Invalid section runs');
const uniform = runs.length === 2 && runs[0] === 4096 && Number.isInteger(runs[1]) && runs[1] >= 0 && runs[1] < palette.length;
if (uniform) return {section:[...record.section],cells:new Uint32Array([palette[runs[1]]])};
const cells=new Uint32Array(4096);let offset=0;
for(let i=0;i<runs.length;i+=2) {
const count=runs[i],index=runs[i+1];
if(!Number.isInteger(count)||count<=0||!Number.isInteger(index)||index<0||index>=palette.length||offset+count>4096)throw Error('Invalid section run');
cells.fill(palette[index],offset,offset+count);offset+=count;
}
if(offset!==4096)throw Error('Incomplete section');
return {section:[...record.section],cells};
}
export function sectionInBounds(section,bounds) {
return section.every((v,i)=>v*16>=bounds.min[i]&&v*16+15<=bounds.max[i]);
}
export function applySectionView(view,message) {
if(message.world!==view.world)return 'ignored';
if(!Number.isSafeInteger(message.generation)||message.generation<=view.viewGeneration)return 'ignored';
if(message.revision!==view.revision||!triple(message.view_center)||message.view_center.some(v=>v%16)||!triple(message.view_min)||!triple(message.view_max)||!Array.isArray(message.unload))return 'resync';
const bounds={min:message.view_min,max:message.view_max};
if(message.full_height===true){
if(bounds.min[1]!==WORLD_MIN_Y||bounds.max[1]!==WORLD_MAX_Y)return 'resync';
bounds.fullHeight=true;
}
let volume=1;
for(let i=0;i<3;i++) {const length=bounds.max[i]-bounds.min[i]+1;if(length<=0||length>(i===1?VIEW_HEIGHT:MAX_VIEW_WIDTH)||bounds.min[i]%16||(bounds.max[i]+1)%16||Math.abs(bounds.min[i])>MAX_VIEW_COORDINATE||Math.abs(bounds.max[i])>MAX_VIEW_COORDINATE||(!(i===1&&bounds.fullHeight)&&(message.view_center[i]<bounds.min[i]||message.view_center[i]>bounds.max[i])))return 'resync';volume*=length;}
if(volume>MAX_VIEW_CELLS||message.total_sections!==undefined&&message.total_sections!==volume/4096)return 'resync';
const unload=new Set();
for(const p of message.unload) {if(!triple(p)||sectionInBounds(p,bounds)||unload.has(sectionId(p)))return 'resync';unload.add(sectionId(p));}
// Authoritative bounds determine departing sections; explicit unloads are
// checked above but cannot accidentally leave old sections resident.
for(const id of view.blocks.loadedSectionKeys())if(!sectionInBounds(id.split(',').map(Number),bounds))unload.add(id);
for(const id of unload) {view.blocks.deleteSection(id);view.loadedSections.delete(id);}
view.viewCenter=[...message.view_center];view.viewBounds=bounds;view.viewGeneration=message.generation;
view.totalSections=volume/4096;
return {unload:[...unload].map(id=>id.split(',').map(Number))};
}
export function applySectionBatch(view,message) {
if(message.world!==view.world||message.generation!==view.viewGeneration)return {status:'ignored'};
if(message.revision!==view.revision||!Array.isArray(message.sections)||message.sections.length>128)return {status:'resync'};
const decoded=[],seen=new Set(),columns=[];
try {
if(message.columns!==undefined&&(!Array.isArray(message.columns)||message.columns.length>128))throw Error('Invalid skylight columns');
for(const record of message.sections) {
const result=decodeSection(record),id=sectionId(result.section);
if(seen.has(id)||!sectionInBounds(result.section,view.viewBounds))throw Error('Section outside view');
seen.add(id);decoded.push(result);
}
for(const column of message.columns||[]) {
if(!Array.isArray(column.column)||column.column.length!==2||!column.column.every(Number.isSafeInteger)||!Array.isArray(column.heights)||column.heights.length!==256||!column.heights.every(v=>Number.isInteger(v)&&v>=-32768&&v<=32767))throw Error('Invalid skylight column');
if(!column.column.every((v,i)=>v*16>=view.viewBounds.min[i*2]&&v*16+15<=view.viewBounds.max[i*2]))throw Error('Skylight column outside view');
columns.push({column:[...column.column],heights:new Int16Array(column.heights)});
}
} catch {return {status:'resync'};}
for(const {section,cells} of decoded) {const id=sectionId(section);view.blocks.setSection(id,cells);view.loadedSections.add(id);}
return {status:'applied',sections:decoded,columns};
}