configure for regolith
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { GameMode, system, world } from '@minecraft/server';
|
||||
import { Raycaster } from '../classes/Raycaster';
|
||||
import { fetchMatchingItemSlot } from '../utils';
|
||||
|
||||
class BlockInfo {
|
||||
static shownToLastTick = new Set();
|
||||
|
||||
static onTick() {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
if (!player)
|
||||
continue;
|
||||
this.showStructureBlockInfo(player);
|
||||
}
|
||||
}
|
||||
|
||||
static showStructureBlockInfo(player) {
|
||||
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);
|
||||
}
|
||||
if (!block)
|
||||
return;
|
||||
player.onScreenDisplay.setActionBar({ text: this.getFormattedBlockInfo(player, block.permutation) });
|
||||
this.shownToLastTick.add(player.id);
|
||||
}
|
||||
|
||||
static getFormattedBlockInfo(player, block) {
|
||||
return 'Structure:' + this.getSupplyMessage(player, block) + '\n' + this.getBlockMessage(block);
|
||||
}
|
||||
|
||||
static getBlockMessage(block) {
|
||||
if (!block)
|
||||
return '§7Unknown';
|
||||
const states = block.getAllStates();
|
||||
if (Object.keys(states).length === 0)
|
||||
return `§a${block.type.id}`;
|
||||
else
|
||||
return `§a${block.type.id}\n§7${this.getFormattedStates(states)}`;
|
||||
}
|
||||
|
||||
static getFormattedStates(states) {
|
||||
return Object.entries(states).map(([key, value]) => `§7${key}: §3${value}`).join('\n');
|
||||
}
|
||||
|
||||
static getSupplyMessage(player, block) {
|
||||
const itemStack = fetchMatchingItemSlot(player, block.getItemStack()?.typeId);
|
||||
const isInSurvival = player.getGameMode() === GameMode.Survival;
|
||||
if (!itemStack && isInSurvival)
|
||||
return ' §c[No Supply]';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
system.runInterval(() => BlockInfo.onTick());
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BuilderOptions } from "./BuilderOptions";
|
||||
|
||||
export class Builder {
|
||||
playerId;
|
||||
materialInstanceName = void 0;
|
||||
|
||||
constructor(playerId) {
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
isOptionEnabled(optionId) {
|
||||
return BuilderOptions.isEnabled(optionId, this.playerId);
|
||||
}
|
||||
|
||||
setOption(optionId, value) {
|
||||
return BuilderOptions.setValue(optionId, this.playerId, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BuilderFormBuilder } from "./BuilderFormBuilder";
|
||||
import { BuilderOptions } from "./BuilderOptions";
|
||||
import { forceShow } from '../../utils';
|
||||
|
||||
export class BuilderForm {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
forceShow(this.player, BuilderFormBuilder.buildBuilderOptions(this.player)).then((response) => {
|
||||
if (response.canceled) return;
|
||||
this.applySettings(response.formValues);
|
||||
});
|
||||
}
|
||||
|
||||
applySettings(formValues) {
|
||||
const optionIds = BuilderOptions.getOptionIds();
|
||||
for (let i = 0; i < optionIds.length; i++) {
|
||||
const option = BuilderOptions.get(optionIds[i]);
|
||||
const changedToValue = option.setValue(this.player.id, formValues[i]);
|
||||
if (changedToValue === true)
|
||||
this.player.sendMessage(`§a${option.displayName} is now enabled!§7 ${option.howToUse}`);
|
||||
else if (changedToValue === false)
|
||||
this.player.sendMessage(`§c${option.displayName} is now disabled.`)
|
||||
}
|
||||
}
|
||||
|
||||
valueChangedToEnabled(hasChanged, newValue) {
|
||||
return hasChanged && newValue === true;
|
||||
}
|
||||
|
||||
valueChangedToDisabled(hasChanged, newValue) {
|
||||
return hasChanged && newValue === false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ModalFormData } from "@minecraft/server-ui";
|
||||
import { MenuFormBuilder } from "../MenuFormBuilder";
|
||||
import { BuilderOptions } from "./BuilderOptions";
|
||||
|
||||
export class BuilderFormBuilder {
|
||||
static buildBuilderOptions(player) {
|
||||
const form = new ModalFormData()
|
||||
.title(MenuFormBuilder.menuTitle);
|
||||
for (const optionId of BuilderOptions.getOptionIds()) {
|
||||
const option = BuilderOptions.get(optionId);
|
||||
form.toggle(`${option.displayName}`, { defaultValue: option.isEnabled(player.id), tooltip: option.description });
|
||||
}
|
||||
form.submitButton('§2Apply');
|
||||
return form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BuilderOptions } from "./BuilderOptions";
|
||||
import { world } from "@minecraft/server";
|
||||
|
||||
export class BuilderOption {
|
||||
identifier;
|
||||
displayName;
|
||||
description;
|
||||
howToUse;
|
||||
#onEnable;
|
||||
#onDisable;
|
||||
#DP_NAMESPACE = "builderOptions";
|
||||
|
||||
constructor({ identifier, displayName, description, howToUse, onEnableCallback = () => {}, onDisableCallback = () => {} }) {
|
||||
this.identifier = identifier;
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
this.howToUse = howToUse;
|
||||
this.#onEnable = onEnableCallback;
|
||||
this.#onDisable = onDisableCallback;
|
||||
BuilderOptions.add(this);
|
||||
}
|
||||
|
||||
isEnabled(playerId) {
|
||||
return world.getDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`) === true;
|
||||
}
|
||||
|
||||
setValue(playerId, value) {
|
||||
if (this.isEnabled(playerId) !== value) {
|
||||
this.save(playerId, value);
|
||||
if (value)
|
||||
this.#onEnable(playerId);
|
||||
else
|
||||
this.#onDisable(playerId);
|
||||
return value;
|
||||
}
|
||||
this.save(playerId, value);
|
||||
return void 0;
|
||||
}
|
||||
|
||||
save(playerId, value) {
|
||||
world.setDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export class BuilderOptions {
|
||||
static options = {};
|
||||
|
||||
static add(builderOption) {
|
||||
this.options[builderOption.identifier] = builderOption;
|
||||
}
|
||||
|
||||
static get(optionId) {
|
||||
return this.options[optionId];
|
||||
}
|
||||
|
||||
static getOptionIds() {
|
||||
return Object.keys(this.options).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
static isEnabled(optionId, playerId) {
|
||||
return this.options[optionId].isEnabled(playerId);
|
||||
}
|
||||
|
||||
static setValue(optionId, playerId, value) {
|
||||
return this.options[optionId].setValue(playerId, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { world } from "@minecraft/server";
|
||||
import { Builder } from "./Builder";
|
||||
|
||||
export class Builders {
|
||||
static builders = {};
|
||||
|
||||
static add(playerId) {
|
||||
if (this.builders[playerId])
|
||||
return;
|
||||
this.builders[playerId] = new Builder(playerId);
|
||||
}
|
||||
|
||||
static remove(playerId) {
|
||||
delete this.builders[playerId];
|
||||
}
|
||||
|
||||
static get(id) {
|
||||
return this.builders[id];
|
||||
}
|
||||
|
||||
static onJoin(playerId) {
|
||||
this.add(playerId);
|
||||
}
|
||||
|
||||
static onLeave(playerId) {
|
||||
this.remove(playerId);
|
||||
}
|
||||
}
|
||||
|
||||
world.afterEvents.playerJoin.subscribe((event) => Builders.onJoin(event.playerId));
|
||||
world.beforeEvents.playerLeave.subscribe((event) => {
|
||||
if (!event.player)
|
||||
return;
|
||||
Builders.onLeave(event.player.id);
|
||||
});
|
||||
world.afterEvents.worldLoad.subscribe((event) => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
Builders.onJoin(player.id);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export const BlockVerificationLevel = Object.freeze({
|
||||
Unknown: 0,
|
||||
NoMatch: 1,
|
||||
TypeMatch: 2,
|
||||
Match: 3,
|
||||
Missing: 4,
|
||||
Air: 5,
|
||||
Skipped: 6
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
export const InstanceButtons = 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',
|
||||
Move: 'Move Here',
|
||||
Statistics: 'Statistics',
|
||||
Settings: 'Settings',
|
||||
Materials: 'Material List'
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export class InstanceNotPlacedError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'InstanceNotPlacedError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class InvalidInstanceError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'InvalidInstanceError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class InvalidStructureError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'InvalidStructureError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { structureCollection } from '../Structure/StructureCollection';
|
||||
import { MenuForm } from '../MenuForm';
|
||||
import { forceShow } from '../../utils';
|
||||
import { InstanceButtons } from '../Enums/InstanceButtons';
|
||||
import { InstanceFormBuilder } from './InstanceFormBuilder';
|
||||
import { FormCancelationReason } from '@minecraft/server-ui';
|
||||
|
||||
export class InstanceForm {
|
||||
instanceName;
|
||||
#buttons = {
|
||||
isEnabled: [
|
||||
InstanceButtons.NextLayer,
|
||||
InstanceButtons.PreviousLayer,
|
||||
InstanceButtons.Move,
|
||||
InstanceButtons.Settings,
|
||||
InstanceButtons.Statistics,
|
||||
InstanceButtons.Materials,
|
||||
InstanceButtons.Rename,
|
||||
InstanceButtons.Disable
|
||||
],
|
||||
isNotEnabledAndIsNotPlaced: [
|
||||
InstanceButtons.Place,
|
||||
InstanceButtons.Rename
|
||||
],
|
||||
isNotEnabledButIsPlaced: [
|
||||
InstanceButtons.Enable,
|
||||
InstanceButtons.Rename
|
||||
],
|
||||
common: [
|
||||
InstanceButtons.Delete,
|
||||
InstanceButtons.MainMenu
|
||||
]
|
||||
}
|
||||
|
||||
constructor(player, instanceName) {
|
||||
this.player = player;
|
||||
this.instanceName = instanceName;
|
||||
this.instance = structureCollection.get(this.instanceName);
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
const currentOptions = this.getActiveOptions();
|
||||
forceShow(this.player, InstanceFormBuilder.buildInstance(this.instance, currentOptions)).then((response) => {
|
||||
if (response.canceled) return;
|
||||
this.handleOption(currentOptions[response.selection]);
|
||||
});
|
||||
}
|
||||
|
||||
getActiveOptions() {
|
||||
let currentOptions = [];
|
||||
if (this.instance.isEnabled())
|
||||
currentOptions = this.#buttons.isEnabled;
|
||||
else if (this.instance.hasLocation())
|
||||
currentOptions = this.#buttons.isNotEnabledButIsPlaced;
|
||||
else
|
||||
currentOptions = this.#buttons.isNotEnabledAndIsNotPlaced;
|
||||
currentOptions = currentOptions.concat(this.#buttons.common);
|
||||
|
||||
if (!this.instance.hasLayers())
|
||||
currentOptions = currentOptions.filter(option =>
|
||||
option !== InstanceButtons.SetLayer
|
||||
&& option !== InstanceButtons.NextLayer
|
||||
&& option !== InstanceButtons.PreviousLayer
|
||||
);
|
||||
return currentOptions;
|
||||
}
|
||||
|
||||
handleOption(option) {
|
||||
switch (option) {
|
||||
case InstanceButtons.Enable:
|
||||
this.instance.enable();
|
||||
break;
|
||||
case InstanceButtons.Disable:
|
||||
this.instance.disable();
|
||||
break;
|
||||
case InstanceButtons.Place:
|
||||
this.instance.place(this.player.dimension.id, this.player.location);
|
||||
break;
|
||||
case InstanceButtons.Rename:
|
||||
this.renameInstanceForm();
|
||||
break;
|
||||
case InstanceButtons.Delete:
|
||||
structureCollection.delete(this.instanceName);
|
||||
break;
|
||||
case InstanceButtons.NextLayer:
|
||||
this.instance.increaseLayer();
|
||||
new InstanceForm(this.player, this.instanceName);
|
||||
break;
|
||||
case InstanceButtons.PreviousLayer:
|
||||
this.instance.decreaseLayer();
|
||||
new InstanceForm(this.player, this.instanceName);
|
||||
break;
|
||||
case InstanceButtons.Move:
|
||||
this.instance.move(this.player.dimension.id, this.player.location);
|
||||
break;
|
||||
case InstanceButtons.Settings:
|
||||
this.settingsForm();
|
||||
break;
|
||||
case InstanceButtons.Statistics:
|
||||
this.statisticsForm();
|
||||
break;
|
||||
case InstanceButtons.Materials:
|
||||
this.materialsForm();
|
||||
break;
|
||||
case InstanceButtons.MainMenu:
|
||||
new MenuForm(this.player, { jumpToInstance: false });
|
||||
break;
|
||||
default:
|
||||
this.player.sendMessage(`§cUnknown option: ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
renameInstanceForm() {
|
||||
InstanceFormBuilder.buildRenameInstance(this.instanceName).show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
const newName = response.formValues[0];
|
||||
if (newName === '') {
|
||||
this.player.sendMessage('§cInstance name cannot be empty.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
structureCollection.rename(this.instanceName, newName);
|
||||
this.instanceName = newName;
|
||||
} catch (e) {
|
||||
this.player.sendMessage(`§cError renaming instance: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLayerForm() {
|
||||
InstanceFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
this.instance.setLayer(parseInt(response.formValues[0]));
|
||||
});
|
||||
}
|
||||
|
||||
async statisticsForm() {
|
||||
let statsForm;
|
||||
try {
|
||||
statsForm = await InstanceFormBuilder.buildStatistics(this.instance);
|
||||
} catch (e) {
|
||||
if (e.message === 'StructureVerifier is already running.') {
|
||||
this.player.sendMessage('§cA verification is already in progress. Please wait until it finishes.');
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (!statsForm) {
|
||||
this.player.sendMessage('§cFailed to build statistics form.');
|
||||
return;
|
||||
}
|
||||
statsForm.form.show(this.player).then((response) => {
|
||||
if (response.canceled && response.cancelationReason === FormCancelationReason.UserBusy)
|
||||
this.player.sendMessage(statsForm.stats);
|
||||
});
|
||||
}
|
||||
|
||||
settingsForm() {
|
||||
InstanceFormBuilder.buildSettings(this.instance).show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
this.instance.setVerifierEnabled(response.formValues[0]);
|
||||
this.instance.setLayer(parseInt(response.formValues[1]));
|
||||
});
|
||||
}
|
||||
|
||||
materialsForm(onlyShowMissingMaterials = false) {
|
||||
forceShow(this.player, InstanceFormBuilder.buildMaterialList(this.instance, onlyShowMissingMaterials, this.player)).then((response) => {
|
||||
if (response.canceled) return;
|
||||
if (response.selection === 0)
|
||||
this.materialsForm(!onlyShowMissingMaterials);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
|
||||
import { MenuFormBuilder } from '../MenuFormBuilder';
|
||||
import { StructureVerifier } from '../Verifier/StructureVerifier';
|
||||
import { StructureStatistics } from '../Structure/StructureStatistics';
|
||||
import { EntityComponentTypes, TicksPerSecond } from '@minecraft/server';
|
||||
|
||||
export class InstanceFormBuilder {
|
||||
static structureVerifier;
|
||||
|
||||
static buildInstance(instance, options) {
|
||||
const location = instance.getLocation();
|
||||
const form = new ActionFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
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);
|
||||
options.forEach(option => {
|
||||
form.button(`${option}`);
|
||||
});
|
||||
return form;
|
||||
}
|
||||
|
||||
static buildRenameInstance(currentName) {
|
||||
return new ModalFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
.textField('Enter a new name for the instance:', currentName)
|
||||
.submitButton('Rename');
|
||||
}
|
||||
|
||||
static async buildStatistics(instance) {
|
||||
const buildStatisticsForm = new ActionFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
if (this.structureVerifier)
|
||||
throw new Error('StructureVerifier is already running.');
|
||||
this.structureVerifier = new StructureVerifier(instance, { isEnabled: true, particleLifetime: 1*TicksPerSecond, isStandalone: true });
|
||||
const verification = await this.structureVerifier.verifyStructure(true);
|
||||
const statistics = new StructureStatistics(instance, verification);
|
||||
const statsMessage = statistics.getMessage();
|
||||
this.structureVerifier = void 0;
|
||||
buildStatisticsForm.body(statsMessage);
|
||||
return { form: buildStatisticsForm, stats: statsMessage };
|
||||
}
|
||||
|
||||
static buildSettings(instance) {
|
||||
return new ModalFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
.toggle('Block Validation', { defaultValue: instance.options.verifier.isEnabled, tooltip: 'Shows missing and incorrect block overlay.' })
|
||||
.slider("Layer", 0, instance.getMaxLayer(), { defaultValue: instance.getLayer(), valueStep: 1, tooltip: 'Changes the active layer. Use 0 for all layers.' })
|
||||
.submitButton('§2Apply');
|
||||
}
|
||||
|
||||
static buildMaterialList(instance, onlyMissing = false, player = false) {
|
||||
const materials = instance.getActiveMaterials();
|
||||
const form = new ActionFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
const bodyText = { rawtext: [] };
|
||||
let buttonText = void 0;
|
||||
if (onlyMissing) {
|
||||
const inventoryContainer = player?.getComponent(EntityComponentTypes.Inventory)?.container;
|
||||
if (!inventoryContainer) {
|
||||
form.body('§cNo player inventory found.');
|
||||
return form;
|
||||
}
|
||||
bodyText.rawtext.push({ text: `§cMaterials Missing From Inventory:` });
|
||||
if (instance.hasLayerSelected())
|
||||
bodyText.rawtext.push({ text: ` §7(layer ${instance.getLayer()})` });
|
||||
bodyText.rawtext.push({ text: `§f\n\n` });
|
||||
bodyText.rawtext.push(materials.formatString(materials.getMaterialsDifference(inventoryContainer)));
|
||||
buttonText = "Show All Materials";
|
||||
} else {
|
||||
bodyText.rawtext.push({ text: `§aAll Materials:` });
|
||||
if (instance.hasLayerSelected())
|
||||
bodyText.rawtext.push({ text: ` §7(layer ${instance.getLayer()})` });
|
||||
bodyText.rawtext.push({ text: `§f\n\n` });
|
||||
bodyText.rawtext.push(materials.formatString());
|
||||
buttonText = "Show Missing Materials";
|
||||
}
|
||||
form.body(bodyText);
|
||||
form.button(buttonText);
|
||||
return form;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Vector } from "../../lib/Vector";
|
||||
import { world } from "@minecraft/server";
|
||||
import { Option } from "../Option";
|
||||
|
||||
export class InstanceOptions extends Option {
|
||||
#DP_NAMESPACE = "instanceOptions";
|
||||
instanceName = void 0;
|
||||
structureId = void 0;
|
||||
isEnabled = false;
|
||||
dimensionId = void 0;
|
||||
worldLocation = new Vector();
|
||||
currentLayer = 0;
|
||||
verifier = {
|
||||
isEnabled: true,
|
||||
trackPlayerDistance: 5,
|
||||
particleLifetime: 10
|
||||
};
|
||||
|
||||
static getInstanceStructureId(instanceName) {
|
||||
const options = new InstanceOptions(instanceName, void 0);
|
||||
return options.structureId;
|
||||
}
|
||||
|
||||
constructor(instanceName, structureId) {
|
||||
super();
|
||||
this.instanceName = instanceName;
|
||||
this.structureId = structureId;
|
||||
this.load();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.saveToDP(this.#DP_NAMESPACE, this.instanceName, this);
|
||||
}
|
||||
|
||||
load() {
|
||||
this.loadFromDP(this.#DP_NAMESPACE, this.instanceName);
|
||||
this.worldLocation = Vector.from(this.worldLocation);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.clearDP(this.#DP_NAMESPACE, this.instanceName);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Vector } from "../../lib/Vector";
|
||||
import { StructureOutliner } from "../Render/StructureOutliner";
|
||||
import { StructureVerifier } from "../Verifier/StructureVerifier";
|
||||
import { Structure } from "../Structure/Structure";
|
||||
import { InstanceOptions } from "./InstanceOptions";
|
||||
import { world, system, TicksPerSecond } from "@minecraft/server";
|
||||
import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError";
|
||||
import { StructureMaterials } from "../Materials/StructureMaterials";
|
||||
import { VerificationRenderer } from "../Render/VerificationRenderer";
|
||||
|
||||
export class StructureInstance {
|
||||
options;
|
||||
structure = void 0;
|
||||
outliner = void 0;
|
||||
verifier = void 0;
|
||||
verificationRenderer = void 0;
|
||||
materials = void 0;
|
||||
|
||||
constructor(instanceName, structureId) {
|
||||
this.structure = new Structure(structureId);
|
||||
this.options = new InstanceOptions(instanceName, structureId);
|
||||
this.refreshBox();
|
||||
this.subscribeToEvents();
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.disable();
|
||||
this.options.clear();
|
||||
delete this.options;
|
||||
delete this.structure;
|
||||
delete this.outliner;
|
||||
delete this.verifier;
|
||||
delete this.verificationRenderer;
|
||||
delete this.materials;
|
||||
}
|
||||
|
||||
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 });
|
||||
if (!this.verificationRenderer)
|
||||
this.verificationRenderer = new VerificationRenderer(this);
|
||||
if (!this.materials)
|
||||
this.materials = new StructureMaterials(this);
|
||||
this.outliner.refresh();
|
||||
this.verifier.refresh();
|
||||
this.verificationRenderer.refresh();
|
||||
this.materials.refresh();
|
||||
}
|
||||
|
||||
subscribeToEvents() {
|
||||
this.disableInstanceWhenNoPlayersOnline();
|
||||
}
|
||||
|
||||
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 InstanceNotPlacedError(`[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 InstanceNotPlacedError(`[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 InstanceNotPlacedError(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
|
||||
if (this.hasLayerSelected())
|
||||
return this.getLayerBlocks(this.getLayer() - 1);
|
||||
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 InstanceNotPlacedError(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
|
||||
if (this.hasLayerSelected())
|
||||
return this.structure.getLayerLocations(this.getLayer()-1);
|
||||
else
|
||||
return this.structure.getAllLocations();
|
||||
}
|
||||
|
||||
getActiveMaterials() {
|
||||
return this.materials;
|
||||
}
|
||||
|
||||
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.enable();
|
||||
this.move(dimensionId, worldLocation);
|
||||
}
|
||||
|
||||
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();
|
||||
this.verificationRenderer.refresh();
|
||||
}
|
||||
|
||||
setVerifierDistance(distance) {
|
||||
this.options.setVerifierDistance(distance);
|
||||
if (this.options.verifier.trackPlayerDistance === 0) {
|
||||
const bounds = this.getBounds();
|
||||
this.options.verifier.particleLifetime = Math.max(bounds.min.volume(bounds.max) / TicksPerSecond, 2*TicksPerSecond);
|
||||
} else {
|
||||
this.options.verifier.particleLifetime = 10;
|
||||
}
|
||||
this.verificationRenderer.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);
|
||||
}
|
||||
|
||||
disableInstanceWhenNoPlayersOnline() {
|
||||
// This prevents a memory leak when no players are online.
|
||||
world.beforeEvents.playerLeave.subscribe(event => {
|
||||
system.run(() => {
|
||||
if (world.getAllPlayers().length === 0 && this.options.isEnabled) {
|
||||
console.info(`[Construct] No players are online. Disabling instance: '${this.options.instanceName}'`);
|
||||
this.disable();
|
||||
this.shouldEnableOnJoin = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
world.afterEvents.playerJoin.subscribe(event => {
|
||||
if (this.shouldEnableOnJoin) {
|
||||
console.info(`[Construct] A player rejoined. Re-enabling instance: '${this.options.instanceName}'`);
|
||||
this.enable();
|
||||
this.shouldEnableOnJoin = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toGlobalCoords(structureLocation) {
|
||||
return Vector.from(structureLocation).add(this.options.worldLocation);
|
||||
}
|
||||
|
||||
toStructureCoords(worldLocation) {
|
||||
return Vector.from(worldLocation).subtract(this.options.worldLocation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { MaterialGrabberFormBuilder } from './MaterialGrabberFormBuilder';
|
||||
import { forceShow } from '../../utils';
|
||||
import { Builders } from '../Builder/Builders';
|
||||
import { structureCollection } from '../Structure/StructureCollection';
|
||||
|
||||
export class MaterialGrabberForm {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
try {
|
||||
return forceShow(this.player, MaterialGrabberFormBuilder.buildInstanceSelector(this.player)).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
const selectedInstanceName = structureCollection.getInstanceNames()[response.selection];
|
||||
if (selectedInstanceName) {
|
||||
this.setActiveInstance(selectedInstanceName);
|
||||
this.player.sendMessage(`§7Selected instance for material grabber: §2${selectedInstanceName}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message === 'Menu timed out.') {
|
||||
this.player.sendMessage('§8Menu timed out.');
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
setActiveInstance(instanceName) {
|
||||
const builder = Builders.get(this.player.id);
|
||||
builder.materialInstanceName = instanceName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ActionFormData } from "@minecraft/server-ui";
|
||||
import { MenuFormBuilder } from "../MenuFormBuilder";
|
||||
import { structureCollection } from "../Structure/StructureCollection";
|
||||
import { Builders } from "../Builder/Builders";
|
||||
|
||||
export class MaterialGrabberFormBuilder {
|
||||
static menuTitle = MenuFormBuilder.menuTitle + ' Material Grabber';
|
||||
|
||||
static buildInstanceSelector(player) {
|
||||
const allInstanceNameForm = new ActionFormData()
|
||||
.title(this.menuTitle);
|
||||
const currInstanceName = Builders.get(player.id).materialInstanceName;
|
||||
let body = '§7Current instance: ';
|
||||
if (currInstanceName)
|
||||
body += `§2${currInstanceName}`;
|
||||
else
|
||||
body += '§7None';
|
||||
body += '\n§7Select an instance:';
|
||||
allInstanceNameForm.body(body);
|
||||
structureCollection.getInstanceNames().forEach(instanceName => {
|
||||
allInstanceNameForm.button(`§2${instanceName}`);
|
||||
});
|
||||
return allInstanceNameForm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ItemStack, system } from "@minecraft/server";
|
||||
import { Vector } from "../../lib/Vector";
|
||||
|
||||
class StructureMaterials {
|
||||
instance;
|
||||
materials;
|
||||
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
this.materials = {};
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.clear();
|
||||
this.populateInstance();
|
||||
}
|
||||
|
||||
populateInstance() {
|
||||
try {
|
||||
if (this.instance.hasLocation() && this.instance.isEnabled())
|
||||
system.runJob(this.populateActive());
|
||||
else
|
||||
system.runJob(this.populateAll());
|
||||
} catch (e) {
|
||||
if (e.name === 'InstanceNotPlacedError')
|
||||
this.clear();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
get(itemType) {
|
||||
return this.materials[itemType];
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return Object.keys(this.materials).length === 0;
|
||||
}
|
||||
|
||||
has(itemType) {
|
||||
return this.materials[itemType] !== undefined;
|
||||
}
|
||||
|
||||
remove(itemType, amount) {
|
||||
if (!this.materials[itemType]) return;
|
||||
this.materials[itemType].count -= amount;
|
||||
if (this.materials[itemType].count <= 0)
|
||||
delete this.materials[itemType];
|
||||
}
|
||||
|
||||
*populateAll() {
|
||||
for (let layer = 0; layer < this.instance.getMaxLayer(); layer++) {
|
||||
for (const block of this.instance.getLayerBlocks(layer)) {
|
||||
this.countBlock(block)
|
||||
yield void 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*populateActive() {
|
||||
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);
|
||||
const block = this.instance.getBlock(location);
|
||||
if (!block) continue;
|
||||
this.countBlock(block);
|
||||
yield void 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countBlock(block) {
|
||||
const itemStack = block?.getItemStack();
|
||||
const typeId = itemStack?.typeId;
|
||||
if (!typeId) return;
|
||||
if (!this.materials[typeId])
|
||||
this.materials[typeId] = { count: 0, stackSize: itemStack.maxAmount };
|
||||
this.materials[typeId].count++;
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const key in this.materials)
|
||||
delete this.materials[key];
|
||||
}
|
||||
|
||||
formatString(otherMaterials = void 0) {
|
||||
const materials = otherMaterials || this.materials;
|
||||
let message = { rawtext: [] };
|
||||
const sortedTypes = Object.keys(materials).sort(
|
||||
(a, b) => materials[b].count - materials[a].count
|
||||
);
|
||||
for (const blockType of sortedTypes) {
|
||||
let count = materials[blockType].count;
|
||||
let countStr = '';
|
||||
const stackSize = materials[blockType].stackSize;
|
||||
const fullShulker = 27 * stackSize;
|
||||
if (count >= fullShulker)
|
||||
countStr = `${Math.floor(count / fullShulker)}\uE200`;
|
||||
if (count > fullShulker && count % fullShulker > 0)
|
||||
countStr += ' + ';
|
||||
count %= fullShulker;
|
||||
if (count >= stackSize) {
|
||||
const numStacks = Math.floor(count / stackSize);
|
||||
countStr += `${numStacks} stack`;
|
||||
if (numStacks > 1)
|
||||
countStr += 's';
|
||||
}
|
||||
if (count > stackSize && count % stackSize > 0)
|
||||
countStr += ' + ';
|
||||
count %= stackSize;
|
||||
if (count > 0)
|
||||
countStr += count;
|
||||
message.rawtext.push({ text: '§3' });
|
||||
message.rawtext.push({ translate: new ItemStack(blockType).localizationKey })
|
||||
message.rawtext.push({ text: `§f: ${countStr}\n` });
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
getMaterialsDifference(container) {
|
||||
const missingMaterials = JSON.parse(JSON.stringify(this.materials));
|
||||
for (let slotIndex = 0; slotIndex < container.size; slotIndex++) {
|
||||
const slot = container.getSlot(slotIndex);
|
||||
if (slot.hasItem()) {
|
||||
const itemType = slot.getItem().typeId;
|
||||
if (!missingMaterials[itemType])
|
||||
continue;
|
||||
missingMaterials[itemType].count -= slot.amount;
|
||||
if (missingMaterials[itemType].count <= 0)
|
||||
delete missingMaterials[itemType];
|
||||
}
|
||||
}
|
||||
return missingMaterials;
|
||||
}
|
||||
}
|
||||
|
||||
export { StructureMaterials };
|
||||
@@ -0,0 +1,102 @@
|
||||
import { forceShow } from '../utils';
|
||||
import { structureCollection } from './Structure/StructureCollection';
|
||||
import { MenuFormBuilder } from './MenuFormBuilder';
|
||||
import { InstanceForm } from './Instance/InstanceForm';
|
||||
import { BuilderForm } from './Builder/BuilderForm';
|
||||
|
||||
export class MenuForm {
|
||||
constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) {
|
||||
this.player = player;
|
||||
this.show(jumpToInstance, instanceName);
|
||||
}
|
||||
|
||||
async show(jumpToInstance = false, instanceName = void 0) {
|
||||
if (jumpToInstance) {
|
||||
if (!instanceName)
|
||||
instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useActiveLayer: false })?.getName();
|
||||
if (instanceName) {
|
||||
new InstanceForm(this.player, instanceName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
instanceName = await this.getInstanceNameFromForm();
|
||||
if (!instanceName)
|
||||
return;
|
||||
new InstanceForm(this.player, instanceName);
|
||||
}
|
||||
|
||||
async getInstanceNameFromForm() {
|
||||
try {
|
||||
return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
let selection = response.selection;
|
||||
if (selection === 0) {
|
||||
new BuilderForm(this.player);
|
||||
return void 0;
|
||||
} else {
|
||||
selection--;
|
||||
const selectedInstanceName = structureCollection.getInstanceNames()[selection];
|
||||
return selectedInstanceName || this.createNewInstance();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message === 'Menu timed out.') {
|
||||
this.player.sendMessage('§8Menu timed out.');
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async createNewInstance() {
|
||||
return MenuFormBuilder.buildNewInstance().show(this.player).then(async (response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
const instanceName = response.formValues[0];
|
||||
if (instanceName === '')
|
||||
return void 0;
|
||||
const structureId = await this.getStructureId();
|
||||
if (!structureId)
|
||||
return;
|
||||
try {
|
||||
structureCollection.add(instanceName, structureId);
|
||||
} catch (e) {
|
||||
if (e.name === 'InvalidInstanceError') {
|
||||
this.player.sendMessage(`§cInstance '${instanceName}' already exists. Try again with a new name.`);
|
||||
return void 0;
|
||||
}
|
||||
if (e.name === 'InvalidStructureError') {
|
||||
this.player.sendMessage(`§cStructure ID '${structureId}' not found. If you're looking for a structure that you put in the structures folder, please restart your world and try again.`);
|
||||
return void 0;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return instanceName;
|
||||
});
|
||||
}
|
||||
|
||||
async getStructureId() {
|
||||
return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
if (response.selection === structureCollection.getWorldStructureIds().length + 1) {
|
||||
MenuFormBuilder.buildHowTo().show(this.player);
|
||||
return;
|
||||
}
|
||||
const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection];
|
||||
return selectedStructureId || this.getOtherStructureId();
|
||||
});
|
||||
}
|
||||
|
||||
getOtherStructureId() {
|
||||
return MenuFormBuilder.buildOtherStructure().show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
const structureId = response.formValues[0];
|
||||
if (structureId === '')
|
||||
return void 0;
|
||||
return structureId;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
|
||||
import { structureCollection } from './Structure/StructureCollection';
|
||||
|
||||
export class MenuFormBuilder {
|
||||
static menuTitle = '§l§2Construct';
|
||||
|
||||
static buildAllInstanceName() {
|
||||
const allInstanceNameForm = new ActionFormData()
|
||||
.title(this.menuTitle)
|
||||
.body('Select an instance:');
|
||||
allInstanceNameForm.button('Builder Settings');
|
||||
structureCollection.getInstanceNames().forEach(instanceName => {
|
||||
allInstanceNameForm.button(`${structureCollection.get(instanceName).isEnabled() ? '§2' : '§c'}${instanceName}`);
|
||||
});
|
||||
allInstanceNameForm.button('Create New Instance');
|
||||
return allInstanceNameForm;
|
||||
}
|
||||
|
||||
static buildNewInstance() {
|
||||
return new ModalFormData()
|
||||
.title(this.menuTitle)
|
||||
.textField('Enter a name for the new instance:', 'example_instance')
|
||||
.submitButton('Submit');
|
||||
}
|
||||
|
||||
static buildAllStructures() {
|
||||
const allStructuresForm = new ActionFormData()
|
||||
.title(this.menuTitle)
|
||||
.body('Select a structure:');
|
||||
structureCollection.getWorldStructureIds().forEach(structureId => {
|
||||
const structureName = structureId.replace('mystructure:', '');
|
||||
allStructuresForm.button(`§2${structureName}`);
|
||||
});
|
||||
allStructuresForm.button('Other');
|
||||
allStructuresForm.button('How to Add/Remove Structures');
|
||||
return allStructuresForm;
|
||||
}
|
||||
|
||||
static buildOtherStructure() {
|
||||
return new ModalFormData()
|
||||
.title(this.menuTitle)
|
||||
.textField('Enter the Structure ID:', 'example_structure')
|
||||
.submitButton('Submit');
|
||||
}
|
||||
|
||||
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 += "§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()
|
||||
.title(this.menuTitle)
|
||||
.body(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { world } from "@minecraft/server";
|
||||
|
||||
export class Option {
|
||||
saveToDP(namespace, id, data) {
|
||||
world.setDynamicProperty(`${namespace}:${id}`, JSON.stringify(data));
|
||||
}
|
||||
|
||||
loadFromDP(namespace, id) {
|
||||
try {
|
||||
const options = JSON.parse(world.getDynamicProperty(`${namespace}:${id}`));
|
||||
if (options)
|
||||
Object.assign(this, options);
|
||||
else
|
||||
throw new Error("Options not found");
|
||||
} catch {
|
||||
this.saveToDP(namespace, id, {});
|
||||
const options = JSON.parse(world.getDynamicProperty(`${namespace}:${id}`) || "{}");
|
||||
Object.assign(this, options);
|
||||
}
|
||||
}
|
||||
|
||||
clearDP(namespace, id) {
|
||||
world.setDynamicProperty(`${namespace}:${id}`, void 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { MolangVariableMap, system, world } from "@minecraft/server";
|
||||
import { Vector } from "../lib/Vector";
|
||||
|
||||
export class Outliner {
|
||||
dimension;
|
||||
min = new Vector();
|
||||
max = new Vector();
|
||||
drawParticle = "construct:outline";
|
||||
drawFrequency = 10;
|
||||
|
||||
#drawParticles = [];
|
||||
#runner = void 0;
|
||||
|
||||
constructor(dimension, min, max) {
|
||||
this.dimension = dimension;
|
||||
this.min = Vector.from(min);
|
||||
this.max = Vector.from(max);
|
||||
this.vertices = this.getVertices(min, max);
|
||||
}
|
||||
|
||||
startDraw() {
|
||||
this.#runner = system.runInterval(() => this.draw(), this.drawFrequency);
|
||||
}
|
||||
|
||||
stopDraw() {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.drawParticles(this.getVerticeParticles(), () => {
|
||||
return { red: 1, green: 1, blue: 1, alpha: 1 }
|
||||
});
|
||||
this.drawParticles(this.getCubiodEdgeParticles(), this.getNextParticleColor.bind(this));
|
||||
}
|
||||
|
||||
drawParticles(particleLocations, colorCallback) {
|
||||
this.#drawParticles.length = 0;
|
||||
this.#drawParticles.push(...particleLocations);
|
||||
for (const [particleType, location] of this.#drawParticles) {
|
||||
const molang = new MolangVariableMap();
|
||||
molang.setColorRGBA("dot_color", colorCallback());
|
||||
try {
|
||||
this.dimension.spawnParticle(particleType, location, molang);
|
||||
} catch (e) {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVertices(min, max) {
|
||||
return [
|
||||
new Vector(min.x, min.y, min.z),
|
||||
new Vector(max.x, min.y, min.z),
|
||||
new Vector(min.x, max.y, min.z),
|
||||
new Vector(max.x, max.y, min.z),
|
||||
new Vector(min.x, min.y, max.z),
|
||||
new Vector(max.x, min.y, max.z),
|
||||
new Vector(min.x, max.y, max.z),
|
||||
new Vector(max.x, max.y, max.z)
|
||||
];
|
||||
}
|
||||
|
||||
setVertices(dimension, min, max) {
|
||||
this.dimension = dimension;
|
||||
this.min = Vector.from(min);
|
||||
this.max = Vector.from(max);
|
||||
this.vertices = this.getVertices(min, max);
|
||||
}
|
||||
|
||||
getVerticeParticles() {
|
||||
return this.vertices.map((v) => [this.drawParticle, v]);
|
||||
}
|
||||
|
||||
getCubiodEdgeParticles() {
|
||||
const edges = [
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[0, 4],
|
||||
[1, 3],
|
||||
[1, 5],
|
||||
[2, 3],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
[4, 5],
|
||||
[4, 6],
|
||||
[5, 7],
|
||||
[6, 7]
|
||||
];
|
||||
const edgePoints = [];
|
||||
for (const edge of edges) {
|
||||
const [startVertex, endVertex] = [this.vertices[edge[0]], this.vertices[edge[1]]];
|
||||
const resolution = Math.min(Math.floor(endVertex.subtract(startVertex).length), 16);
|
||||
for (let i = 1; i < resolution; i++) {
|
||||
const t = i / resolution;
|
||||
edgePoints.push(startVertex.lerp(endVertex, t));
|
||||
}
|
||||
}
|
||||
return edgePoints.map((v) => [this.drawParticle, v]);
|
||||
}
|
||||
|
||||
addStandaloneParticles(locations) {
|
||||
for (const location of locations)
|
||||
this.vertices.push(Vector.from(location));
|
||||
}
|
||||
|
||||
getNextParticleColor() {
|
||||
if (this.lastWasBlack) {
|
||||
this.lastWasBlack = false;
|
||||
return { red: 0.93333333, green: 0.77647059, blue: 0.13333333, alpha: 1 };
|
||||
} else {
|
||||
this.lastWasBlack = true;
|
||||
return { red: 0.09019608, green: 0.09019608, blue: 0.09019608, alpha: 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { structureCollection } from "./Structure/StructureCollection";
|
||||
import { world } from "@minecraft/server";
|
||||
|
||||
export class Raycaster {
|
||||
static STEP_SIZE = 0.2;
|
||||
|
||||
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, { useActiveLayer });
|
||||
if (structure) {
|
||||
const block = structure.getBlock(structure.toStructureCoords(location));
|
||||
if (block?.type.id !== 'minecraft:air') {
|
||||
blocks.push({
|
||||
permutation: block,
|
||||
location: location
|
||||
});
|
||||
if (getFirst)
|
||||
break;
|
||||
}
|
||||
try {
|
||||
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.name === 'LocationOutOfWorldBoundariesError')
|
||||
break;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
location = {
|
||||
x: location.x + (direction.x*this.STEP_SIZE),
|
||||
y: location.y + (direction.y*this.STEP_SIZE),
|
||||
z: location.z + (direction.z*this.STEP_SIZE)
|
||||
};
|
||||
distance += this.STEP_SIZE;
|
||||
}
|
||||
// world.getDimension('minecraft:overworld').spawnParticle('minecraft:villager_happy', location);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
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, useActiveLayer });
|
||||
if (blocks.length === 0)
|
||||
return void 0;
|
||||
return isFirst ? blocks[0] : blocks[blocks.length - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { MolangVariableMap } from "@minecraft/server";
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { Vector } from "../../lib/Vector";
|
||||
|
||||
export class BlockVerificationLevelRender {
|
||||
opacity = 0.2;
|
||||
lifetimeSeconds = 0;
|
||||
|
||||
constructor(dimensionLocation, verificationLevel, lifetimeSeconds = 5) {
|
||||
this.dimension = dimensionLocation.dimension;
|
||||
this.location = Vector.from(dimensionLocation.location);
|
||||
this.verificationLevel = verificationLevel;
|
||||
this.lifetimeSeconds = lifetimeSeconds;
|
||||
this.renderBlock();
|
||||
}
|
||||
|
||||
renderBlock() {
|
||||
const sizeScalar = this.verificationLevelToSizeScalar();
|
||||
for (const particle of this.getBlockParticles(sizeScalar)) {
|
||||
const color = this.getRGBAMolang();
|
||||
if (!color)
|
||||
return;
|
||||
color.setFloat("lifetime", this.lifetimeSeconds);
|
||||
color.setFloat("width", 0.5*sizeScalar);
|
||||
color.setFloat("height", 0.5*sizeScalar);
|
||||
try {
|
||||
this.dimension.spawnParticle(particle.particleType, particle.location, color);
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBlockParticles(sizeScalar = 1) {
|
||||
const bottomFace = new Vector(0.5, 0, 0.5);
|
||||
const topFace = new Vector(0.5, 1, 0.5);
|
||||
const leftFace = new Vector(1, 0.5, 0.5);
|
||||
const rightFace = new Vector(0, 0.5, 0.5);
|
||||
const frontFace = new Vector(0.5, 0.5, 1);
|
||||
const backFace = new Vector(0.5, 0.5, 0);
|
||||
const center = new Vector(0.5, 0.5, 0.5);
|
||||
return [
|
||||
{ particleType: "construct:blockoverlay_xz", location: this.location.add(center).add(topFace.subtract(center).multiply(sizeScalar)) },
|
||||
{ particleType: "construct:blockoverlay_xz", location: this.location.add(center).add(bottomFace.subtract(center).multiply(sizeScalar)) },
|
||||
{ particleType: "construct:blockoverlay_yz", location: this.location.add(center).add(leftFace.subtract(center).multiply(sizeScalar)) },
|
||||
{ particleType: "construct:blockoverlay_yz", location: this.location.add(center).add(rightFace.subtract(center).multiply(sizeScalar)) },
|
||||
{ particleType: "construct:blockoverlay_xy", location: this.location.add(center).add(frontFace.subtract(center).multiply(sizeScalar)) },
|
||||
{ particleType: "construct:blockoverlay_xy", location: this.location.add(center).add(backFace.subtract(center).multiply(sizeScalar)) }
|
||||
];
|
||||
}
|
||||
|
||||
getRGBAMolang() {
|
||||
const rgb = this.verificationLevelToRGB();
|
||||
if (!rgb) return;
|
||||
rgb.alpha = this.opacity;
|
||||
const molang = new MolangVariableMap();
|
||||
molang.setColorRGBA("face_color", rgb);
|
||||
return molang;
|
||||
}
|
||||
|
||||
verificationLevelToRGB() {
|
||||
switch (this.verificationLevel) {
|
||||
case BlockVerificationLevel.NoMatch:
|
||||
return { red: 1, green: 0, blue: 0};
|
||||
case BlockVerificationLevel.TypeMatch:
|
||||
return { red: 1, green: 1, blue: 0};
|
||||
case BlockVerificationLevel.Missing:
|
||||
return { red: 0, green: 0, blue: 1};
|
||||
default:
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
verificationLevelToSizeScalar() {
|
||||
switch (this.verificationLevel) {
|
||||
case BlockVerificationLevel.NoMatch:
|
||||
return 1.01;
|
||||
case BlockVerificationLevel.TypeMatch:
|
||||
return 1.01;
|
||||
case BlockVerificationLevel.Missing:
|
||||
return 0.90;
|
||||
default:
|
||||
return 1.00;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.getLayerBounds(this.instance.getLayer());
|
||||
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,117 @@
|
||||
import { TicksPerSecond } from "@minecraft/server";
|
||||
import { BlockVerificationLevelRender } from "./BlockVerificationLevelRender";
|
||||
import { system } from "@minecraft/server";
|
||||
|
||||
const RENDER_LIFETIME_FACTOR_TICKS = 1;
|
||||
|
||||
export class VerificationRenderer {
|
||||
instance;
|
||||
lastRenderedChunk;
|
||||
bounds;
|
||||
shortestDimension;
|
||||
#runner;
|
||||
#renderQueue = [];
|
||||
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
this.lastRenderedChunk = 0;
|
||||
}
|
||||
|
||||
startContinuousRendering() {
|
||||
this.#runner = system.runInterval(() => {
|
||||
if (this.#renderQueue.length === 0)
|
||||
this.prepareRenderQueue();
|
||||
this.renderNextChunk();
|
||||
}, RENDER_LIFETIME_FACTOR_TICKS);
|
||||
}
|
||||
|
||||
stopContinuousRendering() {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
this.#renderQueue = [];
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.stopContinuousRendering();
|
||||
if (!this.instance.isEnabled() || !this.instance.options.verifier.isEnabled)
|
||||
return;
|
||||
this.startContinuousRendering();
|
||||
}
|
||||
|
||||
prepareRenderQueue() {
|
||||
this.#renderQueue = [];
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
for (let y = bounds.min.y; y < bounds.max.y; y++) {
|
||||
this.prepareRenderQueueLayer(bounds, y);
|
||||
}
|
||||
this.lastRenderedChunk = 0;
|
||||
}
|
||||
|
||||
prepareRenderQueueLayer(bounds, y) {
|
||||
if (bounds.max.x < bounds.max.z) {
|
||||
for (let z = bounds.min.z; z < bounds.max.z; z++) {
|
||||
for (let x = bounds.min.x; x < bounds.max.x; x++) {
|
||||
this.#renderQueue.push({ x, y, z });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let x = bounds.min.x; x < bounds.max.x; x++) {
|
||||
for (let z = bounds.min.z; z < bounds.max.z; z++) {
|
||||
this.#renderQueue.push({ x, y, z });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNextChunk() {
|
||||
if (this.shouldUseLargeStructureRendering())
|
||||
this.renderNextChunkForLargeStructure();
|
||||
else
|
||||
this.renderNextChunkForSmallStructure();
|
||||
}
|
||||
|
||||
renderNextChunkForLargeStructure() {
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
const shortestSideLength = Math.min(bounds.max.x, bounds.max.z);
|
||||
const maxChunk = (bounds.min.volume(bounds.max) / shortestSideLength) / (bounds.max.y - bounds.min.y);
|
||||
const lifetime = (maxChunk * RENDER_LIFETIME_FACTOR_TICKS) / TicksPerSecond;
|
||||
const verificationLevels = this.instance.verifier.getLastVerificationLevels();
|
||||
const dimension = this.instance.getDimension();
|
||||
const chunk = this.#renderQueue.splice(0, shortestSideLength);
|
||||
for (const location of chunk) {
|
||||
const verificationLevel = verificationLevels[JSON.stringify(location)];
|
||||
if (!verificationLevel)
|
||||
continue;
|
||||
const dimensionLocation = {
|
||||
dimension: dimension,
|
||||
location: this.instance.toGlobalCoords(location)
|
||||
};
|
||||
new BlockVerificationLevelRender(dimensionLocation, verificationLevel, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
renderNextChunkForSmallStructure() {
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
const lifetime = (bounds.max.x * (bounds.max.y - bounds.min.y) * bounds.max.z * RENDER_LIFETIME_FACTOR_TICKS) / TicksPerSecond;
|
||||
const verificationLevels = this.instance.verifier.getLastVerificationLevels();
|
||||
const dimension = this.instance.getDimension();
|
||||
for (const location of this.#renderQueue.splice(0, 1)) {
|
||||
const verificationLevel = verificationLevels[JSON.stringify(location)];
|
||||
if (!verificationLevel)
|
||||
continue;
|
||||
const dimensionLocation = {
|
||||
dimension: dimension,
|
||||
location: this.instance.toGlobalCoords(location)
|
||||
};
|
||||
new BlockVerificationLevelRender(dimensionLocation, verificationLevel, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
shouldUseLargeStructureRendering() {
|
||||
const bounds = this.instance.getActiveBounds();
|
||||
const maxVolume = 343;
|
||||
return this.instance.hasLayerSelected() || bounds.min.volume(bounds.max) > maxVolume;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { world } from "@minecraft/server";
|
||||
import { Vector } from "../../lib/Vector";
|
||||
import { InvalidStructureError } from "../Errors/InvalidStructureError";
|
||||
|
||||
export class Structure {
|
||||
structureId;
|
||||
#structure;
|
||||
|
||||
constructor(structureId) {
|
||||
this.structureId = structureId;
|
||||
this.#structure = world.structureManager.get(structureId);
|
||||
if (!this.#structure)
|
||||
throw new InvalidStructureError(`[Construct] Structure '${structureId}' not found on world.`);
|
||||
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 { InvalidInstanceError } from '../Errors/InvalidInstanceError';
|
||||
import { InstanceOptions } from '../Instance/InstanceOptions';
|
||||
import { StructureInstance } from '../Instance/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.getInstanceStructureId(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 InvalidInstanceError(`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 InvalidInstanceError(`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,64 @@
|
||||
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 [location, verificationLevel] of Object.entries(this.verification)) {
|
||||
if (location === 'correctlyAir')
|
||||
continue;
|
||||
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,42 @@
|
||||
import { BlockVerificationLevel } from "../Enums/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 BlockVerificationLevel.Air;
|
||||
if (this.isMissing(worldPermutation, structPermutation))
|
||||
return BlockVerificationLevel.Missing;
|
||||
if (this.isExactMatch(worldPermutation, structPermutation))
|
||||
return BlockVerificationLevel.Match;
|
||||
if (this.isTypeMatch(worldPermutation, structPermutation))
|
||||
return BlockVerificationLevel.TypeMatch;
|
||||
return BlockVerificationLevel.NoMatch;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { BlockVerifier } from "./BlockVerifier";
|
||||
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
|
||||
import { BlockVerificationLevelRender } from "../Render/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;
|
||||
particleLifetime;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
startContinuousVerification() {
|
||||
this.shouldStartNextVerification = true;
|
||||
this.#runner = system.runInterval(() => {
|
||||
if (this.shouldStartNextVerification)
|
||||
this.verifyStructure();
|
||||
});
|
||||
}
|
||||
|
||||
stopContinuousVerification() {
|
||||
if (!this.#runner)
|
||||
return;
|
||||
system.clearRun(this.#runner);
|
||||
this.#runner = void 0;
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.stopContinuousVerification();
|
||||
if (!this.instance.isEnabled())
|
||||
return;
|
||||
this.startContinuousVerification();
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
if (this.isStandalone)
|
||||
return this.enabled;
|
||||
return this.instance.options.verifier.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));
|
||||
}
|
||||
|
||||
async verifyStructure(shouldRender = false) {
|
||||
if (!this.isEnabled())
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
getLastVerificationLevels() {
|
||||
if (!this.lastCompleteVerificationLevels)
|
||||
return {};
|
||||
return this.lastCompleteVerificationLevels;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user