diff --git a/scripts/classes/BlockVerificationLevel.js b/scripts/classes/BlockVerificationLevel.js new file mode 100644 index 0000000..cdc5a32 --- /dev/null +++ b/scripts/classes/BlockVerificationLevel.js @@ -0,0 +1,8 @@ +export const BlockVerificationLevel = Object.freeze({ + Unknown: 0, + NoMatch: 1, + TypeMatch: 2, + TypeAndStateMatch: 3, + Missing: 4, + isAir: 5 +}); \ No newline at end of file diff --git a/scripts/classes/BlockVerifier.js b/scripts/classes/BlockVerifier.js new file mode 100644 index 0000000..5ac24e9 --- /dev/null +++ b/scripts/classes/BlockVerifier.js @@ -0,0 +1,62 @@ +import { BlockVerificationLevel } from "./BlockVerificationLevel"; + +export class BlockVerifier { + constructor(block, instance) { + this.block = block; + this.instance = instance; + this.blockLocationInStructure = instance.toStructureCoords(block.location); + } + + verify() { + const structPermutation = this.instance.getBlock(this.blockLocationInStructure); + return this.evaluatePermutations(this.block.permutation, structPermutation); + } + + evaluatePermutations(worldPermutation, structPermutation) { + if (this.isCorrectlyAir(worldPermutation, structPermutation)) + return this.air(); + if (this.isMissing(worldPermutation, structPermutation)) + return this.missing(); + if (this.isExactMatch(worldPermutation, structPermutation)) + return this.matchingPermutations(); + if (this.isTypeMatch(worldPermutation, structPermutation)) + return this.matchingTypes(); + return this.matchingNone(); + } + + isCorrectlyAir(worldPermutation, structPermutation) { + return worldPermutation.type.id === "minecraft:air" && structPermutation.type.id === "minecraft:air"; + } + + isMissing(worldPermutation, structPermutation) { + return worldPermutation.type.id === "minecraft:air" && structPermutation.type.id !== "minecraft:air"; + } + + isTypeMatch(worldPermuation, structurePermuation) { + return worldPermuation.type.id === structurePermuation.type.id; + } + + isExactMatch(worldPermuation, structurePermuation) { + return worldPermuation.matches(structurePermuation.type.id, structurePermuation.getAllStates()); + } + + air() { + return BlockVerificationLevel.isAir; + } + + missing() { + return BlockVerificationLevel.Missing; + } + + matchingPermutations() { + return BlockVerificationLevel.TypeAndStateMatch; + } + + matchingTypes() { + return BlockVerificationLevel.TypeMatch; + } + + matchingNone() { + return BlockVerificationLevel.NoMatch; + } +} \ No newline at end of file diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js index c4acdb7..928a281 100644 --- a/scripts/classes/InstanceEditForm.js +++ b/scripts/classes/InstanceEditForm.js @@ -11,6 +11,7 @@ export class InstanceEditForm { InstanceEditOptions.PreviousLayer, InstanceEditOptions.SetLayer, InstanceEditOptions.Move, + InstanceEditOptions.Statistics, InstanceEditOptions.RenameInstance, InstanceEditOptions.DisableInstance, ], @@ -93,6 +94,9 @@ export class InstanceEditForm { case InstanceEditOptions.Move: this.instance.move(this.player.dimension.id, this.player.location); break; + case InstanceEditOptions.Statistics: + this.statisticsForm(); + break; case InstanceEditOptions.MainMenu: new MenuForm(this.player, { jumpToInstance: false }); break; @@ -104,7 +108,8 @@ export class InstanceEditForm { renameInstanceForm() { InstanceEditFormBuilder.buildRenameInstance(this.instanceName).show(this.player).then((response) => { - if (response.canceled) return; + if (response.canceled) + return; const newName = response.formValues[0]; if (newName === '') { this.player.sendMessage('§cInstance name cannot be empty.'); @@ -122,9 +127,15 @@ export class InstanceEditForm { setLayerForm() { InstanceEditFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => { - if (response.canceled) return; + if (response.canceled) + return; const selectedLayer = response.formValues[0]; this.instance.setLayer(parseInt(selectedLayer)); }); } + + async statisticsForm() { + const form = await InstanceEditFormBuilder.buildStatistics(this.instance) + form.show(this.player); + } } \ No newline at end of file diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js index 118a062..3564309 100644 --- a/scripts/classes/InstanceEditFormBuilder.js +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -1,5 +1,7 @@ import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; import { MenuFormBuilder } from './MenuFormBuilder'; +import { StructureVerifier } from './StructureVerifier'; +import { BlockVerificationLevel } from './BlockVerificationLevel'; export class InstanceEditFormBuilder { static buildInstance(instance, options) { @@ -30,4 +32,24 @@ export class InstanceEditFormBuilder { .slider("Layer", 0, maxLayer, 1, currentLayer) .submitButton('Set Layer'); } + + static async buildStatistics(instance) { + const buildStatisticsForm = new ActionFormData() + .title(MenuFormBuilder.menuTitle) + let message = ''; + const structureVerifier = new StructureVerifier(instance); + const statistics = await structureVerifier.verifyStructure(); + message += `§fStatistics for §a${instance.name}§f:\n`; + message += `§7Blocks: §2${instance.getTotalVolume() - statistics.correctlyAir}\n`; + message += `§7Correct: §a${this.getFormattedStatistic(statistics, BlockVerificationLevel.TypeAndStateMatch)}\n`; + message += `§7Block State Incorrect: §e${this.getFormattedStatistic(statistics, BlockVerificationLevel.TypeMatch)}\n`; + message += `§7Incorrect: §c${this.getFormattedStatistic(statistics, BlockVerificationLevel.NoMatch)}\n`; + message += `§7Missing: §c${this.getFormattedStatistic(statistics, BlockVerificationLevel.Missing)}\n`; + buildStatisticsForm.body(message); + return buildStatisticsForm; + } + + static getFormattedStatistic(statistics, blockVerificationLevel) { + return `${statistics[blockVerificationLevel]} (${statistics.percentages[blockVerificationLevel].toFixed(2)}%%)`; + } } \ No newline at end of file diff --git a/scripts/classes/InstanceEditOptions.js b/scripts/classes/InstanceEditOptions.js index 7b4fa3b..d74468d 100644 --- a/scripts/classes/InstanceEditOptions.js +++ b/scripts/classes/InstanceEditOptions.js @@ -9,5 +9,6 @@ export const InstanceEditOptions = Object.freeze({ NextLayer: 'Increase Layer', PreviousLayer: 'Decrease Layer', SetLayer: 'Set Layer', - Move: 'Move Here' + Move: 'Move Here', + Statistics: 'Statistics', }); \ No newline at end of file diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js index e049411..390377a 100644 --- a/scripts/classes/MenuFormBuilder.js +++ b/scripts/classes/MenuFormBuilder.js @@ -45,8 +45,8 @@ export class MenuFormBuilder { static buildHowTo() { let body = "§aHow to Add Structures:\n" body += "§7- Save a structure using a §fstructure block§7 or the §f/structure§7 command.\n" - body += "§f§lOR§r\n" - body += "§7- Add a .mcstructure file to this pack's structures folder. When selecting your structure, select the 'Other' option and then use the filename (without '.mcstructure') as the Structure ID. After its first use, it will be added to the list of structures."; + body += "§7§lOR§r\n" + body += "§7- Add a §f.mcstructure§7 file to this pack's §fstructures folder§7. When selecting your structure, select the §fOther§7 option and then use the filename (without '.mcstructure') as the §fStructure ID§7. After its first use, it will be added to the list of structures."; body += "\n\n§cHow to Remove Structures:\n" body += "§7- Use the §f/structure delete§7 command to remove a structure from the world.\n" return new ActionFormData() diff --git a/scripts/classes/StructureInstance.js b/scripts/classes/StructureInstance.js index 00d2696..82e3fc7 100644 --- a/scripts/classes/StructureInstance.js +++ b/scripts/classes/StructureInstance.js @@ -86,14 +86,14 @@ export class StructureInstance { return dimension; } + getBlock(structureLocation) { + return this.#structure.getBlockPermutation(structureLocation); + } + *getBlocks() { const max = this.#structure.size; - for (let x = 0; x < max.x; x++) { - for (let y = 0; y < max.y; y++) { - for (let z = 0; z < max.z; z++) { - yield this.#structure.getBlockPermutation({ x, y, z }); - } - } + for (let y = 0; y < max.y; y++) { + yield * this.getLayerBlocks(y); } } @@ -101,15 +101,13 @@ export class StructureInstance { const max = this.#structure.size; for (let x = 0; x < max.x; x++) { for (let z = 0; z < max.z; z++) { - yield this.#structure.getBlockPermutation({ x, y, z }); + const blockPermutation = this.#structure.getBlockPermutation({ x, y, z }); + blockPermutation.location = { x, y, z }; + yield blockPermutation; } } } - getBlock(structureLocation) { - return this.#structure.getBlockPermutation(structureLocation); - } - getBounds() { return { min: { x: 0, y: 0, z: 0 }, @@ -126,6 +124,10 @@ export class StructureInstance { }; } + getTotalVolume() { + return this.#structure.size.x * this.#structure.size.y * this.#structure.size.z; + } + rename(newName) { world.setDynamicProperty(`structOptions:${this.name}`, void 0); this.name = newName; @@ -247,4 +249,8 @@ export class StructureInstance { else this.setLayer(this.#options.currentLayer - 1); } + + getDimension() { + return world.getDimension(this.#options.dimensionId); + } } \ No newline at end of file diff --git a/scripts/classes/StructureVerifier.js b/scripts/classes/StructureVerifier.js new file mode 100644 index 0000000..10ad29a --- /dev/null +++ b/scripts/classes/StructureVerifier.js @@ -0,0 +1,65 @@ +import { BlockVerifier } from "./BlockVerifier"; +import { BlockVerificationLevel } from "./BlockVerificationLevel"; + +export class StructureVerifier { + constructor(instance) { + this.instance = instance; + this.blockVerificationLevels = {}; + this.statistics = {}; + this.initStatistics(); + } + + async verifyStructure() { + await this.runVerifier(); + this.parseStatistics(); + return this.statistics; + } + + async runVerifier() { + return new Promise((resolve) => { + this.verifyBlocks(); + resolve(); + }); + } + + verifyBlocks() { + for (const block of this.instance.getBlocks()) { + const verificationLevel = this.verifyBlock(block.location); + if (verificationLevel !== BlockVerificationLevel.isAir) + this.blockVerificationLevels[JSON.stringify(block.location)] = verificationLevel; + else + this.statistics.correctlyAir++; + } + } + + verifyBlock(location) { + const worldBlock = this.instance.getDimension().getBlock(this.instance.toGlobalCoords(location)); + if (!worldBlock) + throw new Error(`Block at ${JSON.stringify(location)} could not be accessed.`); + const blockVerifier = new BlockVerifier(worldBlock, this.instance); + return blockVerifier.verify(); + } + + initStatistics() { + for (const verificationlevel of Object.values(BlockVerificationLevel)) { + this.statistics[verificationlevel] = 0; + } + this.statistics.percentages = {}; + this.statistics.correctlyAir = 0; + } + + parseStatistics() { + for (const blockVerificationLevel of Object.values(BlockVerificationLevel)) { + this.parseStatistic(blockVerificationLevel); + } + } + + parseStatistic(blockVerificationLevel) { + for (const verificationlevel of Object.values(this.blockVerificationLevels)) { + if (blockVerificationLevel === verificationlevel) { + this.statistics[verificationlevel]++; + } + } + this.statistics.percentages[blockVerificationLevel] = this.statistics[blockVerificationLevel] / (this.instance.getTotalVolume() - this.statistics.correctlyAir) * 100; + } +} \ No newline at end of file