complete rename and init builder options

This commit is contained in:
ForestOfLight
2025-04-19 17:55:28 -07:00
Unverified
parent 818f2d0183
commit d6c9acdaab
33 changed files with 297 additions and 183 deletions
@@ -0,0 +1,77 @@
import { world } from "@minecraft/server";
import { Vector } from "../lib/Vector";
export class Structure {
structureId;
#structure;
constructor(structureId) {
this.structureId = structureId;
this.#structure = world.structureManager.get(structureId);
if (!this.#structure)
throw new Error(`[Construct] Structure '${structureId}' not found.`);
this.#structure.saveToWorld();
}
getHeight() {
return this.#structure.size.y;
}
getMin() {
return new Vector(0, 0, 0);
}
getMax() {
return Vector.from(this.#structure.size);
}
getBlock(structureLocation) {
const blockPermutation = this.#structure.getBlockPermutation(structureLocation);
if (!blockPermutation)
return void 0;
blockPermutation.location = structureLocation;
return blockPermutation;
}
*getBlocks(locations) {
for (const location of locations) {
yield this.getBlock(location);
}
}
*getLayerBlocks(layer) {
for (let x = 0; x < this.#structure.size.x; x++) {
for (let z = 0; z < this.#structure.size.z; z++) {
yield this.getBlock({ x, y: layer, z });
}
}
}
*getAllBlocks() {
for (let y = 0; y < this.#structure.size.y; y++) {
yield * this.getLayerBlocks(y);
}
}
getLayerLocations(layer) {
const locations = new Set();
for (let x = 0; x < this.#structure.size.x; x++) {
for (let z = 0; z < this.#structure.size.z; z++) {
locations.add(new Vector(x, layer, z));
}
}
return locations;
}
getAllLocations() {
const locations = new Set();
for (let y = 0; y < this.#structure.size.y; y++) {
const layerLocations = this.getLayerLocations(y);
for (const location of layerLocations) {
locations.add(location);
}
}
return locations;
}
}
@@ -0,0 +1,100 @@
import { InstanceOptions } from './InstanceOptions';
import { StructureInstance } from './StructureInstance';
import { world } from '@minecraft/server';
class StructureCollection {
structures;
constructor() {
this.structures = {};
}
loadExistingInstances() {
world.getDynamicPropertyIds().filter(id => id.startsWith('instanceOptions:')).forEach(id => {
const instanceName = id.replace('instanceOptions:', '');
let structureId;
try {
structureId = InstanceOptions.getInstanceStrucetureId(instanceName);
this.structures[instanceName] = new StructureInstance(instanceName, structureId);
} catch (e) {
world.sendMessage(`§c[Construct] Error loading structure instance '${instanceName}'. It will be removed.`);
world.setDynamicProperty(id, void 0);
throw e;
}
});
}
add(instanceName, structureId) {
if (this.structures[instanceName])
throw new Error(`Instance ${instanceName} already exists.`);
const structure = new StructureInstance(instanceName, structureId);
this.structures[instanceName] = structure;
return structure;
}
get(instanceName) {
const structure = this.structures[instanceName];
if (!structure) {
throw new Error(`Instance ${instanceName} not found.`);
}
return structure;
}
delete(instanceName) {
const struct = this.get(instanceName);
struct.delete();
delete this.structures[instanceName];
}
getInstanceNames() {
return Object.keys(this.structures);
}
getStructures(dimensionId, location, options = {}) {
return Object.values(this.structures).filter(structure => {
try {
return structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options)
} catch (e) {
if (e.name === 'InvalidStructureError') {
structureCollection.delete(structure.name);
return false;
} else {
throw e;
}
}
});
}
getStructure(dimensionId, location, options = {}) {
return this.getStructures(dimensionId, location, options)[0];
}
fetchStructureBlock(dimensionId, location) {
const structure = this.getStructure(dimensionId, location);
if (!structure)
return void 0;
return structure.getBlock(structure.toStructureCoords(location));
}
getWorldStructureIds() {
return world.structureManager.getWorldStructureIds()
.filter(id => id.startsWith('mystructure:'))
.map(id => id.replace('mystructure:', ''));
}
rename(instanceName, newName) {
const structure = this.get(instanceName);
if (this.structures[newName])
throw new Error(`Instance '${newName}' already exists.`);
structure.rename(newName);
this.structures[newName] = structure;
delete this.structures[instanceName];
structure.name = newName;
}
}
export const structureCollection = new StructureCollection();
world.afterEvents.worldLoad.subscribe(() => {
structureCollection.loadExistingInstances();
});
@@ -0,0 +1,232 @@
import { Vector } from "../lib/Vector";
import { StructureOutliner } from "./StructureOutliner";
import { StructureVerifier } from "./StructureVerifier";
import { InstanceOptions } from "./InstanceOptions";
import { Structure } from "./Structure";
import { TicksPerSecond } from "@minecraft/server";
export class StructureInstance {
options;
structure = void 0;
verifier = void 0;
outliner = void 0;
constructor(instanceName, structureId) {
this.structure = new Structure(structureId);
this.options = new InstanceOptions(instanceName, structureId);
this.refreshBox();
}
delete() {
this.disable();
delete this.options;
delete this.structure;
delete this.outliner;
delete this.verifier;
this.options.clear();
}
refreshBox() {
if (!this.hasLocation())
return;
if (!this.outliner)
this.outliner = new StructureOutliner(this);
if (!this.verifier)
this.verifier = new StructureVerifier(this, { isEnabled: this.options.verifier.isEnabled, trackPlayerDistance: this.options.verifier.trackPlayerDistance });
this.outliner.refresh();
this.verifier.refresh();
}
getName() {
return this.options.instanceName;
}
getStructureId() {
return this.options.structureId;
}
getLocation() {
return { dimensionId: this.options.dimensionId, location: this.options.worldLocation };
}
getDimension() {
return this.options.getDimension();
}
getLayer() {
return this.options.currentLayer;
}
getMaxLayer() {
return this.structure.getHeight();
}
getBounds() {
return {
min: this.structure.getMin(),
max: this.structure.getMax()
}
}
getActiveBounds() {
if (!this.options.isEnabled)
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.getLayerBounds(this.getLayer());
return this.getBounds();
}
getLayerBounds(layer) {
if (!this.options.isEnabled)
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
const min = this.structure.getMin();
const max = this.structure.getMax();
return {
min: new Vector(min.x, layer - 1, min.z),
max: new Vector(max.x, layer, max.z)
};
}
getBlock(structureLocation) {
return this.structure.getBlock(structureLocation);
}
getBlocks(structureLocations) {
return this.structure.getBlocks(structureLocations);
}
getLayerBlocks(layer) {
return this.structure.getLayerBlocks(layer);
}
getAllBlocks() {
return this.structure.getAllBlocks();
}
getActiveBlocks() {
if (!this.options.isEnabled)
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.getLayerBlocks(this.getLayer());
return this.getAllBlocks();
}
isLocationActive(dimensionId, structureLocation, { useActiveLayer = true } = {}) {
if (!this.options.isEnabled || this.options.dimensionId !== dimensionId)
return false;
let bounds;
if (useActiveLayer)
bounds = this.getActiveBounds();
else
bounds = this.getBounds();
return structureLocation.x >= bounds.min.x && structureLocation.x < bounds.max.x
&& structureLocation.y >= bounds.min.y && structureLocation.y < bounds.max.y
&& structureLocation.z >= bounds.min.z && structureLocation.z < bounds.max.z;
}
getAllActiveLocations() {
if (!this.options.isEnabled)
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.structure.getLayerLocations(this.getLayer()-1);
else
return this.structure.getAllLocations();
}
isEnabled() {
return this.options.isEnabled;
}
hasLocation() {
return this.options.dimensionId && this.options.worldLocation.x !== 0 && this.options.worldLocation.y !== 0 && this.options.worldLocation.z !== 0;
}
hasLayers() {
return this.getMaxLayer() > 1;
}
hasLayerSelected() {
return this.hasLayers() && this.options.currentLayer !== 0;
}
hasWholeStructureSelected() {
return this.hasLocation() && this.options.currentLayer === 0;
}
isAtMaxLayer() {
return !this.hasLayers || this.options.currentLayer >= this.getMaxLayer();
}
isAtMinLayer() {
return !this.hasLayers || this.options.currentLayer <= 0;
}
enable() {
this.options.enable();
this.refreshBox();
}
disable() {
this.options.disable();
this.refreshBox();
}
rename(newName) {
this.options.rename(newName);
}
place(dimensionId, worldLocation) {
this.move(dimensionId, worldLocation);
this.enable();
}
move(dimensionId, worldLocation) {
this.options.move(dimensionId, worldLocation);
this.refreshBox();
}
setLayer(layer) {
if (layer < 0 || layer > this.getMaxLayer())
throw new Error(`[Construct] Layer ${layer} is out of bounds.`);
this.options.setLayer(layer);
this.refreshBox();
}
setVerifierEnabled(enable) {
this.options.setVerifierEnabled(enable);
this.verifier.refresh();
}
setVerifierDistance(distance) {
this.options.setVerifierDistance(distance);
if (this.options.verifier.trackPlayerDistance === 0) {
const bounds = this.getBounds();
this.options.verifier.intervalOrLifetime = Math.max(bounds.min.volume(bounds.max) / TicksPerSecond, 2*TicksPerSecond);
} else {
this.options.verifier.intervalOrLifetime = 10;
}
this.verifier.refresh();
}
increaseLayer() {
if (this.isAtMaxLayer())
this.setLayer(0);
else
this.setLayer(this.options.currentLayer + 1);
}
decreaseLayer() {
if (this.isAtMinLayer())
this.setLayer(this.getMaxLayer());
else
this.setLayer(this.options.currentLayer - 1);
}
toGlobalCoords(structureLocation) {
return Vector.from(structureLocation).add(this.options.worldLocation);
}
toStructureCoords(worldLocation) {
return Vector.from(worldLocation).subtract(this.options.worldLocation);
}
}
@@ -0,0 +1,53 @@
import { Outliner } from './Outliner';
export class StructureOutliner {
constructor(instance) {
this.instance = instance;
this.pullInstanceData();
this.outliner = new Outliner(this.dimension, this.bounds.min, this.bounds.max);
}
pullInstanceData() {
try {
this.dimension = this.instance.getDimension();
this.bounds = this.instance.getBounds();
this.bounds.min = this.instance.toGlobalCoords(this.bounds.min);
this.bounds.max = this.instance.toGlobalCoords(this.bounds.max);
} catch (e) {
if (e.name === 'InvalidStructureError')
this.outliner.stopDraw();
else
throw e;
}
}
refresh() {
this.pullInstanceData();
this.refreshDraw();
}
refreshDraw() {
this.outliner.stopDraw();
if (!this.instance.isEnabled())
return;
if (this.instance.hasLayerSelected())
this.layeredDraw();
else
this.boxDraw();
this.outliner.startDraw();
}
boxDraw() {
this.outliner.setVertices(this.dimension, this.bounds.min, this.bounds.max);
}
layeredDraw() {
const { min, max } = this.instance.getLayeredBounds();
this.outliner.setVertices(this.dimension, this.instance.toGlobalCoords(min), this.instance.toGlobalCoords(max));
this.outliner.addStandaloneParticles(this.getCornerVertices());
}
getCornerVertices() {
return this.outliner.getVertices(this.bounds.min, this.bounds.max);
}
}
@@ -0,0 +1,65 @@
import { BlockVerificationLevel } from './enums/BlockVerificationLevel.js';
export class StructureStatistics {
constructor(instance, verification) {
this.instance = instance;
this.verification = verification;
this.parse();
}
init() {
this.statistics = {};
for (const verificationLevel of Object.values(BlockVerificationLevel)) {
this.statistics[verificationLevel] = 0;
}
this.statistics.correctlyAir = 0;
}
parse() {
this.init();
for (const blockVerificationLevel of Object.values(BlockVerificationLevel)) {
this.parseStatistic(blockVerificationLevel);
}
}
parseStatistic(blockVerificationLevel) {
for (const verificationlevel of Object.values(this.verification)) {
if (blockVerificationLevel === verificationlevel) {
this.statistics[verificationlevel]++;
}
}
}
getNonAirBlocks() {
const activeBounds = this.instance.getActiveBounds();
return activeBounds.min.volume(activeBounds.max) - this.verification.correctlyAir;
}
getStat(blockVerificationLevel) {
return { num: this.statistics[blockVerificationLevel], percent: this.statistics[blockVerificationLevel] / (this.getNonAirBlocks()) * 100 };
}
getSkipped() {
return this.statistics[BlockVerificationLevel.Skipped] || 0;
}
getMessage() {
let message = '';
message += `§fStatistics for §a${this.instance.getName()}§f:`;
if (this.instance.hasLayerSelected())
message += ` §7(layer ${this.instance.getLayer()})`;
message += `\n§7Blocks: §2${this.getNonAirBlocks()}\n`;
const skipped = this.getSkipped();
if (skipped > 0)
message += `§c[!] This analysis skipped ${skipped} blocks.\n`;
message += `§7Correct: §a${this.formatStat(this.getStat(BlockVerificationLevel.Match))}\n`;
message += `§7Block State Incorrect: §e${this.formatStat(this.getStat(BlockVerificationLevel.TypeMatch))}\n`;
message += `§7Incorrect: §c${this.formatStat(this.getStat(BlockVerificationLevel.NoMatch))}\n`;
message += `§7Missing: §3${this.formatStat(this.getStat(BlockVerificationLevel.Missing))}\n`;
return message;
}
formatStat(stat) {
return `${stat.num} (${stat.percent.toFixed(2)}%%)`;
}
}
@@ -0,0 +1,146 @@
import { BlockVerifier } from "./BlockVerifier";
import { BlockVerificationLevel } from "./enums/BlockVerificationLevel";
import { BlockVerificationLevelRender } from "./BlockVerificationLevelRender";
import { system, TicksPerSecond } from "@minecraft/server";
import { Vector } from "../lib/Vector";
const MIN_TRACK_PLAYER_DISTANCE = 0;
const MAX_TRACK_PLAYER_DISTANCE = 7;
const MIN_LIFETIME = 8;
export class StructureVerifier {
instance;
intervalOrLifetime;
locationsToVerify;
blockVerificationLevels;
isLocationPopulationComplete;
isVerificationComplete;
#runner;
#verifyJob;
#populateJob = {};
constructor(instance, { isEnabled = false, trackPlayerDistance = 0, intervalOrLifetime = 10 } = {}) {
this.instance = instance;
this.intervalOrLifetime = Math.max(intervalOrLifetime, MIN_LIFETIME);
this.instance.options.setVerifierEnabled(isEnabled);
this.instance.options.setVerifierDistance(trackPlayerDistance);
this.locationsToVerify = new Set();
}
startContinuousVerification() {
this.#runner = system.runInterval(() => {
this.verifyStructure();
}, this.intervalOrLifetime);
}
stopContinuousVerification() {
if (!this.#runner)
return;
system.clearRun(this.#runner);
this.#runner = void 0;
}
refresh() {
this.stopContinuousVerification();
if (!this.instance.isEnabled())
return;
this.startContinuousVerification();
}
isEnabled() {
return this.instance.options.verifier.isEnabled;
}
getTrackPlayerDistance() {
return Math.min(MAX_TRACK_PLAYER_DISTANCE, Math.max(MIN_TRACK_PLAYER_DISTANCE, this.instance.options.verifier.trackPlayerDistance));
}
init() {
this.locationsToVerify.clear();
this.blockVerificationLevels = { correctlyAir: 0 };
this.isLocationPopulationComplete = false;
this.isVerificationComplete = false;
}
async verifyStructure(shouldRender = true) {
if (!this.isEnabled())
return;
this.init();
return new Promise(async (resolve) => {
await this.populateLocationsToVerify();
if (this.#verifyJob)
system.clearJob(this.#verifyJob);
this.verifyJob = system.runJob(this.verifyBlocks(this.locationsToVerify, shouldRender));
const checker = system.runInterval(() => {
if (this.isVerificationComplete) {
system.clearRun(checker);
resolve(this.blockVerificationLevels);
}
}, 1);
});
}
async populateLocationsToVerify() {
return new Promise((resolve) => {
if (this.getTrackPlayerDistance() === 0) {
this.locationsToVerify = this.instance.getAllActiveLocations();
resolve();
} else {
for (const job of Object.values(this.#populateJob))
system.clearJob(job);
for (const player of this.instance.getDimension().getPlayers())
this.#populateJob[player.id] = system.runJob(this.populateActiveLocationsNearPlayer(player));
const checker = system.runInterval(() => {
if (this.isLocationPopulationComplete) {
system.clearRun(checker);
resolve();
}
}, 1);
}
});
}
*populateActiveLocationsNearPlayer(player) {
const distance = this.getTrackPlayerDistance();
for (let x = -distance; x < distance; x++) {
for (let y = -distance; y < distance; y++) {
for (let z = -distance; z < distance; z++) {
const worldLocation = Vector.from(player.location).add(new Vector(x, y, z)).floor();;
const structureLocation = this.instance.toStructureCoords(worldLocation);
if (this.instance.isLocationActive(player.dimension.id, structureLocation, { useActiveLayer: true })) {
this.locationsToVerify.add(structureLocation);
}
yield void 0;
}
}
}
this.isLocationPopulationComplete = true;
}
*verifyBlocks(locations, shouldRender) {
for (const location of locations) {
const verificationLevel = this.verifyBlock(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.intervalOrLifetime/TicksPerSecond);
}
}
yield void 0;
}
this.isVerificationComplete = true;
}
verifyBlock(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();
}
}