MVP checks / mvp (push) Waiting to run
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.
65 lines
4.8 KiB
JavaScript
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};
|
|
}
|