Form complete but not error handled

This commit is contained in:
ForestOfLight
2025-04-13 01:11:23 -07:00
Unverified
parent 6ced22b57b
commit 373b3df853
11 changed files with 225 additions and 117 deletions
+4 -6
View File
@@ -30,13 +30,11 @@ Removes a structure.
## Roadmap
- [ ] place structure using form **(in progress)**
- [x] Form to manage structure
- [x] easyPlace rule
- [x] Make a viewable material list
- [ ] Make automatic material gathering from inventories
- [ ] structure movement & rotation
- [ ] structuraMode & holoprintMode rules for ease of use
- [ ] more litematica features!
- [ ] Automatic material gathering from inventories
- [ ] Structure movement & rotation
- [ ] More litematica features!
## Issues & Suggestions
+12 -2
View File
@@ -2,6 +2,8 @@ import { system, world } from '@minecraft/server';
import { Raycaster } from '../classes/Raycaster';
class BlockInfo {
static shownToLastTick = new Set();
static onTick() {
for (const player of world.getAllPlayers()) {
if (!player)
@@ -12,16 +14,24 @@ class BlockInfo {
static showStructureBlockInfo(player) {
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: 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(block.permutation) });
this.shownToLastTick.add(player.id);
}
static getFormattedBlockInfo(block) {
const header = 'Structure:\n'
if (!block)
return header + '§7Unknown';
const states = block.getAllStates();
if (Object.keys(states).length === 0)
return `Structure:\n§a${block.type.id}`;
return `Structure:\n§a${block.type.id}\n§7${this.getFormattedStates(states)}`;
return header + `§a${block.type.id}`;
return header + `§a${block.type.id}\n§7${this.getFormattedStates(states)}`;
}
static getFormattedStates(states) {
+53 -35
View File
@@ -1,27 +1,28 @@
import { structureCollection } from './StructureCollection';
import { MenuForm } from '../classes/MenuForm';
import { ActionFormData } from '@minecraft/server-ui';
import { InstanceEditOptions } from './InstanceEditOptions';
import { InstanceEditFormBuilder } from './InstanceEditFormBuilder';
export class InstanceEditForm {
instanceName;
options = {
isPlaced: [
#buttons = {
isEnabled: [
InstanceEditOptions.NextLayer,
InstanceEditOptions.PreviousLayer,
InstanceEditOptions.SetLayer,
InstanceEditOptions.Move,
InstanceEditOptions.Rotate,
InstanceEditOptions.Mirror,
InstanceEditOptions.RemovePlacement,
InstanceEditOptions.RenameInstance,
InstanceEditOptions.DisableInstance,
],
notPlaced: [
InstanceEditOptions.PlaceInstance
isNotEnabledAndIsNotPlaced: [
InstanceEditOptions.PlaceInstance,
InstanceEditOptions.RenameInstance
],
isNotEnabledButIsPlaced: [
InstanceEditOptions.EnableInstance,
InstanceEditOptions.RenameInstance
],
common: [
InstanceEditOptions.MaterialsList,
InstanceEditOptions.RenameInstance,
InstanceEditOptions.DeleteInstance,
InstanceEditOptions.MainMenu
]
@@ -35,57 +36,65 @@ export class InstanceEditForm {
}
show() {
const currentOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced;
InstanceEditFormBuilder.buildInstance(this.instanceName, currentOptions, this.options.common).show(this.player).then((response) => {
const currentOptions = this.getActiveOptions();
InstanceEditFormBuilder.buildInstance(this.instance, currentOptions).show(this.player).then((response) => {
if (response.canceled) return;
let selectedOption;
if (this.instance.isPlaced())
selectedOption = this.options.isPlaced[response.selection] || this.options.common[response.selection-this.options.isPlaced.length];
else
selectedOption = this.options.notPlaced[response.selection] || this.options.common[response.selection-this.options.notPlaced.length];
this.handleOption(selectedOption);
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 !== InstanceEditOptions.SetLayer
&& option !== InstanceEditOptions.NextLayer
&& option !== InstanceEditOptions.PreviousLayer
);
return currentOptions;
}
handleOption(option) {
switch (option) {
case InstanceEditOptions.EnableInstance:
this.instance.enable();
break;
case InstanceEditOptions.DisableInstance:
this.instance.disable();
break;
case InstanceEditOptions.PlaceInstance:
this.instance.place(this.player.dimension.id, this.player.location);
break;
case InstanceEditOptions.RemovePlacement:
this.instance.removePlacement();
break;
case InstanceEditOptions.RenameInstance:
this.renameInstanceForm();
break;
case InstanceEditOptions.DeleteInstance:
this.structureCollection.remove(this.instanceName);
structureCollection.delete(this.instanceName);
break;
case InstanceEditOptions.NextLayer:
this.instance.setLayer(this.instance.getLayer() + 1);
this.instance.increaseLayer();
new InstanceEditForm(this.player, this.instanceName);
break;
case InstanceEditOptions.PreviousLayer:
this.instance.setLayer(this.instance.getLayer() - 1);
this.instance.decreaseLayer();
new InstanceEditForm(this.player, this.instanceName);
break;
case InstanceEditOptions.SetLayer:
this.setLayerForm();
break;
case InstanceEditOptions.Rotate:
this.player.sendMessage('§cRotating not implemented yet.');
break;
case InstanceEditOptions.Mirror:
this.player.sendMessage('§cMirroring not implemented yet.');
break;
case InstanceEditOptions.Move:
this.instance.move(this.player.dimension.id, this.player.location);
break;
case InstanceEditOptions.MaterialsList:
this.player.sendMessage('§cMaterial list not implemented yet.');
break;
case InstanceEditOptions.MainMenu:
new MenuForm(this.player);
new MenuForm(this.player, { jumpToInstance: false });
break;
default:
this.player.sendMessage(`§cUnknown option: ${option}`);
@@ -94,7 +103,16 @@ export class InstanceEditForm {
}
renameInstanceForm() {
InstanceEditFormBuilder.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;
}
structureCollection.rename(this.instanceName, newName);
this.instanceName = newName;
});
}
setLayerForm() {
+9 -8
View File
@@ -2,23 +2,24 @@ import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { MenuFormBuilder } from './MenuFormBuilder';
export class InstanceEditFormBuilder {
static buildInstance(instanceName, currentOptions, commonOptions) {
static buildInstance(instance, options) {
const location = instance.getLocation();
const form = new ActionFormData()
.title(MenuFormBuilder.menuTitle)
.body(`Instance: §2${instanceName}`)
currentOptions.forEach(option => {
form.button(`${option}`);
});
commonOptions.forEach(option => {
let body = `Instance: §2${instance.name}\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() {
static buildRenameInstance(currentName) {
return new ModalFormData()
.title(MenuFormBuilder.menuTitle)
.textField('Enter a new name for the instance:', 'example_instance')
.textField('Enter a new name for the instance:', currentName)
.submitButton('Rename');
}
+6 -8
View File
@@ -1,15 +1,13 @@
export const InstanceEditOptions = Object.freeze({
Unknown: "unknown",
MainMenu: 'Back to Main Menu',
PlaceInstance: 'Place Instance',
RemovePlacement: 'Remove Placement',
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',
Rotate: 'Rotate',
Mirror: 'Mirror',
MaterialsList: 'Get Materials List',
Move: 'Move Here'
});
+15 -9
View File
@@ -4,26 +4,32 @@ import { MenuFormBuilder } from './MenuFormBuilder';
import { InstanceEditForm } from './InstanceEditForm';
export class MenuForm {
constructor(player) {
constructor(player, { jumpToInstance = true } = {}) {
this.player = player;
this.show();
this.show(jumpToInstance);
}
async show() {
let instanceName = this.getInstanceFromLocation();
if (!instanceName)
instanceName = await this.getInstanceNameFromForm();
async show(jumpToInstance = true) {
let instanceName;
if (jumpToInstance) {
instanceName = this.getInstanceNameAtLocation();
if (instanceName) {
new InstanceEditForm(this.player, instanceName);
return;
}
}
instanceName = await this.getInstanceNameFromForm();
if (!instanceName)
return;
new InstanceEditForm(this.player, instanceName);
}
getInstanceFromLocation() {
const locatedStructures = structureCollection.getStructuresAtLocation(this.player.location);
getInstanceNameAtLocation() {
const locatedStructures = structureCollection.getStructures(this.player.dimension.id, this.player.location, { useLayers: false });
if (locatedStructures.length === 0)
return void 0;
const structure = locatedStructures[0];
return structure.instanceName;
return structure.name;
}
async getInstanceNameFromForm() {
+4 -3
View File
@@ -1,4 +1,5 @@
import { structureCollection } from "./StructureCollection";
import { world } from "@minecraft/server";
export class Raycaster {
static STEP_SIZE = 0.2;
@@ -9,12 +10,10 @@ export class Raycaster {
let location = startLocation;
let distance = 0;
while (distance < maxDistance) {
const locatedStructures = structureCollection.getStructuresAtLocation(location);
const locatedStructures = structureCollection.getStructures(dimension.id, location);
if (locatedStructures.length !== 0) {
const structure = locatedStructures[0];
const block = structure.getBlock(structure.toStructureCoords(location));
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
break;
if (block?.type.id !== 'minecraft:air') {
blocks.push({
permutation: block,
@@ -23,6 +22,8 @@ export class Raycaster {
if (getFirst)
break;
}
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
break;
}
location = {
x: location.x + (direction.x*this.STEP_SIZE),
+27 -7
View File
@@ -8,6 +8,21 @@ class StructureCollection {
this.structures = {};
}
loadExistingStructures() {
world.getDynamicPropertyIds().filter(id => id.startsWith('structOptions:')).forEach(id => {
const instanceName = id.replace('structOptions:', '');
let structureId;
try {
structureId = StructureInstance.parseOptions(instanceName).structureId;
this.structures[instanceName] = new StructureInstance(instanceName, structureId);
} catch (e) {
world.sendMessage(`§c[StrucTool] Error loading structure instance '${instanceName}'. It will be removed.`);
world.setDynamicProperty(id, void 0);
throw e;
}
});
}
add(instanceName, structureId) {
if (this.structures[instanceName])
throw new Error(`Instance ${instanceName} already exists.`);
@@ -24,9 +39,9 @@ class StructureCollection {
return structure;
}
remove(instanceName) {
delete(instanceName) {
const struct = this.get(instanceName);
struct.removePlacement();
struct.delete();
delete this.structures[instanceName];
}
@@ -34,12 +49,12 @@ class StructureCollection {
return Object.keys(this.structures);
}
getStructuresAtLocation(location) {
return Object.values(this.structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location)));
getStructures(dimensionId, location, options = {}) {
return Object.values(this.structures).filter(structure => structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options));
}
fetchStructureBlock(location) {
const locatedStructures = this.getStructuresAtLocation(location);
fetchStructureBlock(dimensionId, location) {
const locatedStructures = this.getStructures(dimensionId, location);
if (locatedStructures.length === 0)
return void 0;
const structure = locatedStructures[0];
@@ -56,10 +71,15 @@ class StructureCollection {
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();
export const structureCollection = new StructureCollection();
world.afterEvents.worldLoad.subscribe(() => {
structureCollection.loadExistingStructures();
});
+89 -27
View File
@@ -2,10 +2,12 @@ import { world } from "@minecraft/server";
import { Outliner } from "./Outliner";
export class StructureInstance {
name;
#structure;
#options = {
isPlaced: false,
dimensionId: 'minecraft:overworld',
structureId: void 0,
isEnabled: false,
dimensionId: void 0,
worldLocation: { x: 0, y: 0, z: 0 },
rotation: 0,
mirror: false,
@@ -14,13 +16,14 @@ export class StructureInstance {
constructor(instanceName, structureId) {
this.name = instanceName;
this.structureId = structureId;
this.#structure = world.structureManager.get(structureId);
if (!this.#structure) {
throw new Error(`[StrucTool] Structure '${this.structureId}' not found.`);
}
if (!this.#structure)
throw new Error(`[StrucTool] Structure '${structureId}' not found.`);
this.#options = this.loadOptions();
this.#options.isPlaced = false;
this.#options.structureId = structureId;
if (this.#options.isEnabled)
this.refreshOutliner();
this.updateOptions();
}
loadOptions() {
@@ -32,6 +35,21 @@ export class StructureInstance {
return this.#options;
}
delete() {
this.disable();
world.setDynamicProperty(`structOptions:${this.name}`, void 0);
this.#structure = void 0;
this.#options = void 0;
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));
}
@@ -84,7 +102,7 @@ export class StructureInstance {
}
getLayeredBounds() {
if (!this.#options.isPlaced)
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 },
@@ -92,22 +110,30 @@ export class StructureInstance {
};
}
place(dimensionId, worldLocation) {
this.#options.isPlaced = true;
this.move(dimensionId, worldLocation);
rename(newName) {
world.setDynamicProperty(`structOptions:${this.name}`, void 0);
this.name = newName;
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
removePlacement() {
if (!this.#options.isPlaced)
throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`);
this.#options.isPlaced = false;
place(dimensionId, worldLocation) {
this.move(dimensionId, worldLocation);
this.enable();
}
enable() {
this.#options.isEnabled = true;
this.updateOptions();
this.refreshOutliner();
}
disable() {
this.#options.isEnabled = false;
this.updateOptions();
this.outliner.stopDraw();
}
move(dimensionId, location) {
if (!this.#options.isPlaced)
throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`);
this.#options.dimensionId = dimensionId;
this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) };
this.updateOptions();
@@ -116,13 +142,15 @@ export class StructureInstance {
setLayer(layer) {
if (layer < 0 || layer > this.#structure.size.y)
throw new Error(`[StrucTool] Instance '${this.name}' of '${this.structureId}' does not have layer ${layer}.`);
throw new Error(`[StrucTool] Layer ${layer} is out of bounds.`);
this.#options.currentLayer = layer;
this.updateOptions();
this.refreshOutliner();
}
refreshOutliner() {
if (!this.#options.isEnabled)
return;
if (this.outliner)
this.outliner.stopDraw();
if (this.#options.currentLayer > 0) {
@@ -133,26 +161,30 @@ export class StructureInstance {
}
}
isLocationInStructure(structureLocation) {
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;
}
isLocationInLayer(structureLocation) {
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;
}
isLocationActive(structureLocation) {
if (!this.#options.isPlaced)
isLocationActive(dimensionId, structureLocation, { useLayers = true } = {}) {
if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId)
return false
if (this.#options.currentLayer > 0)
return this.isLocationInLayer(structureLocation);
return this.isLocationInStructure(structureLocation);
if (useLayers && this.#options.currentLayer !== 0)
return this.isLocationInLayer(dimensionId, structureLocation);
return this.isLocationInStructure(dimensionId, structureLocation);
}
toGlobalCoords(structureLocation) {
@@ -171,7 +203,37 @@ export class StructureInstance {
};
}
isPlaced() {
return this.#options.isPlaced;
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.#structure.size.y > 1;
}
isAtMaxLayer() {
return !this.hasLayers || this.#options.currentLayer >= this.#structure.size.y;
}
isAtMinLayer() {
return !this.hasLayers || this.#options.currentLayer <= 0;
}
increaseLayer() {
if (this.isAtMaxLayer())
this.setLayer(0);
else
this.setLayer(this.#options.currentLayer + 1);
}
decreaseLayer() {
if (this.isAtMinLayer())
this.setLayer(this.#structure.size.y);
else
this.setLayer(this.#options.currentLayer - 1);
}
}
+1 -9
View File
@@ -18,7 +18,7 @@ extension.addRule(easyPlace);
function onPlayerPlaceBlock(event) {
const { player, block } = event;
if (!player || !block || !hasActionItemInCorrectSlot(player)) return;
const structureBlock = fetchStructureBlock(block.location);
const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location);
if (!structureBlock)
return;
tryPlaceBlock(event, player, block, structureBlock);
@@ -32,14 +32,6 @@ function hasActionItemInCorrectSlot(player) {
return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace';
}
function fetchStructureBlock(location) {
const locatedStructures = structureCollection.getStructuresAtLocation(location);
if (locatedStructures.length === 0)
return void 0;
const structure = locatedStructures[0];
return structure.getBlock(structure.toStructureCoords(location));
}
function tryPlaceBlock(event, player, block, structureBlock) {
if (isBannedBlock(player, structureBlock)) return;
structureBlock = tryConvertBannedToValidBlock(structureBlock);
+5 -3
View File
@@ -8,7 +8,7 @@ import { Raycaster } from '../classes/Raycaster';
let runner = void 0;
const easyPlace = new Rule({
identifier: 'fastEasyPlace',
description: { text: "Looking at structure blocks with paper named 'easyPlace' in your hand will place them." },
description: { text: "Looking at structure blocks with a paper named 'easyPlace' in your hand will place them." },
onEnableCallback: () => { runner = system.runInterval(onTick, 2); },
onDisableCallback: () => { system.clearRun(runner); }
})
@@ -39,7 +39,7 @@ function isHoldingActionItem(player) {
}
function tryPlaceBlock(player, worldBlock, structureBlock) {
if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(worldBlock)) return;
if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock)) return;
structureBlock = tryConvertBannedToValidBlock(structureBlock);
if (player.getGameMode() === GameMode.creative) {
placeBlock(worldBlock, structureBlock);
@@ -49,11 +49,13 @@ function tryPlaceBlock(player, worldBlock, structureBlock) {
}
}
function locationIsPlaceable(worldBlock) {
function locationIsPlaceable(player, worldBlock) {
return worldBlock.isAir;
}
function isBannedBlock(player, structureBlock) {
if (!structureBlock)
return true;
const blockId = structureBlock.type.id.replace('minecraft:', '');
if (bannedBlocks.includes(blockId))
return true;