world/block.tstypescript
import { AABB, BaseAABB, VoxelShape } from "../physics/aabb";
import { Epsilon, getFrac, getSign, lerp } from "../base/math";
import { BlockRegistry } from "../version/registry";
import { BaseVec3, Direction, Vec3 } from "../physics/direction";
import { SharedState } from "./state";
import { NotImplemented } from "../base/error";
/**
* Represent a state of a block
*/
export class BlockState {
public shape: VoxelShape;
constructor(
public owner: Block,
public id: string,
public properties: Record<string, any>,
public boxes: BaseAABB[]
) {
const bbs = boxes.map(val => new AABB(
val.minX, val.minY, val.minZ,
val.maxX, val.maxY, val.maxZ
));
this.shape = bbs.reduce(
(prevVoxel, currentBox) => VoxelShape.or(prevVoxel, VoxelShape.fromBox(currentBox)),
VoxelShape.Empty
);
}
private get<T>(field: string): T | null;
private get<T>(field: string, defaultVal: T): T;
private get<T>(field: string, defaultVal?: T): T | null {
if (field in this.properties) return this.properties[field];
if (defaultVal) return defaultVal;
return null;
}
public getFacing(): "north" | "south" | "west" | "east" | null { return this.get("facing"); }
}
/**
* Represent a block
*/
export class Block {
public states: BlockState[];
constructor(
public type: string,
public definitions: Record<"type" | "block_set_type" | string, any>,
public properties: Record<string, string[]>,
states: Record<string, any> | BlockState[]
) {
this.states = [];
states.forEach((obj: any) =>
this.states.push(
obj instanceof BlockState ? obj : new BlockState(this, obj['id'], obj['properties'], obj['boxes'])
)
);
}
private get<T>(field: string): T | null;
private get<T>(field: string, defaultVal: T): T;
private get<T>(field: string, defaultVal?: T): T | null {
if (field in this.definitions) return this.definitions[field];
if (defaultVal) return defaultVal;
return null;
}
public getType() { return this.get("type")!; }
public getFriction() { return this.get("friction", 0.6); }
public getBounciness() { return this.get("bounciness", 0); }
public getSpeedFactor() { return this.get("speed_factor", 1); }
public getClimbable() { return this.get("climbable", false); }
public getJumpFactor() { return this.get("jump_factor", 1); }
}
/**
* Somehow, this is an equivalent implementation of Minecraft `Level`
*/
export class BlockManager {
constructor(private state: SharedState) { }
/**
* Check if the chunk section is loaded
*/
public hasChunkAt(x: number, y: number, z: number): boolean;
public hasChunkAt(position: BaseVec3): boolean;
public hasChunkAt(a: BaseVec3 | number, b?: number, c?: number): boolean {
this.state.checkReady();
const { x, y, z } = Vec3.loadArgs(a, b, c);
const sx = Math.floor(x / 16), sy = Math.floor((y + 64) / 16), sz = Math.floor(z / 16);
// console.log({
// key: `${this.tcp.player!.dimension}:${sx}:${sz}`,
// hasKey: `${this.tcp.player!.dimension}:${sx}:${sz}` in this.tcp.world!.chunks
// });
const section = this.state.world!.chunks[`${this.state.player!.dimension}:${sx}:${sz}`]?.sections[sy];
if (!section) return false;
return true;
}
/**
* Get block state at position
*/
public at(x: number, y: number, z: number): BlockState | null;
public at(position: BaseVec3): BlockState | null;
public at(a: BaseVec3 | number, b?: number, c?: number): BlockState | null {
this.state.checkReady();
let { x, y, z } = Vec3.loadArgs(a, b, c);
x = Math.floor(x); y = Math.floor(y); z = Math.floor(z);
const sx = Math.floor(x / 16), sy = Math.floor((y + 64) / 16), sz = Math.floor(z / 16);
const px = ((x % 16) + 16) % 16, py = ((y % 16) + 16) % 16, pz = ((z % 16) + 16) % 16;
if (this.state.cache)
return this.getWithCache(sx, sy, sz, px, py, pz);
else
return this.getWithoutCache(sx, sy, sz, px, py, pz);
}
private getBlock(stateId: number | string) {
return BlockRegistry.getState(String(stateId));
}
private getWithCache(
sx: number, sy: number, sz: number,
px: number, py: number, pz: number,
) {
if (!this.state.cache)
throw new NotImplemented();
const localPos = { x: px, y: py, z: pz };
const sectionKey = `${this.state.player!.dimension}:${sx}:${sy}:${sz}`;
const sectionCache = this.state.cache.get(sectionKey);
if (sectionCache) {
const cachedState = sectionCache.at(this.getLocalPosKey(localPos));
if (cachedState) {
console.log("cache hit", sectionKey, localPos);
return this.getBlock(cachedState);
}
}
console.log("cache miss", sectionKey, localPos);
const section = this.state.world!.chunks[`${this.state.player!.dimension}:${sx}:${sz}`]?.sections[sy]?.block;
if (!section) return null;
if (section.data === null) return BlockRegistry.getState(section.palette[0]!.toString())!;
const arr = new Uint16Array(16 * 16 * 16);
const dataArray = section.data!;
const bitsPerEntry = section.bpe;
const entriesPerLong = Math.floor(64 / bitsPerEntry);
const entryMask = (1 << bitsPerEntry) - 1;
for (let localX = 0; localX < 16; localX++)
for (let localY = 0; localY < 16; localY++)
for (let localZ = 0; localZ < 16; localZ++) {
const entryIndex = this.getLocalPosKey({ x: localX, y: localY, z: localZ });
const longIndex = Math.floor(entryIndex / entriesPerLong);
const bit_index = entryIndex % entriesPerLong * bitsPerEntry;
const localId = Number((dataArray[longIndex]! >> BigInt(bit_index)) & BigInt(entryMask));
const stateId = bitsPerEntry <= 8 ? section.palette[localId]! : localId;
arr[entryIndex]! = stateId;
}
this.state.cache.put(sectionKey, arr);
const state = arr.at(this.getLocalPosKey(localPos))!;
return this.getBlock(state);
}
public deleteChunkCache(
sx: number, sy: number, sz: number,
) {
const sectionKey = `${this.state.player!.dimension}:${sx}:${sy}:${sz}`;
if (this.state.cache)
this.state.cache.del(sectionKey);
}
private getWithoutCache(
sx: number, sy: number, sz: number,
px: number, py: number, pz: number,
) {
const section = this.state.world!.chunks[`${this.state.player!.dimension}:${sx}:${sz}`]?.sections[sy]?.block;
if (!section) return null;
if (section.data === null) return BlockRegistry.getState(section.palette[0]!.toString())!;
const dataArray = section.data!;
const bitsPerEntry = section.bpe;
const entryIndex = px + (pz * 16) + (py * 16 * 16);
const entriesPerLong = Math.floor(64 / bitsPerEntry);
const entryMask = (1 << bitsPerEntry) - 1;
const longIndex = Math.floor(entryIndex / entriesPerLong);
const bit_index = entryIndex % entriesPerLong * bitsPerEntry;
const localId = Number((dataArray[longIndex]! >> BigInt(bit_index)) & BigInt(entryMask));
const stateId = bitsPerEntry <= 8 ? section.palette[localId]! : localId;
return BlockRegistry.getState(String(stateId));
}
private getLocalPosKey(pos: BaseVec3) {
return pos.x + 16 * (pos.y + 16 * pos.z);
}
/**
* Query the entity in the section that intersec with BB
* @param queryBB The querying bounding box
* @returns
*/
public queryAABB(queryBB: AABB) {
this.state.checkReady();
const { minX, minY, minZ, maxX, maxY, maxZ } = queryBB;
const sx0 = Math.floor(minX), sx1 = Math.floor(maxX);
const sy0 = Math.floor(minY), sy1 = Math.floor(maxY);
const sz0 = Math.floor(minZ), sz1 = Math.floor(maxZ);
const blockShapes: VoxelShape[] = [];
for (let x = sx0; x <= sx1; x++)
for (let y = sy0; y <= sy1; y++)
for (let z = sz0; z <= sz1; z++) {
const blockState = this.at(x, y, z);
if (blockState)
blockShapes.push(blockState.shape.move(x, y, z));
}
return blockShapes;
}
}
interface Context {
getBlockState: (position: BaseVec3) => VoxelShape,
from: Vec3,
to: Vec3
}
/**
* For clipping math
*
* @hidden
*/
export class BlockGetter {
public static clip(from: Vec3, to: Vec3, context: Context) {
return this.traverseBlocks(
from, to, context,
(context, pos) => {
const blockState = context.getBlockState(pos);
const { from, to } = context;
return blockState.clip(from, to, pos);
},
(context) => {
const { from, to } = context;
const delta = from.subtract(to);
return BlockHitResult.miss(to, Direction.getApproximateNearest(delta), to);
}
);
}
public static traverseBlocks<T, C = Context>(
from: Vec3,
to: Vec3,
context: C,
consumer: (context: C, position: BaseVec3) => T,
missFactory: (context: C) => T): T {
let result: T;
if (from.equal(to)) return missFactory(context);
const toX = lerp(-Epsilon, to.x, from.x);
const toY = lerp(-Epsilon, to.y, from.y);
const toZ = lerp(-Epsilon, to.z, from.z);
const fromX = lerp(-Epsilon, from.x, to.x);
const fromY = lerp(-Epsilon, from.y, to.y);
const fromZ = lerp(-Epsilon, from.z, to.z);
let currentBlockX = Math.floor(fromX);
let currentBlockY = Math.floor(fromY);
let currentBlockZ = Math.floor(fromZ);
const firstBlock = consumer(context, { x: currentBlockX, y: currentBlockY, z: currentBlockZ });
if (firstBlock !== null) return firstBlock;
const dx = toX - fromX;
const dy = toY - fromY;
const dz = toZ - fromZ;
const signX = getSign(dx);
const signY = getSign(dy);
const signZ = getSign(dz);
const tDeltaX = signX == 0 ? Infinity : signX / dx;
const tDeltaY = signY == 0 ? Infinity : signY / dy;
const tDeltaZ = signZ == 0 ? Infinity : signZ / dz;
let tX = tDeltaX * (signX > 0 ? 1 - getFrac(fromX) : getFrac(fromX));
let tY = tDeltaY * (signY > 0 ? 1 - getFrac(fromY) : getFrac(fromY));
let tZ = tDeltaZ * (signZ > 0 ? 1 - getFrac(fromZ) : getFrac(fromZ));
do {
if (tX <= 1.0 || tY <= 1.0 || tZ <= 1.0) {
if (tX < tY) {
if (tX < tZ) {
currentBlockX += signX;
tX += tDeltaX;
} else {
currentBlockZ += signZ;
tZ += tDeltaZ;
}
} else if (tY < tZ) {
currentBlockY += signY;
tY += tDeltaY;
} else {
currentBlockZ += signZ;
tZ += tDeltaZ;
}
result = consumer(context, { x: currentBlockX, y: currentBlockY, z: currentBlockZ });
} else {
return missFactory(context);
}
} while (result == null);
return result;
}
}
/**
* For clipping math
*
* @hidden
*/
export class BlockHitResult {
constructor(
public miss: boolean,
public location: Vec3,
public direction: Direction,
public pos: BaseVec3,
) { }
public static hit(location: Vec3, direction: Direction, pos: BaseVec3) {
return new BlockHitResult(true, location, direction, pos);
}
public static miss(location: Vec3, direction: Direction, pos: BaseVec3) {
return new BlockHitResult(true, location, direction, pos);
}
}