@@ -0,0 +1,19 @@
|
||||
export class BlockBudget {
|
||||
#remaining = 0;
|
||||
|
||||
credit(blocks) {
|
||||
this.#remaining += blocks;
|
||||
}
|
||||
|
||||
spend(blocks) {
|
||||
this.#remaining -= blocks;
|
||||
}
|
||||
|
||||
isExhausted() {
|
||||
return this.#remaining <= 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#remaining = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export class CellFlags {
|
||||
static #sideMask = 0b111111;
|
||||
static #opaqueShift = 6;
|
||||
static NONE = 0;
|
||||
|
||||
static pack(markerMask, opaqueMask) {
|
||||
return (markerMask & CellFlags.#sideMask) | ((opaqueMask & CellFlags.#sideMask) << CellFlags.#opaqueShift);
|
||||
}
|
||||
|
||||
static markerMask(packed) {
|
||||
return packed & CellFlags.#sideMask;
|
||||
}
|
||||
|
||||
static opaqueMask(packed) {
|
||||
return (packed >> CellFlags.#opaqueShift) & CellFlags.#sideMask;
|
||||
}
|
||||
|
||||
static hasMarker(packed, side) {
|
||||
return ((packed >> side) & 1) === 1;
|
||||
}
|
||||
|
||||
static hasOpaque(packed, side) {
|
||||
return ((packed >> (side + CellFlags.#opaqueShift)) & 1) === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { blockModelResolver } from "../Render/model/BlockModelResolver";
|
||||
import { CellFlags } from "./CellFlags";
|
||||
|
||||
export class CellShape {
|
||||
#instance;
|
||||
#showsBlockPreview;
|
||||
|
||||
constructor(instance, showsBlockPreview) {
|
||||
this.#instance = instance;
|
||||
this.#showsBlockPreview = showsBlockPreview;
|
||||
}
|
||||
|
||||
flagsFor(location, verificationLevel) {
|
||||
switch (verificationLevel) {
|
||||
case BlockVerificationLevel.NoMatch:
|
||||
case BlockVerificationLevel.TypeMatch:
|
||||
return blockModelResolver.overlaySideMasks();
|
||||
case BlockVerificationLevel.Match:
|
||||
return this.#structureBlockFlags(location);
|
||||
case BlockVerificationLevel.Missing:
|
||||
return this.#showsBlockPreview ? this.#structureBlockFlags(location) : CellFlags.NONE;
|
||||
default:
|
||||
return CellFlags.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
#structureBlockFlags(location) {
|
||||
const structureBlock = this.#instance.getBlock(location);
|
||||
if (structureBlock === void 0)
|
||||
return CellFlags.NONE;
|
||||
return blockModelResolver.sideMasksOf(structureBlock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { CellShape } from "./CellShape";
|
||||
|
||||
export class CellVerifier {
|
||||
#instance;
|
||||
#shape;
|
||||
|
||||
constructor(instance, showsBlockPreview) {
|
||||
this.#instance = instance;
|
||||
this.#shape = new CellShape(instance, showsBlockPreview);
|
||||
}
|
||||
|
||||
verify(location) {
|
||||
const verificationLevel = this.#levelAt(location);
|
||||
return {
|
||||
verificationLevel,
|
||||
flags: this.#shape.flagsFor(location, verificationLevel)
|
||||
};
|
||||
}
|
||||
|
||||
#levelAt(location) {
|
||||
const globalLocation = this.#instance.toGlobalCoords(location);
|
||||
const worldBlock = this.#instance.getDimension()?.getBlock(globalLocation);
|
||||
if (!worldBlock)
|
||||
return BlockVerificationLevel.Skipped;
|
||||
return this.#compare(worldBlock, this.#instance.getBlock(location));
|
||||
}
|
||||
|
||||
#compare(worldBlock, structureBlock) {
|
||||
if (!structureBlock)
|
||||
return BlockVerificationLevel.Air;
|
||||
if (structureBlock.typeId === "minecraft:air")
|
||||
return worldBlock.isAir ? BlockVerificationLevel.Air : BlockVerificationLevel.NoMatch;
|
||||
if (worldBlock.isAir)
|
||||
return BlockVerificationLevel.Missing;
|
||||
if (worldBlock.typeId !== structureBlock.typeId)
|
||||
return BlockVerificationLevel.NoMatch;
|
||||
if (worldBlock.isWaterlogged !== structureBlock.isWaterlogged)
|
||||
return BlockVerificationLevel.TypeMatch;
|
||||
if (!structureBlock.hasStates)
|
||||
return BlockVerificationLevel.Match;
|
||||
if (worldBlock.permutation.matches(structureBlock.typeId, structureBlock.states))
|
||||
return BlockVerificationLevel.Match;
|
||||
return BlockVerificationLevel.TypeMatch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Vector } from "../../lib/Vector";
|
||||
import { Side } from "../Enums/Side";
|
||||
import { instanceCollection } from "../Instance/InstanceCollection";
|
||||
|
||||
export class ChangedCellPatcher {
|
||||
#patchedOffsets = Object.freeze([Vector.zero, ...Side.OFFSETS]);
|
||||
|
||||
patchAround(dimensionId, worldLocation) {
|
||||
for (const offset of this.#patchedOffsets)
|
||||
this.#patchCell(dimensionId, Vector.add(worldLocation, offset));
|
||||
}
|
||||
|
||||
#patchCell(dimensionId, worldLocation) {
|
||||
for (const instance of instanceCollection.getInstancesAt(dimensionId, worldLocation)) {
|
||||
if (!instance.verifier || !instance.previewRenderer)
|
||||
continue;
|
||||
const location = instance.toStructureCoords(worldLocation);
|
||||
instance.verifier.patchCell(location);
|
||||
instance.previewRenderer.renderBlockAt(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export class ChunkGrid {
|
||||
#chunkSize = 16;
|
||||
#chunkMask = this.#chunkSize - 1;
|
||||
#chunkShift = 4;
|
||||
#chunkKeyStride = 4194304;
|
||||
|
||||
#origin;
|
||||
|
||||
constructor(origin) {
|
||||
this.#origin = origin;
|
||||
}
|
||||
|
||||
keyAt(x, z) {
|
||||
const chunkX = (x + this.#origin.x) >> this.#chunkShift;
|
||||
const chunkZ = (z + this.#origin.z) >> this.#chunkShift;
|
||||
return chunkX * this.#chunkKeyStride + chunkZ;
|
||||
}
|
||||
|
||||
endOfSpanX(x) {
|
||||
return x + this.#chunkSize - ((x + this.#origin.x) & this.#chunkMask);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { VerificationGrid } from "./VerificationGrid";
|
||||
|
||||
export class GridBuffers {
|
||||
#completed;
|
||||
#filling;
|
||||
|
||||
completed() {
|
||||
return this.#completed;
|
||||
}
|
||||
|
||||
filling() {
|
||||
return this.#filling;
|
||||
}
|
||||
|
||||
beginPass(bounds) {
|
||||
this.#filling = this.#recycled(this.#filling, bounds);
|
||||
return this.#filling;
|
||||
}
|
||||
|
||||
commit() {
|
||||
const finished = this.#filling;
|
||||
this.#filling = this.#completed;
|
||||
this.#completed = finished;
|
||||
}
|
||||
|
||||
#recycled(grid, bounds) {
|
||||
if (!grid?.matchesBounds(bounds))
|
||||
return new VerificationGrid(bounds);
|
||||
grid.clear();
|
||||
return grid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Armed means the drawn picture does not yet describe the current bounds, so
|
||||
// the next pass should run flat out. Reading and clearing are separate because
|
||||
// each holder clears at a different point: the verifier only when a pass
|
||||
// commits, the render cursor only when a lap ends.
|
||||
export class PriorityPass {
|
||||
#isArmed = false;
|
||||
|
||||
arm() {
|
||||
this.#isArmed = true;
|
||||
}
|
||||
|
||||
isArmed() {
|
||||
return this.#isArmed;
|
||||
}
|
||||
|
||||
disarm() {
|
||||
this.#isArmed = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TicksPerSecond } from "@minecraft/server";
|
||||
|
||||
export class RefreshRate {
|
||||
static MAX_BLOCKS_PER_TICK = 30;
|
||||
|
||||
static MIN_SECONDS = 1;
|
||||
static MAX_SECONDS = 60;
|
||||
static SECONDS_STEP = 1;
|
||||
static DEFAULT_SECONDS = 15;
|
||||
|
||||
static clampSeconds(seconds) {
|
||||
return Math.min(RefreshRate.MAX_SECONDS, Math.max(RefreshRate.MIN_SECONDS, seconds));
|
||||
}
|
||||
|
||||
static priorityBlocksPerTick() {
|
||||
return RefreshRate.MAX_BLOCKS_PER_TICK;
|
||||
}
|
||||
|
||||
static blocksPerTick(volume, refreshSeconds) {
|
||||
if (volume <= 0)
|
||||
return 0;
|
||||
const seconds = Math.max(refreshSeconds, RefreshRate.MIN_SECONDS);
|
||||
return Math.min(volume / (seconds * TicksPerSecond), RefreshRate.MAX_BLOCKS_PER_TICK);
|
||||
}
|
||||
|
||||
static cycleSeconds(volume, refreshSeconds) {
|
||||
const rate = RefreshRate.blocksPerTick(volume, refreshSeconds);
|
||||
if (rate <= 0)
|
||||
return 0;
|
||||
return volume / (rate * TicksPerSecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export class SkippedChunkTracker {
|
||||
#unloadedChunks = new Set();
|
||||
#outOfBoundsChunks = new Set();
|
||||
|
||||
startLayer() {
|
||||
this.#outOfBoundsChunks.clear();
|
||||
}
|
||||
|
||||
isSkipped(chunkKey) {
|
||||
return this.#unloadedChunks.has(chunkKey) || this.#outOfBoundsChunks.has(chunkKey);
|
||||
}
|
||||
|
||||
trackError(error, chunkKey) {
|
||||
if (error?.name === 'LocationInUnloadedChunkError') {
|
||||
this.#unloadedChunks.add(chunkKey);
|
||||
return true;
|
||||
}
|
||||
if (error?.name === 'LocationOutOfWorldBoundariesError') {
|
||||
this.#outOfBoundsChunks.add(chunkKey);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,144 +1,147 @@
|
||||
import { BlockVerifier } from "./BlockVerifier";
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { BlockVerificationLevelRender } from "../Render/BlockVerificationLevelRender";
|
||||
import { system, TicksPerSecond } from "@minecraft/server";
|
||||
import { system } from "@minecraft/server";
|
||||
import { Vector } from "../../lib/Vector";
|
||||
|
||||
const MIN_TRACK_PLAYER_DISTANCE = 0;
|
||||
const MAX_TRACK_PLAYER_DISTANCE = 7;
|
||||
const MIN_LIFETIME = 8;
|
||||
import { CellVerifier } from "./CellVerifier";
|
||||
import { GridBuffers } from "./GridBuffers";
|
||||
import { PriorityPass } from "./PriorityPass";
|
||||
import { RefreshRate } from "./RefreshRate";
|
||||
import { DebugBoxSweepObserver, SilentSweepObserver } from "./SweepObserver";
|
||||
import { VerificationRun } from "./VerificationRun";
|
||||
import { VerificationSweep } from "./VerificationSweep";
|
||||
import { InstanceVerifierSettings, StandaloneVerifierSettings } from "./VerifierSettings";
|
||||
|
||||
export class StructureVerifier {
|
||||
instance;
|
||||
particleLifetime;
|
||||
#instance;
|
||||
#settings;
|
||||
#buffers = new GridBuffers();
|
||||
#priorityPass = new PriorityPass();
|
||||
#run;
|
||||
#loop;
|
||||
#isPassPending = false;
|
||||
|
||||
locationsToVerify;
|
||||
blockVerificationLevels;
|
||||
isLocationPopulationComplete;
|
||||
isVerificationComplete;
|
||||
shouldStartNextVerification;
|
||||
lastCompleteVerificationLevels;
|
||||
|
||||
#runner;
|
||||
#verifyJob;
|
||||
#populateJob = {};
|
||||
|
||||
constructor(instance, { isEnabled = false, trackPlayerDistance = 0, particleLifetime = 10, isStandalone = false } = {}) {
|
||||
this.instance = instance;
|
||||
this.particleLifetime = Math.max(particleLifetime, MIN_LIFETIME);
|
||||
if (isStandalone) {
|
||||
this.isStandalone = isStandalone;
|
||||
this.enabled = isEnabled;
|
||||
this.trackPlayerDistance = trackPlayerDistance;
|
||||
} else {
|
||||
this.instance.options.setVerifierEnabled(isEnabled);
|
||||
this.instance.options.setVerifierDistance(trackPlayerDistance);
|
||||
}
|
||||
this.locationsToVerify = new Set();
|
||||
static forInstance(instance) {
|
||||
return new StructureVerifier(instance, new InstanceVerifierSettings(instance));
|
||||
}
|
||||
|
||||
startContinuousVerification() {
|
||||
this.shouldStartNextVerification = true;
|
||||
this.#runner = system.runInterval(() => {
|
||||
if (this.shouldStartNextVerification)
|
||||
this.verifyStructure();
|
||||
});
|
||||
static standalone(instance, options) {
|
||||
return new StructureVerifier(instance, new StandaloneVerifierSettings(options));
|
||||
}
|
||||
|
||||
stopContinuousVerification() {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.stopContinuousVerification();
|
||||
if (!this.instance.isEnabled())
|
||||
return;
|
||||
this.startContinuousVerification();
|
||||
constructor(instance, settings) {
|
||||
this.#instance = instance;
|
||||
this.#settings = settings;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
if (this.isStandalone)
|
||||
return this.enabled;
|
||||
return this.instance.options.verifier.isEnabled;
|
||||
return this.#settings.isEnabled();
|
||||
}
|
||||
|
||||
getTrackPlayerDistance() {
|
||||
let distance;
|
||||
if (this.isStandalone)
|
||||
distance = this.trackPlayerDistance;
|
||||
else
|
||||
distance = this.instance.options.verifier.trackPlayerDistance
|
||||
return Math.min(MAX_TRACK_PLAYER_DISTANCE, Math.max(MIN_TRACK_PLAYER_DISTANCE, distance));
|
||||
getCompletedGrid() {
|
||||
return this.#buffers.completed();
|
||||
}
|
||||
|
||||
refresh(isPriority = false) {
|
||||
this.#stopLoop();
|
||||
this.#cancelRun();
|
||||
if (isPriority)
|
||||
this.#priorityPass.arm();
|
||||
if (this.#instance.isEnabled())
|
||||
this.#startLoop();
|
||||
}
|
||||
|
||||
async verifyStructure(shouldRender = false) {
|
||||
if (!this.isEnabled())
|
||||
return void 0;
|
||||
const bounds = this.#instance.getActiveBounds();
|
||||
const volume = Vector.volume(bounds.min, bounds.max);
|
||||
if (volume <= 0)
|
||||
return this.#buffers.completed();
|
||||
this.#cancelRun();
|
||||
const run = this.#beginRun(bounds, volume, shouldRender);
|
||||
this.#finishRun(run, await run.start());
|
||||
return this.#buffers.completed();
|
||||
}
|
||||
|
||||
patchCell(location) {
|
||||
const completed = this.#buffers.completed();
|
||||
if (!completed)
|
||||
return;
|
||||
this.initVerification();
|
||||
return new Promise(async (resolve) => {
|
||||
if (this.#verifyJob)
|
||||
system.clearJob(this.#verifyJob);
|
||||
this.#verifyJob = system.runJob(this.verifyBlocks(shouldRender));
|
||||
const checker = system.runInterval(() => {
|
||||
if (this.isVerificationComplete) {
|
||||
system.clearRun(checker);
|
||||
this.lastCompleteVerificationLevels = JSON.parse(JSON.stringify(this.blockVerificationLevels));
|
||||
this.shouldStartNextVerification = true;
|
||||
resolve(this.blockVerificationLevels);
|
||||
}
|
||||
}, 1);
|
||||
const cell = this.#tryVerifyCell(location);
|
||||
if (!cell)
|
||||
return;
|
||||
completed.setCell(location, cell.verificationLevel, cell.flags);
|
||||
const filling = this.#buffers.filling()
|
||||
filling?.setCell(location, cell.verificationLevel, cell.flags);
|
||||
}
|
||||
|
||||
#tryVerifyCell(location) {
|
||||
try {
|
||||
return this.#newCellVerifier().verify(location);
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
#startLoop() {
|
||||
this.#isPassPending = true;
|
||||
this.#loop = system.runInterval(() => {
|
||||
if (this.#isPassPending)
|
||||
this.verifyStructure();
|
||||
});
|
||||
}
|
||||
|
||||
initVerification() {
|
||||
this.shouldStartNextVerification = false;
|
||||
this.locationsToVerify.clear();
|
||||
this.blockVerificationLevels = { correctlyAir: 0 };
|
||||
this.isLocationPopulationComplete = false;
|
||||
this.isVerificationComplete = false;
|
||||
}
|
||||
|
||||
*verifyBlocks(shouldRender) {
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
for (let y = bounds.min.y; y < bounds.max.y; y++) {
|
||||
for (let z = bounds.min.z; z < bounds.max.z; z++) {
|
||||
for (let x = bounds.min.x; x < bounds.max.x; x++) {
|
||||
const location = new Vector(x, y, z);
|
||||
this.verifyBlock(location, shouldRender);
|
||||
yield void 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.isVerificationComplete = true;
|
||||
#stopLoop() {
|
||||
if (!this.#loop)
|
||||
return;
|
||||
system.clearRun(this.#loop);
|
||||
this.#loop = void 0;
|
||||
}
|
||||
|
||||
verifyBlock(location, shouldRender) {
|
||||
const verificationLevel = this.getVerificationLevel(location);
|
||||
if (verificationLevel === BlockVerificationLevel.Air) {
|
||||
this.blockVerificationLevels.correctlyAir++;
|
||||
} else {
|
||||
this.blockVerificationLevels[JSON.stringify(location)] = verificationLevel;
|
||||
if (shouldRender) {
|
||||
const dimensionLocation = { dimension: this.instance.getDimension(), location: this.instance.toGlobalCoords(location) };
|
||||
new BlockVerificationLevelRender(dimensionLocation, verificationLevel, this.particleLifetime/TicksPerSecond);
|
||||
}
|
||||
}
|
||||
#beginRun(bounds, volume, shouldRender) {
|
||||
this.#isPassPending = false;
|
||||
this.#run = new VerificationRun(
|
||||
this.#newSweep(bounds, shouldRender),
|
||||
this.#blocksPerTick(volume)
|
||||
);
|
||||
return this.#run;
|
||||
}
|
||||
|
||||
getVerificationLevel(location) {
|
||||
const worldBlock = this.instance.getDimension()?.getBlock(this.instance.toGlobalCoords(location));
|
||||
if (!worldBlock)
|
||||
return BlockVerificationLevel.Skipped;
|
||||
const blockVerifier = new BlockVerifier(worldBlock, this.instance);
|
||||
return blockVerifier.verify();
|
||||
#blocksPerTick(volume) {
|
||||
if (this.#priorityPass.isArmed())
|
||||
return RefreshRate.priorityBlocksPerTick();
|
||||
return this.#settings.blocksPerTick(volume);
|
||||
}
|
||||
|
||||
getLastVerificationLevels() {
|
||||
if (!this.lastCompleteVerificationLevels)
|
||||
return {};
|
||||
return this.lastCompleteVerificationLevels;
|
||||
#finishRun(run, didComplete) {
|
||||
if (this.#run === run)
|
||||
this.#run = void 0;
|
||||
if (!didComplete)
|
||||
return;
|
||||
this.#priorityPass.disarm();
|
||||
this.#buffers.commit();
|
||||
this.#isPassPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
#cancelRun() {
|
||||
this.#run?.cancel();
|
||||
}
|
||||
|
||||
#newSweep(bounds, shouldRender) {
|
||||
return new VerificationSweep({
|
||||
bounds,
|
||||
grid: this.#buffers.beginPass(bounds),
|
||||
origin: this.#instance.toGlobalCoords({ x: 0, y: 0, z: 0 }),
|
||||
cellVerifier: this.#newCellVerifier(),
|
||||
observer: this.#newObserver(shouldRender)
|
||||
});
|
||||
}
|
||||
|
||||
#newCellVerifier() {
|
||||
return new CellVerifier(this.#instance, this.#settings.showsBlockPreview());
|
||||
}
|
||||
|
||||
#newObserver(shouldRender) {
|
||||
if (!shouldRender)
|
||||
return new SilentSweepObserver();
|
||||
return new DebugBoxSweepObserver(this.#instance, this.#settings.particleLifetimeSeconds());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { drawExpiringDebugBox } from "../Render/preview/ExpiringDebugBox";
|
||||
|
||||
export class SilentSweepObserver {
|
||||
onCellVerified() {}
|
||||
}
|
||||
|
||||
export class DebugBoxSweepObserver {
|
||||
#instance;
|
||||
#lifetimeSeconds;
|
||||
|
||||
constructor(instance, lifetimeSeconds) {
|
||||
this.#instance = instance;
|
||||
this.#lifetimeSeconds = lifetimeSeconds;
|
||||
}
|
||||
|
||||
onCellVerified(location, verificationLevel) {
|
||||
if (verificationLevel === BlockVerificationLevel.Air)
|
||||
return;
|
||||
drawExpiringDebugBox(
|
||||
this.#instance.getDimension(),
|
||||
this.#instance.toGlobalCoords(location),
|
||||
verificationLevel,
|
||||
this.#lifetimeSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel.js";
|
||||
import { Side } from "../Enums/Side.js";
|
||||
import { CellFlags } from "./CellFlags.js";
|
||||
|
||||
const LEVEL_COUNT = Object.keys(BlockVerificationLevel).length;
|
||||
|
||||
export class VerificationGrid {
|
||||
#min;
|
||||
#sizeX;
|
||||
#sizeY;
|
||||
#sizeZ;
|
||||
#levels;
|
||||
#flags;
|
||||
|
||||
constructor(bounds) {
|
||||
this.#min = { x: bounds.min.x, y: bounds.min.y, z: bounds.min.z };
|
||||
this.#sizeX = Math.max(bounds.max.x - bounds.min.x, 0);
|
||||
this.#sizeY = Math.max(bounds.max.y - bounds.min.y, 0);
|
||||
this.#sizeZ = Math.max(bounds.max.z - bounds.min.z, 0);
|
||||
this.#levels = new Uint8Array(this.#sizeX * this.#sizeY * this.#sizeZ);
|
||||
this.#flags = new Uint16Array(this.#levels.length);
|
||||
}
|
||||
|
||||
matchesBounds(bounds) {
|
||||
return this.#min.x === bounds.min.x && this.#min.y === bounds.min.y && this.#min.z === bounds.min.z
|
||||
&& this.#sizeX === bounds.max.x - bounds.min.x
|
||||
&& this.#sizeY === bounds.max.y - bounds.min.y
|
||||
&& this.#sizeZ === bounds.max.z - bounds.min.z;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#levels.fill(BlockVerificationLevel.Unknown);
|
||||
this.#flags.fill(CellFlags.NONE);
|
||||
}
|
||||
|
||||
setCell(location, verificationLevel, flags) {
|
||||
const index = this.indexOf(location);
|
||||
if (index === -1)
|
||||
return;
|
||||
this.#levels[index] = verificationLevel;
|
||||
this.#flags[index] = flags;
|
||||
}
|
||||
|
||||
setLevel(location, verificationLevel) {
|
||||
const index = this.indexOf(location);
|
||||
if (index === -1)
|
||||
return;
|
||||
this.#levels[index] = verificationLevel;
|
||||
}
|
||||
|
||||
get(location) {
|
||||
const index = this.indexOf(location);
|
||||
if (index === -1)
|
||||
return BlockVerificationLevel.Unknown;
|
||||
return this.#levels[index];
|
||||
}
|
||||
|
||||
occlusionMaskAt(location) {
|
||||
let markerMask = CellFlags.NONE;
|
||||
let opaqueMask = CellFlags.NONE;
|
||||
for (let side = 0; side < Side.COUNT; side++) {
|
||||
const neighborFlags = this.#neighborFlags(location, side);
|
||||
const facingSide = Side.opposite(side);
|
||||
if (CellFlags.hasMarker(neighborFlags, facingSide))
|
||||
markerMask |= 1 << side;
|
||||
if (CellFlags.hasOpaque(neighborFlags, facingSide))
|
||||
opaqueMask |= 1 << side;
|
||||
}
|
||||
return CellFlags.pack(markerMask, opaqueMask);
|
||||
}
|
||||
|
||||
#neighborFlags(location, side) {
|
||||
const offset = Side.OFFSETS[side];
|
||||
const index = this.#indexOfCoords(
|
||||
location.x + offset.x, location.y + offset.y, location.z + offset.z
|
||||
);
|
||||
if (index === -1)
|
||||
return CellFlags.NONE;
|
||||
return this.#flags[index];
|
||||
}
|
||||
|
||||
countByLevel() {
|
||||
const counts = new Uint32Array(LEVEL_COUNT);
|
||||
for (let index = 0; index < this.#levels.length; index++)
|
||||
counts[this.#levels[index]]++;
|
||||
return counts;
|
||||
}
|
||||
|
||||
indexOf(location) {
|
||||
return this.#indexOfCoords(location.x, location.y, location.z);
|
||||
}
|
||||
|
||||
#indexOfCoords(x, y, z) {
|
||||
const localX = x - this.#min.x;
|
||||
const localY = y - this.#min.y;
|
||||
const localZ = z - this.#min.z;
|
||||
if (localX < 0 || localY < 0 || localZ < 0
|
||||
|| localX >= this.#sizeX || localY >= this.#sizeY || localZ >= this.#sizeZ)
|
||||
return -1;
|
||||
return (localY * this.#sizeZ + localZ) * this.#sizeX + localX;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { system } from "@minecraft/server";
|
||||
import { BlockBudget } from "./BlockBudget";
|
||||
|
||||
export class VerificationRun {
|
||||
#sweep;
|
||||
#blocksPerTick;
|
||||
#budget = new BlockBudget();
|
||||
#steps;
|
||||
#runner;
|
||||
#settle;
|
||||
#completion;
|
||||
|
||||
constructor(sweep, blocksPerTick) {
|
||||
this.#sweep = sweep;
|
||||
this.#blocksPerTick = blocksPerTick;
|
||||
this.#completion = new Promise((resolve) => { this.#settle = resolve; });
|
||||
}
|
||||
|
||||
start() {
|
||||
this.#steps = this.#sweep.run();
|
||||
this.#runner = system.runInterval(() => this.#advance());
|
||||
return this.#completion;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.#settleWith(false);
|
||||
}
|
||||
|
||||
#advance() {
|
||||
this.#budget.credit(this.#blocksPerTick);
|
||||
try {
|
||||
while (!this.#budget.isExhausted()) {
|
||||
if (!this.#advanceOneStep())
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.#settleWith(false);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
#advanceOneStep() {
|
||||
const step = this.#steps.next();
|
||||
if (step.done) {
|
||||
this.#settleWith(true);
|
||||
return false;
|
||||
}
|
||||
this.#budget.spend(step.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
#settleWith(didComplete) {
|
||||
if (!this.#settle)
|
||||
return;
|
||||
const settle = this.#settle;
|
||||
this.#settle = void 0;
|
||||
this.#stop();
|
||||
settle(didComplete);
|
||||
}
|
||||
|
||||
#stop() {
|
||||
if (this.#runner !== void 0)
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
this.#steps = void 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { Vector } from "../../lib/Vector";
|
||||
import { ChunkGrid } from "./ChunkGrid";
|
||||
import { SkippedChunkTracker } from "./SkippedChunkTracker";
|
||||
|
||||
export class VerificationSweep {
|
||||
#bounds;
|
||||
#grid;
|
||||
#cellVerifier;
|
||||
#observer;
|
||||
#chunks;
|
||||
#skippedChunks = new SkippedChunkTracker();
|
||||
#location = new Vector();
|
||||
|
||||
constructor({ bounds, grid, cellVerifier, observer, origin }) {
|
||||
this.#bounds = bounds;
|
||||
this.#grid = grid;
|
||||
this.#cellVerifier = cellVerifier;
|
||||
this.#observer = observer;
|
||||
this.#chunks = new ChunkGrid(origin);
|
||||
}
|
||||
|
||||
*run() {
|
||||
for (let y = this.#bounds.min.y; y < this.#bounds.max.y; y++) {
|
||||
this.#skippedChunks.startLayer();
|
||||
for (let z = this.#bounds.min.z; z < this.#bounds.max.z; z++)
|
||||
yield* this.#sweepRow(y, z);
|
||||
}
|
||||
}
|
||||
|
||||
*#sweepRow(y, z) {
|
||||
for (let x = this.#bounds.min.x; x < this.#bounds.max.x;) {
|
||||
const endX = this.#sweepChunkSpan(x, y, z);
|
||||
yield endX - x;
|
||||
x = endX;
|
||||
}
|
||||
}
|
||||
|
||||
#sweepChunkSpan(startX, y, z) {
|
||||
const chunkKey = this.#chunks.keyAt(startX, z);
|
||||
const endX = Math.min(this.#chunks.endOfSpanX(startX), this.#bounds.max.x);
|
||||
if (this.#skippedChunks.isSkipped(chunkKey))
|
||||
this.#markSkipped(startX, endX, y, z);
|
||||
else
|
||||
this.#verifySpan(chunkKey, startX, endX, y, z);
|
||||
return endX;
|
||||
}
|
||||
|
||||
#verifySpan(chunkKey, startX, endX, y, z) {
|
||||
let x = startX;
|
||||
try {
|
||||
for (; x < endX; x++)
|
||||
this.#verifyCell(this.#location.set(x, y, z));
|
||||
} catch (error) {
|
||||
if (!this.#skippedChunks.trackError(error, chunkKey))
|
||||
throw error;
|
||||
this.#markSkipped(x, endX, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
#verifyCell(location) {
|
||||
const { verificationLevel, flags } = this.#cellVerifier.verify(location);
|
||||
this.#grid.setCell(location, verificationLevel, flags);
|
||||
this.#observer.onCellVerified(location, verificationLevel);
|
||||
}
|
||||
|
||||
#markSkipped(startX, endX, y, z) {
|
||||
for (let x = startX; x < endX; x++)
|
||||
this.#grid.setLevel(this.#location.set(x, y, z), BlockVerificationLevel.Skipped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { TicksPerSecond } from "@minecraft/server";
|
||||
import { renderProfileOf } from "../Enums/RenderMode";
|
||||
import { RefreshRate } from "./RefreshRate";
|
||||
|
||||
const DEFAULT_PARTICLE_LIFETIME_TICKS = 10;
|
||||
const DEFAULT_BLOCKS_PER_TICK = 10;
|
||||
|
||||
class ParticleLifetime {
|
||||
static MIN_TICKS = 8;
|
||||
|
||||
static toSeconds(ticks) {
|
||||
return Math.max(ticks, ParticleLifetime.MIN_TICKS) / TicksPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
export class InstanceVerifierSettings {
|
||||
#instance;
|
||||
|
||||
constructor(instance) {
|
||||
this.#instance = instance;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.#verifierOptions().isEnabled;
|
||||
}
|
||||
|
||||
blocksPerTick(volume) {
|
||||
return RefreshRate.blocksPerTick(volume, this.#verifierOptions().refreshSeconds);
|
||||
}
|
||||
|
||||
showsBlockPreview() {
|
||||
return renderProfileOf(this.#instance.options.renderMode).blockPreview;
|
||||
}
|
||||
|
||||
particleLifetimeSeconds() {
|
||||
return ParticleLifetime.toSeconds(this.#verifierOptions().particleLifetime);
|
||||
}
|
||||
|
||||
#verifierOptions() {
|
||||
return this.#instance.options.verifier;
|
||||
}
|
||||
}
|
||||
|
||||
export class StandaloneVerifierSettings {
|
||||
#blocksPerTick;
|
||||
#particleLifetimeTicks;
|
||||
|
||||
constructor({ blocksPerTick = DEFAULT_BLOCKS_PER_TICK, particleLifetime = DEFAULT_PARTICLE_LIFETIME_TICKS } = {}) {
|
||||
this.#blocksPerTick = blocksPerTick;
|
||||
this.#particleLifetimeTicks = particleLifetime;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
blocksPerTick() {
|
||||
return this.#blocksPerTick;
|
||||
}
|
||||
|
||||
showsBlockPreview() {
|
||||
return true;
|
||||
}
|
||||
|
||||
particleLifetimeSeconds() {
|
||||
return ParticleLifetime.toSeconds(this.#particleLifetimeTicks);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user