Merge branch 'main' into easyPlace

This commit is contained in:
ForestOfLight
2025-04-14 12:17:57 -07:00
Unverified
18 changed files with 739 additions and 249 deletions
+7 -7
View File
@@ -30,14 +30,14 @@ Removes a structure.
## Roadmap
- [ ] place structure **(in progress)**
- [x] Form to manage structure
- [x] Structure naming & movement
- [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
- [ ] send structure as a scriptevent so that other addons can use it
- [ ] more litematica features!
- [ ] Automatic Armor stand posing
- [ ] Automatic material gathering from inventories
- [ ] Correct block placement checking
- [ ] Structure Mirroring & Rotation
- [ ] Structure Merging into SuperStructures
## Issues & Suggestions
+5 -5
View File
@@ -4,7 +4,7 @@
"name": "StrucTool",
"description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.",
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"min_engine_version": [1, 21, 60],
"min_engine_version": [1, 21, 70],
"version": [1, 0, 0]
},
"modules": [
@@ -26,19 +26,19 @@
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.18.0-beta"
"version": "2.0.0-beta"
},
{
"module_name": "@minecraft/server-ui",
"version": "1.4.0-beta"
"version": "2.0.0-beta"
},
{
"uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP
"version": [1, 0, 2]
"version": [1, 0, 3]
},
{
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
"version": [1, 3, 8]
"version": [1, 3, 9]
}
],
"metadata": {
+28 -10
View File
@@ -1,24 +1,42 @@
import { system, world } from '@minecraft/server';
import { Raycaster } from '../classes/Raycaster';
system.runInterval(() => {
class BlockInfo {
static shownToLastTick = new Set();
static onTick() {
for (const player of world.getAllPlayers()) {
if (!player)
continue;
showStructureBlockInfo(player);
this.showStructureBlockInfo(player);
}
}
});
function showStructureBlockInfo(player) {
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true });
static showStructureBlockInfo(player) {
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, useLayers: false });
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: getFormattedBlockInfo(block.permutation) });
}
player.onScreenDisplay.setActionBar({ text: this.getFormattedBlockInfo(block.permutation) });
this.shownToLastTick.add(player.id);
}
function getFormattedBlockInfo(block) {
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${JSON.stringify(block.getAllStates())}`;
return header + `§a${block.type.id}`;
return header + `§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');
}
}
system.runInterval(() => BlockInfo.onTick());
+130
View File
@@ -0,0 +1,130 @@
import { structureCollection } from './StructureCollection';
import { MenuForm } from '../classes/MenuForm';
import { InstanceEditOptions } from './InstanceEditOptions';
import { InstanceEditFormBuilder } from './InstanceEditFormBuilder';
export class InstanceEditForm {
instanceName;
#buttons = {
isEnabled: [
InstanceEditOptions.NextLayer,
InstanceEditOptions.PreviousLayer,
InstanceEditOptions.SetLayer,
InstanceEditOptions.Move,
InstanceEditOptions.RenameInstance,
InstanceEditOptions.DisableInstance,
],
isNotEnabledAndIsNotPlaced: [
InstanceEditOptions.PlaceInstance,
InstanceEditOptions.RenameInstance
],
isNotEnabledButIsPlaced: [
InstanceEditOptions.EnableInstance,
InstanceEditOptions.RenameInstance
],
common: [
InstanceEditOptions.DeleteInstance,
InstanceEditOptions.MainMenu
]
}
constructor(player, instanceName) {
this.player = player;
this.instanceName = instanceName;
this.instance = structureCollection.get(this.instanceName);
this.show();
}
show() {
const currentOptions = this.getActiveOptions();
InstanceEditFormBuilder.buildInstance(this.instance, currentOptions).show(this.player).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 !== 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.RenameInstance:
this.renameInstanceForm();
break;
case InstanceEditOptions.DeleteInstance:
structureCollection.delete(this.instanceName);
break;
case InstanceEditOptions.NextLayer:
this.instance.increaseLayer();
new InstanceEditForm(this.player, this.instanceName);
break;
case InstanceEditOptions.PreviousLayer:
this.instance.decreaseLayer();
new InstanceEditForm(this.player, this.instanceName);
break;
case InstanceEditOptions.SetLayer:
this.setLayerForm();
break;
case InstanceEditOptions.Move:
this.instance.move(this.player.dimension.id, this.player.location);
break;
case InstanceEditOptions.MainMenu:
new MenuForm(this.player, { jumpToInstance: false });
break;
default:
this.player.sendMessage(`§cUnknown option: ${option}`);
break;
}
}
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;
}
try {
structureCollection.rename(this.instanceName, newName);
this.instanceName = newName;
} catch (e) {
this.player.sendMessage(`§cError renaming instance: ${e.message}`);
return;
}
});
}
setLayerForm() {
InstanceEditFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => {
if (response.canceled) return;
const selectedLayer = response.formValues[0];
this.instance.setLayer(parseInt(selectedLayer));
});
}
}
@@ -0,0 +1,33 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { MenuFormBuilder } from './MenuFormBuilder';
export class InstanceEditFormBuilder {
static buildInstance(instance, options) {
const location = instance.getLocation();
const form = new ActionFormData()
.title(MenuFormBuilder.menuTitle)
let body = `Instance: §a${instance.name}\n§fStructure: §2${instance.getStructureId()}\n`;
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 buildSetLayer(maxLayer, currentLayer) {
return new ModalFormData()
.title(MenuFormBuilder.menuTitle)
.label('Use the slider to select the layer. Use 0 for all layers.')
.slider("Layer", 0, maxLayer, 1, currentLayer)
.submitButton('Set Layer');
}
}
+13
View File
@@ -0,0 +1,13 @@
export const InstanceEditOptions = Object.freeze({
Unknown: 'Unknown',
MainMenu: '<<',
PlaceInstance: '§aPlace Instance',
EnableInstance: '§aEnable Instance',
DisableInstance: '§cDisable Instance',
RenameInstance: 'Rename Instance',
DeleteInstance: '§cDelete Instance',
NextLayer: 'Increase Layer',
PreviousLayer: 'Decrease Layer',
SetLayer: 'Set Layer',
Move: 'Move Here'
});
+81
View File
@@ -0,0 +1,81 @@
import { forceShow } from '../utils';
import { structureCollection } from './StructureCollection';
import { MenuFormBuilder } from './MenuFormBuilder';
import { InstanceEditForm } from './InstanceEditForm';
export class MenuForm {
constructor(player, { jumpToInstance = true } = {}) {
this.player = player;
this.show(jumpToInstance);
}
async show(jumpToInstance = true) {
let instanceName;
if (jumpToInstance) {
instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useLayers: false })?.name;
if (instanceName) {
new InstanceEditForm(this.player, instanceName);
return;
}
}
instanceName = await this.getInstanceNameFromForm();
if (!instanceName)
return;
new InstanceEditForm(this.player, instanceName);
}
async getInstanceNameFromForm() {
try {
return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => {
if (response.canceled) return;
if (response.selection === structureCollection.getInstanceNames().length + 1) {
MenuFormBuilder.buildHowToAddNewStructures().show(this.player);
return;
}
const selectedInstanceName = structureCollection.getInstanceNames()[response.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;
structureCollection.add(instanceName, structureId);
return instanceName;
});
}
async getStructureId() {
return MenuFormBuilder.buildAllStructures().show(this.player).then((response) => {
if (response.canceled)
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;
});
}
}
+54
View File
@@ -0,0 +1,54 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { structureCollection } from './StructureCollection';
export class MenuFormBuilder {
static menuTitle = '§l§2StrucTool §8Menu';
static buildAllInstanceName() {
const allInstanceNameForm = new ActionFormData()
.title(this.menuTitle)
.body('Select an instance:');
structureCollection.getInstanceNames().forEach(instanceName => {
allInstanceNameForm.button(`§2${instanceName}`);
});
allInstanceNameForm.button('Create New Instance');
allInstanceNameForm.button('How to Add New Structures');
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');
return allStructuresForm;
}
static buildOtherStructure() {
return new ModalFormData()
.title(this.menuTitle)
.textField('Enter the Structure ID:', 'example_structure')
.submitButton('Submit');
}
static buildHowToAddNewStructures() {
let body = "How to Add Structures:\n"
body += "§7- Save a structure using a structure block or the /structure command.\n"
body += "§7OR\n"
body += "§7- Add a .mcstructure file to this pack's structures folder. When selecting your structure, select the 'Other' option and then use the filename (without '.mcstructure') as the Structure ID. After its first use, it will be added to the list of structures.";
return new ActionFormData()
.title(this.menuTitle)
.body(body);
}
}
+14 -9
View File
@@ -1,21 +1,18 @@
import { world } from "@minecraft/server";
import { structureCollection } from "./StructureCollection";
import { world } from "@minecraft/server";
export class Raycaster {
static STEP_SIZE = 0.2;
static getStructureBlocks(dimension, startLocation, direction, { maxDistance, getFirst = true, collideWithWorldBlocks = true }) {
static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, useLayers = 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 locatedStructures = structureCollection.getStructuresAtLocation(location);
if (locatedStructures.length !== 0) {
const structure = locatedStructures[0];
const structure = structureCollection.getStructure(dimension.id, location, { useLayers });
if (structure) {
const block = structure.getBlock(structure.toStructureCoords(location));
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
break;
if (block?.type.id !== 'minecraft:air') {
blocks.push({
permutation: block,
@@ -24,6 +21,14 @@ export class Raycaster {
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),
@@ -36,11 +41,11 @@ export class Raycaster {
return blocks;
}
static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true }) {
static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, useLayers = true } = {}) {
const startLocation = player.getHeadLocation();
const direction = player.getViewDirection();
const maxDistance = 7;
const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks });
const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks, useLayers });
if (blocks.length === 0)
return void 0;
return isFirst ? blocks[0] : blocks[blocks.length - 1];
-158
View File
@@ -1,158 +0,0 @@
import { MinecraftDimensionTypes, world } from "@minecraft/server";
import { Outliner } from "./Outliner";
export class Structure {
#structure;
#options = {
isPlaced: false,
dimensionId: MinecraftDimensionTypes.overworld,
worldLocation: { x: 0, y: 0, z: 0 },
rotation: 0,
mirror: false,
currentLayer: 0
};
constructor(structureName) {
this.name = structureName;
this.#structure = world.structureManager.get(structureName);
if (!this.#structure) {
throw new Error(`[StrucTool] Structure '${structureName}' not found.`);
}
this.#options = this.loadOptions();
}
loadOptions() {
try {
return JSON.parse(world.getDynamicProperty(`structOptions:${this.name}`));
} catch (e) {
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
return this.#options;
}
updateOptions() {
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
getStructure() {
return this.#structure;
}
getLocation() {
return this.#options.worldLocation;
}
getHeight() {
return this.#structure.size.y;
}
getLayer() {
return this.#options.currentLayer;
}
*getBlocks() {
const max = this.#structure.size;
for (let x = 0; x < max.x; x++) {
for (let y = 0; y < max.y; y++) {
for (let z = 0; z < max.z; z++) {
yield this.#structure.getBlockPermutation({ x, y, z });
}
}
}
}
*getLayerBlocks(y) {
const max = this.#structure.size;
for (let x = 0; x < max.x; x++) {
for (let z = 0; z < max.z; z++) {
yield this.#structure.getBlockPermutation({ x, y, z });
}
}
}
getBlock(structureLocation) {
return this.#structure.getBlockPermutation(structureLocation);
}
getBounds() {
return {
min: { x: 0, y: 0, z: 0 },
max: this.#structure.size
};
}
getLayeredBounds() {
if (!this.#options.isPlaced)
throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`);
return {
min: { x: 0, y: this.#options.currentLayer - 1, z: 0 },
max: { x: this.#structure.size.x, y: this.#options.currentLayer, z: this.#structure.size.z }
};
}
place(dimensionId, worldLocation) {
this.#options = {
isPlaced: true,
dimensionId,
worldLocation: { x: Math.floor(worldLocation.x), y: Math.floor(worldLocation.y), z: Math.floor(worldLocation.z) },
};
this.updateOptions();
this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max));
}
remove() {
if (!this.#options.isPlaced)
throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`);
this.#options.isPlaced = false;
this.updateOptions();
this.outliner.stopDraw();
}
setLayer(layer) {
if (layer < 1 || layer > this.#structure.size.y)
throw new Error(`[StrucTool] Structure '${this.name}' does not have layer ${layer}.`);
this.#options.currentLayer = layer;
this.updateOptions();
this.outliner.stopDraw();
const { min, max } = this.getLayeredBounds();
this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max));
}
isLocationInStructure(structureLocation) {
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) {
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)
return false
if (this.#options.currentLayer > 0)
return this.isLocationInLayer(structureLocation);
return this.isLocationInStructure(structureLocation);
}
toGlobalCoords(structureLocation) {
return {
x: this.#options.worldLocation.x + structureLocation.x,
y: this.#options.worldLocation.y + structureLocation.y,
z: this.#options.worldLocation.z + structureLocation.z
};
}
toStructureCoords(worldLocation) {
return {
x: worldLocation.x - this.#options.worldLocation.x,
y: worldLocation.y - this.#options.worldLocation.y,
z: worldLocation.z - this.#options.worldLocation.z
};
}
}
+63 -21
View File
@@ -1,46 +1,88 @@
import { Structure } from './Structure';
import { StructureInstance } from './StructureInstance';
import { world } from '@minecraft/server';
class StructureCollection {
#structures;
structures;
constructor() {
this.#structures = {};
this.structures = {};
}
add(name) {
if (this.#structures[name]) {
throw new Error(`Structure ${name} already exists.`);
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;
}
const structure = new Structure(name);
this.#structures[name] = structure;
});
}
add(instanceName, structureId) {
if (this.structures[instanceName])
throw new Error(`Instance ${instanceName} already exists.`);
const structure = new StructureInstance(instanceName, structureId);
this.structures[instanceName] = structure;
return structure;
}
get(name) {
const structure = this.#structures[name];
get(instanceName) {
const structure = this.structures[instanceName];
if (!structure) {
throw new Error(`Structure ${name} not found.`);
throw new Error(`Instance ${instanceName} not found.`);
}
return structure;
}
remove(name) {
const struct = this.get(name);
struct.remove();
delete this.#structures[name];
delete(instanceName) {
const struct = this.get(instanceName);
struct.delete();
delete this.structures[instanceName];
}
getStructuresAtLocation(location) {
return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location)));
getInstanceNames() {
return Object.keys(this.structures);
}
fetchStructureBlock(location) {
const locatedStructures = this.getStructuresAtLocation(location);
if (locatedStructures.length === 0)
getStructures(dimensionId, location, options = {}) {
return Object.values(this.structures).filter(structure => structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options));
}
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;
const structure = locatedStructures[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.loadExistingStructures();
});
+244
View File
@@ -0,0 +1,244 @@
import { world } from "@minecraft/server";
import { Outliner } from "./Outliner";
export class StructureInstance {
name;
#structure;
#options = {
structureId: void 0,
isEnabled: false,
dimensionId: void 0,
worldLocation: { x: 0, y: 0, z: 0 },
rotation: 0,
mirror: false,
currentLayer: 0
};
constructor(instanceName, structureId) {
this.name = instanceName;
this.#structure = world.structureManager.get(structureId);
if (!this.#structure)
throw new Error(`[StrucTool] Structure '${structureId}' not found.`);
this.#structure.saveToWorld();
this.#options = this.loadOptions();
this.#options.structureId = structureId;
if (this.#options.isEnabled)
this.refreshOutliner();
this.updateOptions();
}
loadOptions() {
try {
return JSON.parse(world.getDynamicProperty(`structOptions:${this.name}`));
} catch (e) {
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
return this.#options;
}
delete() {
this.disable();
world.setDynamicProperty(`structOptions:${this.name}`, void 0);
this.#structure = void 0;
this.#options = void 0;
delete this.outliner;
}
static parseOptions(instanceName) {
const options = JSON.parse(world.getDynamicProperty(`structOptions:${instanceName}`));
if (!options)
throw new Error(`[StrucTool] Instance '${instanceName}' not found.`);
return options;
}
updateOptions() {
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
getStructure() {
return this.#structure;
}
getStructureId() {
return this.#options.structureId;
}
getLocation() {
return { dimensionId: this.#options.dimensionId, location: this.#options.worldLocation };
}
getHeight() {
return this.#structure.size.y;
}
getLayer() {
return this.#options.currentLayer || 0;
}
*getBlocks() {
const max = this.#structure.size;
for (let x = 0; x < max.x; x++) {
for (let y = 0; y < max.y; y++) {
for (let z = 0; z < max.z; z++) {
yield this.#structure.getBlockPermutation({ x, y, z });
}
}
}
}
*getLayerBlocks(y) {
const max = this.#structure.size;
for (let x = 0; x < max.x; x++) {
for (let z = 0; z < max.z; z++) {
yield this.#structure.getBlockPermutation({ x, y, z });
}
}
}
getBlock(structureLocation) {
return this.#structure.getBlockPermutation(structureLocation);
}
getBounds() {
return {
min: { x: 0, y: 0, z: 0 },
max: this.#structure.size
};
}
getLayeredBounds() {
if (!this.#options.isEnabled)
throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`);
return {
min: { x: 0, y: this.#options.currentLayer - 1, z: 0 },
max: { x: this.#structure.size.x, y: this.#options.currentLayer, z: this.#structure.size.z }
};
}
rename(newName) {
world.setDynamicProperty(`structOptions:${this.name}`, void 0);
this.name = newName;
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
}
place(dimensionId, worldLocation) {
this.move(dimensionId, worldLocation);
this.enable();
}
enable() {
this.#options.isEnabled = true;
this.updateOptions();
this.refreshOutliner();
}
disable() {
this.#options.isEnabled = false;
this.updateOptions();
this.outliner.stopDraw();
}
move(dimensionId, location) {
this.#options.dimensionId = dimensionId;
this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) };
this.updateOptions();
this.refreshOutliner();
}
setLayer(layer) {
if (layer < 0 || layer > this.#structure.size.y)
throw new Error(`[StrucTool] Layer ${layer} is out of bounds.`);
this.#options.currentLayer = layer;
this.updateOptions();
this.refreshOutliner();
}
refreshOutliner() {
if (!this.#options.isEnabled)
return;
if (this.outliner)
this.outliner.stopDraw();
if (this.#options.currentLayer > 0) {
const { min, max } = this.getLayeredBounds();
this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max));
} else {
this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max));
}
}
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(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(dimensionId, structureLocation, { useLayers = true } = {}) {
if (!this.#options.isEnabled || this.#options.dimensionId !== dimensionId)
return false
if (useLayers && this.#options.currentLayer !== 0)
return this.isLocationInLayer(dimensionId, structureLocation);
return this.isLocationInStructure(dimensionId, structureLocation);
}
toGlobalCoords(structureLocation) {
return {
x: this.#options.worldLocation.x + structureLocation.x,
y: this.#options.worldLocation.y + structureLocation.y,
z: this.#options.worldLocation.z + structureLocation.z
};
}
toStructureCoords(worldLocation) {
return {
x: worldLocation.x - this.#options.worldLocation.x,
y: worldLocation.y - this.#options.worldLocation.y,
z: worldLocation.z - this.#options.worldLocation.z
};
}
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);
}
}
+24
View File
@@ -0,0 +1,24 @@
import { Command } from '../lib/canopy/CanopyExtension';
import { extension } from '../config';
import { world, system } from '@minecraft/server';
import { MenuForm } from '../classes/MenuForm';
const ACTION_ITEM = 'minecraft:paper';
const structCmd = new Command({
name: 'menu',
description: { text: 'Manages current StrucTool structures.' },
usage: 'menu',
callback: structCommand
});
extension.addCommand(structCmd);
world.beforeEvents.itemUse.subscribe((event) => {
if (!event.source || event.itemStack?.typeId !== ACTION_ITEM) return;
event.cancel = true;
system.run(() => structCommand(event.source));
});
function structCommand(sender) {
new MenuForm(sender);
}
+5 -5
View File
@@ -17,7 +17,7 @@ const structCmd = new Command({
extension.addCommand(structCmd);
function structCommand(sender, args) {
const { option, name, arg3 } = args;
const { name, option, arg3 } = args;
switch (option) {
case 'add':
addStructure(sender, name);
@@ -41,7 +41,7 @@ function structCommand(sender, args) {
function addStructure(sender, name) {
try {
structureCollection.add(name);
structureCollection.add(name, name);
} catch (e) {
if (e.message.includes('already exists')) {
sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
@@ -70,7 +70,7 @@ function placeStructure(sender, name) {
structure = structureCollection.get(name);
} catch (e) {
try {
structure = structureCollection.add(name);
structure = structureCollection.add(name, name);
} catch (e) {
if (e.message.includes('already exists')) {
sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
@@ -108,8 +108,8 @@ function printInfo(sender, name) {
sender.sendMessage({ text: `§cStructure '${name}' not found.` });
return;
}
const location = structure.getLocation();
sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}]` });
const { dimensionId, location } = structure.getLocation();
sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}] in '${dimensionId}'` });
sender.sendMessage({ text: `§7Current Layer: ${structure.getLayer()}` });
sender.sendMessage({ text: `§7Materials: ${MaterialCounter.getPrintable(name)}` });
}
+1
View File
@@ -4,6 +4,7 @@ import './rules/fastEasyPlace';
// Commands
import './commands/struct';
import './commands/menu';
// Other
import './classes/BlockInfo';
+16 -16
View File
@@ -5,11 +5,11 @@ import { structureCollection } from '../classes/StructureCollection';
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
blockIdToItemStackMap } from '../data';
const ARROW_SLOT = 35;
const ACTION_SLOT = 35;
const easyPlace = new Rule({
identifier: 'easyPlace',
description: { text: 'Simplifies placing blocks in a structure (arrow in bottom right inventory slot).' },
description: { text: "Simplifies placing blocks in a structure (paper named 'easyPlace' in bottom right inventory slot)." },
onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); },
onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); }
})
@@ -17,31 +17,24 @@ extension.addRule(easyPlace);
function onPlayerPlaceBlock(event) {
const { player, block } = event;
if (!player || !block || !hasArrowInCorrectSlot(player)) return;
const structureBlock = fetchStructureBlock(block.location);
if (!player || !block || !hasActionItemInCorrectSlot(player)) return;
const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location);
if (!structureBlock)
return;
tryPlaceBlock(event, player, block, structureBlock);
}
function hasArrowInCorrectSlot(player) {
function hasActionItemInCorrectSlot(player) {
const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container;
if (!inventory)
return false;
const arrowSlot = inventory.getSlot(ARROW_SLOT);
return arrowSlot.hasItem() && arrowSlot.typeId === 'minecraft:arrow';
}
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));
const actionSlot = inventory.getSlot(ACTION_SLOT);
return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace';
}
function tryPlaceBlock(event, player, block, structureBlock) {
if (isBannedBlock(player, structureBlock)) return;
if (isBannedBlock(player, structureBlock))
preventAction(event, player);
structureBlock = tryConvertBannedToValidBlock(structureBlock);
if (player.getGameMode() === GameMode.creative) {
placeBlock(block, structureBlock);
@@ -51,6 +44,13 @@ function tryPlaceBlock(event, player, block, structureBlock) {
}
}
function preventAction(event, player) {
event.cancel = true;
system.run(() => {
player.onScreenDisplay.setActionBar('§cAction prevented by easyPlace.');
});
}
function isBannedBlock(player, structureBlock) {
const blockId = structureBlock.type.id.replace('minecraft:', '');
if (bannedBlocks.includes(blockId))
+8 -6
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 an arrow 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); }
})
@@ -23,7 +23,7 @@ function onTick() {
}
function processEasyPlace(player) {
if (!player || !isHoldingArrow(player)) return;
if (!player || !isHoldingActionItem(player)) return;
const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true });
if (!structureBlock)
return;
@@ -31,15 +31,15 @@ function processEasyPlace(player) {
tryPlaceBlock(player, worldBlock, structureBlock.permutation);
}
function isHoldingArrow(player) {
function isHoldingActionItem(player) {
const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand);
if (!mainhandItemStack)
return false;
return mainhandItemStack.typeId === 'minecraft:arrow';
return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'easyPlace';
}
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;
+3 -2
View File
@@ -1,3 +1,4 @@
import { system } from '@minecraft/server';
import { FormCancelationReason } from '@minecraft/server-ui';
export async function forceShow(player, form, timeout = Infinity) {
@@ -5,9 +6,9 @@ export async function forceShow(player, form, timeout = Infinity) {
while ((system.currentTick - startTick) < timeout) {
const response = await form.show(player);
if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy)
player.sendMessage({ translate: 'commands.canopy.menu.busy' });
player.sendMessage("§8Close your chat window to access the menu.");
if (response.cancelationReason !== FormCancelationReason.UserBusy)
return response;
}
throw new Error({ translate: 'commands.canopy.menu.timeout', with: [String(timeout)] });
throw new Error("Menu timed out.");
};