Files
shacraft-core/client/tests/local-terrain-meshing.test.js
Emil c7e86663d8
MVP checks / mvp (push) Canceled after 0s
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

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();}
});