diff --git a/StrucTool [BP]/scripts/classes/BlockInfo.js b/StrucTool [BP]/scripts/classes/BlockInfo.js index 844c391..34f6b19 100644 --- a/StrucTool [BP]/scripts/classes/BlockInfo.js +++ b/StrucTool [BP]/scripts/classes/BlockInfo.js @@ -14,7 +14,7 @@ class BlockInfo { } static showStructureBlockInfo(player) { - const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useLayers: false }); + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useActiveLayer: true }); if (!block && this.shownToLastTick.has(player.id)) { player.onScreenDisplay.setActionBar({ text: 'Structure:\n§7None' }); this.shownToLastTick.delete(player.id); diff --git a/StrucTool [BP]/scripts/classes/BlockVerificationLevelRender.js b/StrucTool [BP]/scripts/classes/BlockVerificationLevelRender.js index 33ed279..34ebde3 100644 --- a/StrucTool [BP]/scripts/classes/BlockVerificationLevelRender.js +++ b/StrucTool [BP]/scripts/classes/BlockVerificationLevelRender.js @@ -4,13 +4,13 @@ import { Vector } from "../lib/Vector"; export class BlockVerificationLevelRender { opacity = 0.2; - lifetime = 0; + lifetimeSeconds = 0; - constructor(dimensionLocation, verificationLevel, lifetime = 5) { + constructor(dimensionLocation, verificationLevel, lifetimeSeconds = 5) { this.dimension = dimensionLocation.dimension; - this.location = new Vector(dimensionLocation.location.x, dimensionLocation.location.y, dimensionLocation.location.z); + this.location = Vector.from(dimensionLocation.location); this.verificationLevel = verificationLevel; - this.lifetime = lifetime; + this.lifetimeSeconds = lifetimeSeconds; this.renderBlock(); } @@ -19,7 +19,7 @@ export class BlockVerificationLevelRender { const color = this.getRGBAMolang(); if (!color) return; - color.setFloat("lifetime", this.lifetime); + color.setFloat("lifetime", this.lifetimeSeconds); try { this.dimension.spawnParticle(particleLocation.particleType, particleLocation.location, color); } catch { diff --git a/StrucTool [BP]/scripts/classes/InstanceEditForm.js b/StrucTool [BP]/scripts/classes/InstanceEditForm.js index 0a87d14..e68911e 100644 --- a/StrucTool [BP]/scripts/classes/InstanceEditForm.js +++ b/StrucTool [BP]/scripts/classes/InstanceEditForm.js @@ -1,6 +1,7 @@ import { structureCollection } from './StructureCollection'; import { MenuForm } from '../classes/MenuForm'; -import { InstanceEditOptions } from './enums/InstanceEditOptions'; +import { forceShow } from '../utils'; +import { InstanceEditButtons } from './enums/InstanceEditButtons'; import { InstanceEditFormBuilder } from './InstanceEditFormBuilder'; import { FormCancelationReason } from '@minecraft/server-ui'; @@ -8,25 +9,26 @@ export class InstanceEditForm { instanceName; #buttons = { isEnabled: [ - InstanceEditOptions.NextLayer, - InstanceEditOptions.PreviousLayer, - InstanceEditOptions.SetLayer, - InstanceEditOptions.Move, - InstanceEditOptions.Statistics, - InstanceEditOptions.RenameInstance, - InstanceEditOptions.DisableInstance, + InstanceEditButtons.NextLayer, + InstanceEditButtons.PreviousLayer, + InstanceEditButtons.SetLayer, + InstanceEditButtons.Move, + InstanceEditButtons.Statistics, + InstanceEditButtons.Settings, + InstanceEditButtons.Rename, + InstanceEditButtons.Disable, ], isNotEnabledAndIsNotPlaced: [ - InstanceEditOptions.PlaceInstance, - InstanceEditOptions.RenameInstance + InstanceEditButtons.Place, + InstanceEditButtons.Rename ], isNotEnabledButIsPlaced: [ - InstanceEditOptions.EnableInstance, - InstanceEditOptions.RenameInstance + InstanceEditButtons.Enable, + InstanceEditButtons.Rename ], common: [ - InstanceEditOptions.DeleteInstance, - InstanceEditOptions.MainMenu + InstanceEditButtons.Delete, + InstanceEditButtons.MainMenu ] } @@ -39,7 +41,7 @@ export class InstanceEditForm { show() { const currentOptions = this.getActiveOptions(); - InstanceEditFormBuilder.buildInstance(this.instance, currentOptions).show(this.player).then((response) => { + forceShow(this.player, InstanceEditFormBuilder.buildInstance(this.instance, currentOptions)).then((response) => { if (response.canceled) return; this.handleOption(currentOptions[response.selection]); }); @@ -57,50 +59,53 @@ export class InstanceEditForm { if (!this.instance.hasLayers()) currentOptions = currentOptions.filter(option => - option !== InstanceEditOptions.SetLayer - && option !== InstanceEditOptions.NextLayer - && option !== InstanceEditOptions.PreviousLayer + option !== InstanceEditButtons.SetLayer + && option !== InstanceEditButtons.NextLayer + && option !== InstanceEditButtons.PreviousLayer ); return currentOptions; } handleOption(option) { switch (option) { - case InstanceEditOptions.EnableInstance: + case InstanceEditButtons.Enable: this.instance.enable(); break; - case InstanceEditOptions.DisableInstance: + case InstanceEditButtons.Disable: this.instance.disable(); break; - case InstanceEditOptions.PlaceInstance: + case InstanceEditButtons.Place: this.instance.place(this.player.dimension.id, this.player.location); break; - case InstanceEditOptions.RenameInstance: + case InstanceEditButtons.Rename: this.renameInstanceForm(); break; - case InstanceEditOptions.DeleteInstance: + case InstanceEditButtons.Delete: structureCollection.delete(this.instanceName); break; - case InstanceEditOptions.NextLayer: + case InstanceEditButtons.NextLayer: this.instance.increaseLayer(); new InstanceEditForm(this.player, this.instanceName); break; - case InstanceEditOptions.PreviousLayer: + case InstanceEditButtons.PreviousLayer: this.instance.decreaseLayer(); new InstanceEditForm(this.player, this.instanceName); break; - case InstanceEditOptions.SetLayer: + case InstanceEditButtons.SetLayer: this.setLayerForm(); break; - case InstanceEditOptions.Move: + case InstanceEditButtons.Move: this.instance.move(this.player.dimension.id, this.player.location); break; - case InstanceEditOptions.Statistics: + case InstanceEditButtons.Statistics: this.statisticsForm(); break; - case InstanceEditOptions.MainMenu: + case InstanceEditButtons.MainMenu: new MenuForm(this.player, { jumpToInstance: false }); break; + case InstanceEditButtons.Settings: + this.settingsForm(); + break; default: this.player.sendMessage(`§cUnknown option: ${option}`); break; @@ -130,8 +135,7 @@ export class InstanceEditForm { InstanceEditFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => { if (response.canceled) return; - const selectedLayer = response.formValues[0]; - this.instance.setLayer(parseInt(selectedLayer)); + this.instance.setLayer(parseInt(response.formValues[0])); }); } @@ -142,4 +146,12 @@ export class InstanceEditForm { this.player.sendMessage(statsForm.stats); }); } + + settingsForm() { + InstanceEditFormBuilder.buildSettings(this.instance).show(this.player).then((response) => { + if (response.canceled) + return; + this.instance.setVerifierEnabled(response.formValues[0]); + }); + } } \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/InstanceEditFormBuilder.js b/StrucTool [BP]/scripts/classes/InstanceEditFormBuilder.js index c98d61a..cd45b8b 100644 --- a/StrucTool [BP]/scripts/classes/InstanceEditFormBuilder.js +++ b/StrucTool [BP]/scripts/classes/InstanceEditFormBuilder.js @@ -2,13 +2,14 @@ import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; import { MenuFormBuilder } from './MenuFormBuilder'; import { StructureVerifier } from './StructureVerifier'; import { StructureStatistics } from './StructureStatistics'; +import { TicksPerSecond } from '@minecraft/server'; export class InstanceEditFormBuilder { static buildInstance(instance, options) { const location = instance.getLocation(); const form = new ActionFormData() .title(MenuFormBuilder.menuTitle) - let body = `Instance: §a${instance.name}\n§fStructure: §2${instance.getStructureId()}\n`; + let body = `Instance: §a${instance.getName()}\n§fStructure: §2${instance.getStructureId()}\n`; if (instance.hasLocation()) body += `§7(${location.location.x} ${location.location.y} ${location.location.z} in ${location.dimensionId})\n`; form.body(body); @@ -36,11 +37,18 @@ export class InstanceEditFormBuilder { static async buildStatistics(instance) { const buildStatisticsForm = new ActionFormData() .title(MenuFormBuilder.menuTitle) - const structureVerifier = new StructureVerifier(instance); + const structureVerifier = new StructureVerifier(instance, { isEnabled: true, trackPlayerDistance: 0, intervalOrLifetime: 30 * TicksPerSecond }); const verification = await structureVerifier.verifyStructure(); const statistics = new StructureStatistics(instance, verification); const statsMessage = statistics.getMessage(); buildStatisticsForm.body(statsMessage); return { form: buildStatisticsForm, stats: statsMessage }; } + + static buildSettings(instance) { + return new ModalFormData() + .title(MenuFormBuilder.menuTitle) + .toggle('Toggle block validation.', instance.options.verifier.isEnabled) + .submitButton('§aApply'); + } } \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/InstanceOptions.js b/StrucTool [BP]/scripts/classes/InstanceOptions.js new file mode 100644 index 0000000..afac847 --- /dev/null +++ b/StrucTool [BP]/scripts/classes/InstanceOptions.js @@ -0,0 +1,91 @@ +import { Vector } from "../lib/Vector"; +import { world } from "@minecraft/server"; + +export class InstanceOptions { + instanceName = void 0; + structureId = void 0; + isEnabled = false; + dimensionId = void 0; + worldLocation = new Vector(); + currentLayer = 0; + verifier = { + isEnabled: true, + trackPlayerDistance: 5, + intervalOrLifetime: 10 + }; + + static getInstanceStrucetureId(instanceName) { + const options = new InstanceOptions(instanceName, void 0); + return options.structureId; + } + + constructor(instanceName, structureId) { + this.instanceName = instanceName; + this.structureId = structureId; + this.load(); + } + + save() { + world.setDynamicProperty(`instanceOptions:${this.instanceName}`, JSON.stringify(this)); + } + + load() { + try { + const options = JSON.parse(world.getDynamicProperty(`instanceOptions:${this.instanceName}`)); + if (options) + Object.assign(this, options); + else + throw new Error("Options not found"); + } catch { + this.save(); + const options = JSON.parse(world.getDynamicProperty(`instanceOptions:${this.instanceName}`) || "{}"); + Object.assign(this, options); + } + this.worldLocation = Vector.from(this.worldLocation); + } + + clear() { + world.setDynamicProperty(`instanceOptions:${this.instanceName}`, void 0); + } + + getDimension() { + return world.getDimension(this.dimensionId); + } + + enable() { + this.isEnabled = true; + this.save(); + } + + disable() { + this.isEnabled = false; + this.save(); + } + + rename(newName) { + this.clear(); + this.instanceName = newName; + this.save(); + } + + move(dimensionId, worldLocation) { + this.dimensionId = dimensionId; + this.worldLocation = Vector.from(worldLocation).floor(); + this.save(); + } + + setLayer(layer) { + this.currentLayer = layer; + this.save(); + } + + setVerifierEnabled(enable) { + this.verifier.isEnabled = enable; + this.save(); + } + + setVerifierDistance(distance) { + this.verifier.trackPlayerDistance = distance; + this.save(); + } +} \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/MaterialCounter.js b/StrucTool [BP]/scripts/classes/MaterialCounter.js index 3c7974c..a880a13 100644 --- a/StrucTool [BP]/scripts/classes/MaterialCounter.js +++ b/StrucTool [BP]/scripts/classes/MaterialCounter.js @@ -4,7 +4,7 @@ class MaterialCounter { static getAll(name) { const structure = structureCollection.get(name); const materials = {}; - for (const block of structure.getBlocks()) { + for (const block of structure.getAllBlocks()) { const typeId = block?.getItemStack()?.typeId.replace('minecraft:', ''); if (!typeId) continue; if (!materials[typeId]) { @@ -32,7 +32,7 @@ class MaterialCounter { static getPrintable(name) { const structure = structureCollection.get(name); const materials = {}; - for (const block of structure.getBlocks()) { + for (const block of structure.getAllBlocks()) { const itemStack = block?.getItemStack(); const typeId = itemStack?.typeId.replace('minecraft:', ''); if (!typeId) continue; diff --git a/StrucTool [BP]/scripts/classes/MenuForm.js b/StrucTool [BP]/scripts/classes/MenuForm.js index 13cc1be..22c6681 100644 --- a/StrucTool [BP]/scripts/classes/MenuForm.js +++ b/StrucTool [BP]/scripts/classes/MenuForm.js @@ -12,7 +12,7 @@ export class MenuForm { async show(jumpToInstance = true) { let instanceName; if (jumpToInstance) { - instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useLayers: false })?.name; + instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useActiveLayer: false })?.getName(); if (instanceName) { new InstanceEditForm(this.player, instanceName); return; diff --git a/StrucTool [BP]/scripts/classes/Outliner.js b/StrucTool [BP]/scripts/classes/Outliner.js index 817604a..6343e58 100644 --- a/StrucTool [BP]/scripts/classes/Outliner.js +++ b/StrucTool [BP]/scripts/classes/Outliner.js @@ -13,8 +13,8 @@ export class Outliner { constructor(dimension, min, max) { this.dimension = dimension; - this.min = new Vector(min.x, min.y, min.z); - this.max = new Vector(max.x, max.y, max.z); + this.min = Vector.from(min); + this.max = Vector.from(max); this.vertices = this.getVertices(min, max); } @@ -65,8 +65,8 @@ export class Outliner { setVertices(dimension, min, max) { this.dimension = dimension; - this.min = new Vector(min.x, min.y, min.z); - this.max = new Vector(max.x, max.y, max.z); + this.min = Vector.from(min); + this.max = Vector.from(max); this.vertices = this.getVertices(min, max); } @@ -103,7 +103,7 @@ export class Outliner { addStandaloneParticles(locations) { for (const location of locations) - this.vertices.push(new Vector(location.x, location.y, location.z)); + this.vertices.push(Vector.from(location)); } getNextParticleColor() { diff --git a/StrucTool [BP]/scripts/classes/Raycaster.js b/StrucTool [BP]/scripts/classes/Raycaster.js index 030b58c..0e36355 100644 --- a/StrucTool [BP]/scripts/classes/Raycaster.js +++ b/StrucTool [BP]/scripts/classes/Raycaster.js @@ -4,13 +4,13 @@ import { world } from "@minecraft/server"; export class Raycaster { static STEP_SIZE = 0.2; - static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useLayers = true }) { + static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useActiveLayer = true }) { // Can probably be optimized by the fact that we only need full blocks and aren't checking for partial blocks const blocks = []; let location = startLocation; let distance = 0; while (distance < maxDistance) { - const structure = structureCollection.getStructure(dimension.id, location, { useLayers }); + const structure = structureCollection.getStructure(dimension.id, location, { useActiveLayer }); if (structure) { const block = structure.getBlock(structure.toStructureCoords(location)); if (block?.type.id !== 'minecraft:air') { @@ -41,11 +41,11 @@ export class Raycaster { return blocks; } - static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, useLayers = true } = {}) { + static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, useActiveLayer = true } = {}) { const startLocation = player.getHeadLocation(); const direction = player.getViewDirection(); const maxDistance = 7; - const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks, useLayers }); + const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks, useActiveLayer }); if (blocks.length === 0) return void 0; return isFirst ? blocks[0] : blocks[blocks.length - 1]; diff --git a/StrucTool [BP]/scripts/classes/Structure.js b/StrucTool [BP]/scripts/classes/Structure.js new file mode 100644 index 0000000..983d6bb --- /dev/null +++ b/StrucTool [BP]/scripts/classes/Structure.js @@ -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(`[StrucTool] 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; + } +} + \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/StructureCollection.js b/StrucTool [BP]/scripts/classes/StructureCollection.js index 42628a4..299067c 100644 --- a/StrucTool [BP]/scripts/classes/StructureCollection.js +++ b/StrucTool [BP]/scripts/classes/StructureCollection.js @@ -1,3 +1,4 @@ +import { InstanceOptions } from './InstanceOptions'; import { StructureInstance } from './StructureInstance'; import { world } from '@minecraft/server'; @@ -8,12 +9,12 @@ class StructureCollection { this.structures = {}; } - loadExistingStructures() { - world.getDynamicPropertyIds().filter(id => id.startsWith('structOptions:')).forEach(id => { - const instanceName = id.replace('structOptions:', ''); + loadExistingInstances() { + world.getDynamicPropertyIds().filter(id => id.startsWith('instanceOptions:')).forEach(id => { + const instanceName = id.replace('instanceOptions:', ''); let structureId; try { - structureId = StructureInstance.parseOptions(instanceName).structureId; + structureId = InstanceOptions.getInstanceStrucetureId(instanceName); this.structures[instanceName] = new StructureInstance(instanceName, structureId); } catch (e) { world.sendMessage(`§c[StrucTool] Error loading structure instance '${instanceName}'. It will be removed.`); @@ -95,5 +96,5 @@ class StructureCollection { export const structureCollection = new StructureCollection(); world.afterEvents.worldLoad.subscribe(() => { - structureCollection.loadExistingStructures(); + structureCollection.loadExistingInstances(); }); \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/StructureInstance.js b/StrucTool [BP]/scripts/classes/StructureInstance.js index a507dae..8cd540b 100644 --- a/StrucTool [BP]/scripts/classes/StructureInstance.js +++ b/StrucTool [BP]/scripts/classes/StructureInstance.js @@ -1,181 +1,29 @@ -import { world } from "@minecraft/server"; -import { Outliner } from "./Outliner"; +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 { - name; - #structure; - #options = { - structureId: void 0, - isEnabled: false, - dimensionId: void 0, - worldLocation: { x: 0, y: 0, z: 0 }, - rotation: 0, - mirror: false, - currentLayer: 0 - }; - outliner = void 0; + options; + structure = void 0; verifier = void 0; + outliner = void 0; constructor(instanceName, structureId) { - this.name = instanceName; - this.#structure = world.structureManager.get(structureId); - if (!this.#structure) - throw new Error(`[StrucTool] Structure '${structureId}' not found.`); - this.#structure.saveToWorld(); - this.#options = this.loadOptions(); - this.#options.structureId = structureId; - this.updateOptions(); + this.structure = new Structure(structureId); + this.options = new InstanceOptions(instanceName, structureId); this.refreshBox(); } - loadOptions() { - try { - return JSON.parse(world.getDynamicProperty(`structOptions:${this.name}`)); - } catch (e) { - world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options)); - } - return this.#options; - } - delete() { this.disable(); - world.setDynamicProperty(`structOptions:${this.name}`, void 0); - this.#structure = void 0; - this.#options = void 0; + delete this.options; + delete this.structure; delete this.outliner; - } - - static parseOptions(instanceName) { - const options = JSON.parse(world.getDynamicProperty(`structOptions:${instanceName}`)); - if (!options) - throw new Error(`[StrucTool] Instance '${instanceName}' not found.`); - return options; - } - - updateOptions() { - world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options)); - } - - getStructure() { - return this.#structure; - } - - getStructureId() { - return this.#options.structureId; - } - - getLocation() { - return { dimensionId: this.#options.dimensionId, location: this.#options.worldLocation }; - } - - getHeight() { - return this.#structure.size.y; - } - - getLayer() { - return this.#options.currentLayer || 0; - } - - getDimension() { - let dimension; - try { - dimension = world.getDimension(this.#options.dimensionId); - } catch (e) { - dimension = world.getDimension("minecraft:overworld"); - } - return dimension; - } - - getBlock(structureLocation) { - return this.#structure.getBlockPermutation(structureLocation); - } - - *getBlocks() { - const max = this.#structure.size; - for (let y = 0; y < max.y; y++) { - yield * this.getLayerBlocks(y); - } - } - - *getLayerBlocks(y) { - const max = this.#structure.size; - for (let x = 0; x < max.x; x++) { - for (let z = 0; z < max.z; z++) { - const blockPermutation = this.#structure.getBlockPermutation({ x, y, z }); - if (!blockPermutation) - yield void 0; - blockPermutation.location = { x, y, z }; - yield blockPermutation; - } - } - } - - getBounds() { - return { - min: { x: 0, y: 0, z: 0 }, - max: this.#structure.size - }; - } - - getLayeredBounds() { - if (!this.#options.isEnabled) - throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); - return { - min: { x: 0, y: this.#options.currentLayer - 1, z: 0 }, - max: { x: this.#structure.size.x, y: this.#options.currentLayer, z: this.#structure.size.z } - }; - } - - getTotalVolume() { - return this.#structure.size.x * this.#structure.size.y * this.#structure.size.z; - } - - getActiveVolume() { - if (!this.#options.isEnabled) - return 0; - if (this.#options.currentLayer === 0) - return this.getTotalVolume(); - return this.#structure.size.x * this.#structure.size.z; - } - - rename(newName) { - world.setDynamicProperty(`structOptions:${this.name}`, void 0); - this.name = newName; - world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options)); - } - - place(dimensionId, worldLocation) { - this.move(dimensionId, worldLocation); - this.enable(); - } - - enable() { - this.#options.isEnabled = true; - this.updateOptions(); - this.refreshBox(); - } - - disable() { - this.#options.isEnabled = false; - this.updateOptions(); - this.refreshBox(); - } - - move(dimensionId, location) { - this.#options.dimensionId = dimensionId; - this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }; - this.updateOptions(); - this.refreshBox(); - } - - setLayer(layer) { - if (layer < 0 || layer > this.#structure.size.y) - throw new Error(`[StrucTool] Layer ${layer} is out of bounds.`); - this.#options.currentLayer = layer; - this.updateOptions(); - this.refreshBox(); + delete this.verifier; + this.options.clear(); } refreshBox() { @@ -184,92 +32,193 @@ export class StructureInstance { if (!this.outliner) this.outliner = new StructureOutliner(this); if (!this.verifier) - this.verifier = new StructureVerifier(this, { shouldRender: true }); + this.verifier = new StructureVerifier(this, { isEnabled: this.options.verifier.isEnabled, trackPlayerDistance: this.options.verifier.trackPlayerDistance }); this.outliner.refresh(); this.verifier.refresh(); } - isLocationInStructure(dimensionId, structureLocation) { - if (this.#options.dimensionId !== dimensionId) - return false - const { min, max } = this.getBounds(); - return structureLocation.x >= min.x && structureLocation.x < max.x - && structureLocation.y >= min.y && structureLocation.y < max.y - && structureLocation.z >= min.z && structureLocation.z < max.z; + getName() { + return this.options.instanceName; } - isLocationInLayer(dimensionId, structureLocation) { - if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId) - return false - const { min, max } = this.getLayeredBounds(true); - return structureLocation.x >= min.x && structureLocation.x < max.x - && structureLocation.y >= min.y && structureLocation.y < max.y - && structureLocation.z >= min.z && structureLocation.z < max.z; + getStructureId() { + return this.options.structureId; } - isLocationActive(dimensionId, structureLocation, { useLayers = true } = {}) { - if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId) - return false - if (useLayers && this.#options.currentLayer !== 0) - return this.isLocationInLayer(dimensionId, structureLocation); - return this.isLocationInStructure(dimensionId, structureLocation); + getLocation() { + return { dimensionId: this.options.dimensionId, location: this.options.worldLocation }; } - toGlobalCoords(structureLocation) { + getDimension() { + return this.options.getDimension(); + } + + getMaxLayer() { + return this.structure.getHeight(); + } + + getLayer() { + return this.options.currentLayer; + } + + getBounds() { return { - x: this.#options.worldLocation.x + structureLocation.x, - y: this.#options.worldLocation.y + structureLocation.y, - z: this.#options.worldLocation.z + structureLocation.z + min: this.structure.getMin(), + max: this.structure.getMax() + } + } + + getActiveBounds() { + if (!this.options.isEnabled) + throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`); + if (this.hasLayerSelected()) + return this.getLayeredBounds(); + return this.getBounds(); + } + + getLayeredBounds() { + if (!this.options.isEnabled) + throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`); + const min = this.structure.getMin(); + const max = this.structure.getMax(); + return { + min: new Vector(min.x, this.options.currentLayer - 1, min.z), + max: new Vector(max.x, this.options.currentLayer, max.z) }; } - toStructureCoords(worldLocation) { - return { - x: worldLocation.x - this.#options.worldLocation.x, - y: worldLocation.y - this.#options.worldLocation.y, - z: worldLocation.z - this.#options.worldLocation.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(); + } + + 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(`[StrucTool] 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; + return this.options.isEnabled; } hasLocation() { - return this.#options.dimensionId && this.#options.worldLocation.x !== 0 && this.#options.worldLocation.y !== 0 && this.#options.worldLocation.z !== 0; - } - - isUsingLayers() { - return this.hasLayers() && this.#options.currentLayer !== 0; + return this.options.dimensionId && this.options.worldLocation.x !== 0 && this.options.worldLocation.y !== 0 && this.options.worldLocation.z !== 0; } hasLayers() { - return this.#structure.size.y > 1; + 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.#structure.size.y; + return !this.hasLayers || this.options.currentLayer >= this.getMaxLayer(); } isAtMinLayer() { - return !this.hasLayers || this.#options.currentLayer <= 0; + 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(`[StrucTool] 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); + this.setLayer(this.options.currentLayer + 1); } decreaseLayer() { if (this.isAtMinLayer()) - this.setLayer(this.#structure.size.y); + this.setLayer(this.getMaxLayer()); else - this.setLayer(this.#options.currentLayer - 1); + this.setLayer(this.options.currentLayer - 1); } - getDimension() { - return world.getDimension(this.#options.dimensionId); + toGlobalCoords(structureLocation) { + return Vector.from(structureLocation).add(this.options.worldLocation); + } + + toStructureCoords(worldLocation) { + return Vector.from(worldLocation).subtract(this.options.worldLocation); } } \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/StructureOutliner.js b/StrucTool [BP]/scripts/classes/StructureOutliner.js index a11cbc6..65648fa 100644 --- a/StrucTool [BP]/scripts/classes/StructureOutliner.js +++ b/StrucTool [BP]/scripts/classes/StructureOutliner.js @@ -30,7 +30,7 @@ export class StructureOutliner { this.outliner.stopDraw(); if (!this.instance.isEnabled()) return; - if (this.instance.isUsingLayers()) + if (this.instance.hasLayerSelected()) this.layeredDraw(); else this.boxDraw(); diff --git a/StrucTool [BP]/scripts/classes/StructureStatistics.js b/StrucTool [BP]/scripts/classes/StructureStatistics.js index d1612d4..0b09187 100644 --- a/StrucTool [BP]/scripts/classes/StructureStatistics.js +++ b/StrucTool [BP]/scripts/classes/StructureStatistics.js @@ -31,7 +31,8 @@ export class StructureStatistics { } getNonAirBlocks() { - return this.instance.getActiveVolume() - this.verification.correctlyAir; + const activeBounds = this.instance.getActiveBounds(); + return activeBounds.min.volume(activeBounds.max) - this.verification.correctlyAir; } getStat(blockVerificationLevel) { @@ -44,8 +45,8 @@ export class StructureStatistics { getMessage() { let message = ''; - message += `§fStatistics for §a${this.instance.name}§f:`; - if (this.instance.isUsingLayers()) + 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(); diff --git a/StrucTool [BP]/scripts/classes/StructureVerifier.js b/StrucTool [BP]/scripts/classes/StructureVerifier.js index 1d9d712..441b629 100644 --- a/StrucTool [BP]/scripts/classes/StructureVerifier.js +++ b/StrucTool [BP]/scripts/classes/StructureVerifier.js @@ -2,23 +2,37 @@ 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 { - shouldRender; - isComplete; - interval = 5*20; - #runner; + instance; + intervalOrLifetime; - constructor(instance, { shouldRender = false } = {}) { - this.shouldRender = shouldRender; + locationsToVerify; + blockVerificationLevels; + isLocationPopulationComplete; + isVerificationComplete; + + #runner; + #verifyJob; + #populateJob = {}; + + constructor(instance, { isEnabled = false, trackPlayerDistance = 0, intervalOrLifetime = 10 } = {}) { this.instance = instance; - this.interval = Math.max(instance.getActiveVolume() / 50, 20); + 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.interval); + }, this.intervalOrLifetime); } stopContinuousVerification() { @@ -35,50 +49,97 @@ export class StructureVerifier { this.startContinuousVerification(); } - init(shouldRender) { - this.blockVerificationLevels = { correctlyAir: 0 }; - this.shouldRender = shouldRender; - this.isComplete = false; + isEnabled() { + return this.instance.options.verifier.isEnabled; } - async verifyStructure() { - this.init(this.shouldRender); - return new Promise((resolve) => { - if (this.instance.isUsingLayers()) - system.runJob(this.verifyBlocks(this.instance.getLayerBlocks(this.instance.getLayer()-1))); - else - system.runJob(this.verifyBlocks(this.instance.getBlocks())); + 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.isComplete) { + 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(blocks) { - for (const block of blocks) { - const verificationLevel = this.verifyBlock(block.location); + *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(block.location)] = verificationLevel; - if (this.shouldRender) { - const dimensionLocation = { dimension: this.instance.getDimension(), location: this.instance.toGlobalCoords(block.location) }; - new BlockVerificationLevelRender(dimensionLocation, verificationLevel, this.interval/TicksPerSecond); + 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.isComplete = true; + this.isVerificationComplete = true; } verifyBlock(location) { const worldBlock = this.instance.getDimension().getBlock(this.instance.toGlobalCoords(location)); - if (!worldBlock) { + if (!worldBlock) return BlockVerificationLevel.Skipped; - } const blockVerifier = new BlockVerifier(worldBlock, this.instance); return blockVerifier.verify(); } diff --git a/StrucTool [BP]/scripts/classes/enums/InstanceEditButtons.js b/StrucTool [BP]/scripts/classes/enums/InstanceEditButtons.js new file mode 100644 index 0000000..1fafa5b --- /dev/null +++ b/StrucTool [BP]/scripts/classes/enums/InstanceEditButtons.js @@ -0,0 +1,15 @@ +export const InstanceEditButtons = Object.freeze({ + Unknown: 'Unknown', + MainMenu: '<<', + Place: '§aPlace Instance', + Enable: '§aEnable Instance', + Disable: '§cDisable Instance', + Rename: 'Rename Instance', + Delete: '§cDelete Instance', + NextLayer: 'Increase Layer', + PreviousLayer: 'Decrease Layer', + SetLayer: 'Set Layer', + Move: 'Move Here', + Statistics: 'Statistics', + Settings: 'Settings' +}); \ No newline at end of file diff --git a/StrucTool [BP]/scripts/classes/enums/InstanceEditOptions.js b/StrucTool [BP]/scripts/classes/enums/InstanceEditOptions.js deleted file mode 100644 index d74468d..0000000 --- a/StrucTool [BP]/scripts/classes/enums/InstanceEditOptions.js +++ /dev/null @@ -1,14 +0,0 @@ -export const InstanceEditOptions = Object.freeze({ - Unknown: 'Unknown', - MainMenu: '<<', - PlaceInstance: '§aPlace Instance', - EnableInstance: '§aEnable Instance', - DisableInstance: '§cDisable Instance', - RenameInstance: 'Rename Instance', - DeleteInstance: '§cDelete Instance', - NextLayer: 'Increase Layer', - PreviousLayer: 'Decrease Layer', - SetLayer: 'Set Layer', - Move: 'Move Here', - Statistics: 'Statistics', -}); \ No newline at end of file diff --git a/StrucTool [BP]/scripts/lib/Vector.js b/StrucTool [BP]/scripts/lib/Vector.js index 85fe960..b0f4391 100644 --- a/StrucTool [BP]/scripts/lib/Vector.js +++ b/StrucTool [BP]/scripts/lib/Vector.js @@ -1,6 +1,5 @@ /** - * Part of ItemStack Database by @gameza_src - * Unknown author + * Unknown author, with additions. */ const isVec3Symbol = Symbol("isVec3"); export function Vector(x = 0, y = 0, z = 0) { @@ -23,6 +22,10 @@ Vector.multiply = function multiply(vec, num) { } Vector.isVec3 = function isVec3(vec) { return vec[isVec3Symbol] === true; } Vector.floor = function floor(vec) { return { x: Math.floor(vec.x), y: Math.floor(vec.y), z: Math.floor(vec.z), __proto__: Vector.prototype }; } +Vector.volume = function volume(a, b) { + const [min, max] = Vector.sort(a, b); + return (max.x - min.x) * (max.y - min.y) * (max.z - min.z); +} Vector.projection = function projection(a, b) { return Vector.multiply(b, Vector.dot(a, b) / ((b.x * b.x + b.y * b.y + b.z * b.z) ** 2)); } Vector.rejection = function rejection(a, b) { return Vector.subtract(a, Vector.projection(a, b)); } Vector.reflect = function reflect(v, n) { return Vector.subtract(v, Vector.multiply(n, 2 * Vector.dot(v, n))); } @@ -56,6 +59,7 @@ Vector.prototype = { cross(vec) { return Vector.cross(this, vec); }, dot(vec) { return Vector.dot(this, vec); }, floor() { return Vector.floor(this); }, + volume(vec) { return Vector.volume(this, vec); }, add(vec) { return Vector.add(this, vec); }, subtract(vec) { return Vector.subtract(this, vec); }, multiply(num) { return Vector.multiply(this, num); },