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