MVP checks / mvp (push) Canceled after 0s
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.
118 lines
7.7 KiB
JavaScript
118 lines
7.7 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFile} from 'node:fs/promises';
|
|
import {TerrainState} from '../terrain-state.js';
|
|
import {TerrainController} from '../terrain-controller.js';
|
|
import {SectionVoxelMap} from '../section-voxel-map.js';
|
|
const properties=JSON.parse(await readFile(new URL('../light-properties.json',import.meta.url)));
|
|
const cube={min:[0,0,0],max:[1,1,1]};
|
|
const materials=new Map([[1,{state:'minecraft:stone',color:[128,128,128],render:[cube]}],[2,{state:'minecraft:sea_lantern',color:[240,245,250],render:[cube],light:15}],[3,{state:'minecraft:water',color:[80,110,240],render:[cube],opacity:0.5}],[4,{state:'minecraft:glass',color:[180,220,240],render:[cube],opacity:0.3}]]);
|
|
|
|
test('local light meshes match whole-view light with distant sources, a high roof, water and negative coordinates',()=>{
|
|
const blocks=new SectionVoxelMap(),bounds={min:[-48,-16,-48],max:[79,47,63]},columns=[];
|
|
for(let x=-48;x<=79;x++)for(let z=-48;z<=63;z++)blocks.set(`${x},0,${z}`,1);
|
|
for(let x=4;x<13;x++)for(let z=5;z<13;z++)blocks.set(`${x},40,${z}`,1);
|
|
for(let y=1;y<40;y++)blocks.set(`10,${y},11`,3);
|
|
blocks.set('-14,1,4',2);blocks.set('31,1,8',2);blocks.set('3,1,8',4);
|
|
for(let x=-3;x<=4;x++)for(let z=-3;z<=3;z++) {
|
|
const heights=new Int16Array(256);
|
|
for(let lx=0;lx<16;lx++)for(let lz=0;lz<16;lz++)if(x*16+lx>=4&&x*16+lx<13&&z*16+lz>=5&&z*16+lz<13)heights[lx+16*lz]=40;
|
|
columns.push({column:[x,z],heights});
|
|
}
|
|
const state=new TerrainState(properties);
|
|
state.sync({epoch:1,version:1,reset:true,compact:true,sections:[...blocks.sectionEntries()].map(([id,cells])=>({section:id.split(',').map(Number),cells})),materials:[...materials],textures:[],bounds,columns});
|
|
state.light();
|
|
for(const id of ['0,0,0','-1,0,0','0,1,0']) {
|
|
const packet={id,epoch:1,version:1,job:1,meshTicket:1};
|
|
const expected=state.mesh(packet),actual=state.meshLocal(packet);
|
|
assert.deepEqual(actual.vertices,expected.vertices);assert.deepEqual(actual.transparent,expected.transparent);
|
|
assert.ok(actual.lightCells<=68*68*64);
|
|
}
|
|
assert.equal(state.meshLocal({id:'0,2,0',epoch:1,version:1}).lightCached,true,'vertical sections share their column light');
|
|
state.sync({epoch:1,version:2,materials:[],bounds,changes:new Int32Array([70,1,50,2])});
|
|
assert.equal(state.meshLocal({id:'0,0,0',epoch:1,version:2}).lightCached,true,'distant edits keep the local field');
|
|
state.sync({epoch:1,version:3,materials:[],bounds,changes:new Int32Array([0,1,1,2])});
|
|
assert.equal(state.meshLocal({id:'0,0,0',epoch:1,version:3}).lightCached,false,'near edits invalidate light');
|
|
state.light();assert.deepEqual(state.meshLocal({id:'0,0,0',epoch:1,version:3}).vertices,state.mesh({id:'0,0,0',epoch:1,version:3}).vertices);
|
|
});
|
|
|
|
test('a roof outside the vertical view stays dark in local meshes and cache size stays bounded',()=>{
|
|
const state=new TerrainState(properties),bounds={min:[-144,0,-144],max:[159,79,159]},columns=[];
|
|
for(let x=-9;x<=9;x++)for(let z=-9;z<=9;z++)columns.push({column:[x,z],heights:new Int16Array(256).fill(100)});
|
|
state.sync({epoch:1,version:1,reset:true,compact:true,initial:new Int32Array([0,1,0,1]),materials:[...materials],bounds,columns});
|
|
const result=state.meshLocal({epoch:1,version:1,id:'0,0,0'});
|
|
for(let i=19;i<result.vertices.length;i+=20)assert.equal(result.vertices[i],0);
|
|
for(let x=-9;x<=9;x++)state.meshLocal({epoch:1,version:1,id:`${x},0,0`});
|
|
assert.equal(state.localLights.size,8);
|
|
});
|
|
|
|
class Worker {
|
|
packets=[];postMessage(packet,transfers=[]){this.packets.push(structuredClone(packet,{transfer:transfers}));}
|
|
terminate(){this.terminated=true;}
|
|
reply(data){this.onmessage({data});}
|
|
sync(){const p=this.packets.filter(p=>p.type==='sync').at(-1);this.reply({type:'synced',epoch:p.epoch,version:p.version});}
|
|
mesh(packet=this.packets.at(-1)){this.reply({...packet,vertices:new Float32Array(20),transparent:new Float32Array()});}
|
|
}
|
|
function fixture(){const light=new Worker(),mesh=new Worker(),dirty=new Set(),c=new TerrainController({workerFactory:()=>light,meshWorkerFactory:()=>mesh,onDirty:ids=>ids.forEach(id=>dirty.add(id))});
|
|
const cells=new Uint32Array(4096).fill(1),blocks=new SectionVoxelMap();blocks.setSection('0,0,0',cells);
|
|
c.reset(blocks,materials,new Map(),{min:[-144,-32,-144],max:[159,47,159]});c.flush();mesh.sync();return {light,mesh,c,cells,dirty};}
|
|
|
|
test('nearby meshing starts before any global light result and survives repeated far chunk packets',()=>{
|
|
const {light,mesh,c,cells}=fixture();
|
|
try {
|
|
assert.equal(c.workerVersion,-1);assert.equal(c.requestMesh('0,0,0'),true);
|
|
const job=mesh.packets.at(-1);
|
|
for(let i=5;i<9;i++){c.update({sections:[{section:[i,0,i],cells:new Uint32Array(4096)}],materials});c.flush();mesh.sync();}
|
|
mesh.mesh(job);const ready=c.ready.get('0,0,0');assert.ok(ready);assert.equal(c.isCurrent(ready),true);
|
|
assert.equal(cells.byteLength,16384);
|
|
assert.notEqual(light.packets[0].sections[0].cells.buffer,mesh.packets[0].sections[0].cells.buffer);
|
|
assert.equal(c.workerVersion,-1,'global lighting is still deliberately unfinished');
|
|
c.update({changes:[{pos:[16,0,0],block:2}]});assert.equal(c.ready.size,0);assert.equal(c.isCurrent(ready),false);
|
|
c.flush();mesh.sync();assert.equal(c.requestMesh('0,0,0'),true);
|
|
const old=mesh.packets.at(-1);c.update({unload:[[0,0,0]]});mesh.mesh(old);assert.equal(c.ready.size,0);
|
|
}finally{c.dispose();}
|
|
});
|
|
|
|
test('material, sky-column and world changes reject affected meshes, but repeated definitions do not',()=>{
|
|
const {mesh,c}=fixture();try{
|
|
c.requestMesh('0,0,0');mesh.mesh();let ready=c.ready.get('0,0,0');
|
|
c.update({materials:new Map([...materials].map(([id,m])=>[id,structuredClone(m)]))});assert.equal(c.isCurrent(ready),true);
|
|
c.update({columns:[{column:[0,0],heights:new Int16Array(256).fill(100)}]});assert.equal(c.isCurrent(ready),false);
|
|
c.flush();mesh.sync();c.requestMesh('0,0,0');mesh.mesh();ready=c.ready.get('0,0,0');
|
|
c.update({materials:new Map([[1,{...materials.get(1),color:[12,34,56]}]])});assert.equal(c.isCurrent(ready),false);
|
|
c.reset(new SectionVoxelMap(),materials,new Map(),{min:[0,0,0],max:[15,15,15]});assert.equal(c.isCurrent(ready),false);
|
|
}finally{c.dispose();}
|
|
});
|
|
|
|
test('unload and re-entry cannot reuse a ticket from an old in-flight mesh',()=>{
|
|
const {mesh,c}=fixture();try{
|
|
c.requestMesh('0,0,0');const old=mesh.packets.at(-1);
|
|
c.update({unload:[[0,0,0]]});c.flush();mesh.sync();
|
|
c.update({sections:[{section:[0,0,0],cells:new Uint32Array(4096).fill(2)}]});c.flush();mesh.sync();
|
|
mesh.mesh(old);assert.equal(c.ready.size,0);
|
|
c.requestMesh('0,0,0');const current=mesh.packets.at(-1);
|
|
assert.notEqual(current.meshTicket,old.meshTicket);assert.equal(c.isCurrent(old),false);
|
|
mesh.mesh(current);assert.equal(c.isCurrent(c.ready.get('0,0,0')),true);
|
|
}finally{c.dispose();}
|
|
});
|
|
|
|
test('horizontal view movement retains interior meshes and invalidates only changed light boundaries',()=>{
|
|
const {mesh,c}=fixture();try{
|
|
c.requestMesh('0,0,0');mesh.mesh();const near=c.ready.get('0,0,0');c.ready.delete('0,0,0');
|
|
c.requestMesh('-9,0,0');mesh.mesh();const edge=c.ready.get('-9,0,0');
|
|
c.update({bounds:{min:[-112,-32,-144],max:[191,47,159]}});
|
|
assert.equal(c.isCurrent(near),true);assert.equal(c.isCurrent(edge),false);
|
|
c.update({bounds:{min:[-112,-16,-144],max:[191,63,159]}});
|
|
assert.equal(c.isCurrent(near),false,'moving the top sky boundary can change light throughout a column');
|
|
}finally{c.dispose();}
|
|
});
|
|
|
|
test('an obsolete failed mesh releases its job instead of blocking subsequent chunks',()=>{
|
|
const {mesh,c}=fixture();try{
|
|
c.requestMesh('0,0,0');const old=mesh.packets.at(-1);
|
|
c.update({changes:[{pos:[0,0,0],block:2}]});c.flush();mesh.sync();
|
|
mesh.reply({...old,type:'error',error:'obsolete work'});
|
|
assert.equal(c.busy,null);assert.equal(c.meshWorker,mesh);assert.equal(c.requestMesh('0,0,0'),true);
|
|
}finally{c.dispose();}
|
|
});
|