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 ## Roadmap
- [ ] place structure **(in progress)** - [x] Form to manage structure
- [x] Structure naming & movement
- [x] easyPlace rule - [x] easyPlace rule
- [x] Make a viewable material list - [ ] Automatic Armor stand posing
- [ ] Make automatic material gathering from inventories - [ ] Automatic material gathering from inventories
- [ ] structure movement & rotation - [ ] Correct block placement checking
- [ ] structuraMode & holoprintMode rules for ease of use - [ ] Structure Mirroring & Rotation
- [ ] send structure as a scriptevent so that other addons can use it - [ ] Structure Merging into SuperStructures
- [ ] more litematica features!
## Issues & Suggestions ## Issues & Suggestions
+5 -5
View File
@@ -4,7 +4,7 @@
"name": "StrucTool", "name": "StrucTool",
"description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.", "description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.",
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"min_engine_version": [1, 21, 60], "min_engine_version": [1, 21, 70],
"version": [1, 0, 0] "version": [1, 0, 0]
}, },
"modules": [ "modules": [
@@ -26,19 +26,19 @@
"dependencies": [ "dependencies": [
{ {
"module_name": "@minecraft/server", "module_name": "@minecraft/server",
"version": "1.18.0-beta" "version": "2.0.0-beta"
}, },
{ {
"module_name": "@minecraft/server-ui", "module_name": "@minecraft/server-ui",
"version": "1.4.0-beta" "version": "2.0.0-beta"
}, },
{ {
"uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP
"version": [1, 0, 2] "version": [1, 0, 3]
}, },
{ {
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
"version": [1, 3, 8] "version": [1, 3, 9]
} }
], ],
"metadata": { "metadata": {
+36 -18
View File
@@ -1,24 +1,42 @@
import { system, world } from '@minecraft/server'; import { system, world } from '@minecraft/server';
import { Raycaster } from '../classes/Raycaster'; import { Raycaster } from '../classes/Raycaster';
system.runInterval(() => { class BlockInfo {
for (const player of world.getAllPlayers()) { static shownToLastTick = new Set();
if (!player)
continue;
showStructureBlockInfo(player);
}
});
function showStructureBlockInfo(player) { static onTick() {
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); for (const player of world.getAllPlayers()) {
if (!block) if (!player)
return; continue;
player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); this.showStructureBlockInfo(player);
}
}
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: 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 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');
}
} }
function getFormattedBlockInfo(block) { system.runInterval(() => BlockInfo.onTick());
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())}`;
}
+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 { structureCollection } from "./StructureCollection";
import { world } from "@minecraft/server";
export class Raycaster { export class Raycaster {
static STEP_SIZE = 0.2; 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 // Can probably be optimized by the fact that we only need full blocks and aren't checking for partial blocks
const blocks = []; const blocks = [];
let location = startLocation; let location = startLocation;
let distance = 0; let distance = 0;
while (distance < maxDistance) { while (distance < maxDistance) {
const locatedStructures = structureCollection.getStructuresAtLocation(location); const structure = structureCollection.getStructure(dimension.id, location, { useLayers });
if (locatedStructures.length !== 0) { if (structure) {
const structure = locatedStructures[0];
const block = structure.getBlock(structure.toStructureCoords(location)); const block = structure.getBlock(structure.toStructureCoords(location));
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
break;
if (block?.type.id !== 'minecraft:air') { if (block?.type.id !== 'minecraft:air') {
blocks.push({ blocks.push({
permutation: block, permutation: block,
@@ -24,6 +21,14 @@ export class Raycaster {
if (getFirst) if (getFirst)
break; break;
} }
try {
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
break;
} catch (e) {
if (e.name === 'LocationOutOfWorldBoundariesError')
break;
throw e;
}
} }
location = { location = {
x: location.x + (direction.x*this.STEP_SIZE), x: location.x + (direction.x*this.STEP_SIZE),
@@ -36,11 +41,11 @@ export class Raycaster {
return blocks; return blocks;
} }
static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true }) { static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, useLayers = true } = {}) {
const startLocation = player.getHeadLocation(); const startLocation = player.getHeadLocation();
const direction = player.getViewDirection(); const direction = player.getViewDirection();
const maxDistance = 7; 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) if (blocks.length === 0)
return void 0; return void 0;
return isFirst ? blocks[0] : blocks[blocks.length - 1]; 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
};
}
}
+65 -23
View File
@@ -1,46 +1,88 @@
import { Structure } from './Structure'; import { StructureInstance } from './StructureInstance';
import { world } from '@minecraft/server';
class StructureCollection { class StructureCollection {
#structures; structures;
constructor() { constructor() {
this.#structures = {}; this.structures = {};
} }
add(name) { loadExistingStructures() {
if (this.#structures[name]) { world.getDynamicPropertyIds().filter(id => id.startsWith('structOptions:')).forEach(id => {
throw new Error(`Structure ${name} already exists.`); const instanceName = id.replace('structOptions:', '');
} let structureId;
const structure = new Structure(name); try {
this.#structures[name] = structure; 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.`);
const structure = new StructureInstance(instanceName, structureId);
this.structures[instanceName] = structure;
return structure; return structure;
} }
get(name) { get(instanceName) {
const structure = this.#structures[name]; const structure = this.structures[instanceName];
if (!structure) { if (!structure) {
throw new Error(`Structure ${name} not found.`); throw new Error(`Instance ${instanceName} not found.`);
} }
return structure; return structure;
} }
remove(name) { delete(instanceName) {
const struct = this.get(name); const struct = this.get(instanceName);
struct.remove(); struct.delete();
delete this.#structures[name]; delete this.structures[instanceName];
} }
getStructuresAtLocation(location) { getInstanceNames() {
return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); return Object.keys(this.structures);
} }
fetchStructureBlock(location) { getStructures(dimensionId, location, options = {}) {
const locatedStructures = this.getStructuresAtLocation(location); return Object.values(this.structures).filter(structure => structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options));
if (locatedStructures.length === 0) }
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 void 0;
const structure = locatedStructures[0];
return structure.getBlock(structure.toStructureCoords(location)); 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(); 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); extension.addCommand(structCmd);
function structCommand(sender, args) { function structCommand(sender, args) {
const { option, name, arg3 } = args; const { name, option, arg3 } = args;
switch (option) { switch (option) {
case 'add': case 'add':
addStructure(sender, name); addStructure(sender, name);
@@ -41,7 +41,7 @@ function structCommand(sender, args) {
function addStructure(sender, name) { function addStructure(sender, name) {
try { try {
structureCollection.add(name); structureCollection.add(name, name);
} catch (e) { } catch (e) {
if (e.message.includes('already exists')) { if (e.message.includes('already exists')) {
sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
@@ -70,7 +70,7 @@ function placeStructure(sender, name) {
structure = structureCollection.get(name); structure = structureCollection.get(name);
} catch (e) { } catch (e) {
try { try {
structure = structureCollection.add(name); structure = structureCollection.add(name, name);
} catch (e) { } catch (e) {
if (e.message.includes('already exists')) { if (e.message.includes('already exists')) {
sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
@@ -108,8 +108,8 @@ function printInfo(sender, name) {
sender.sendMessage({ text: `§cStructure '${name}' not found.` }); sender.sendMessage({ text: `§cStructure '${name}' not found.` });
return; return;
} }
const location = structure.getLocation(); const { dimensionId, location } = structure.getLocation();
sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}]` }); sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}] in '${dimensionId}'` });
sender.sendMessage({ text: `§7Current Layer: ${structure.getLayer()}` }); sender.sendMessage({ text: `§7Current Layer: ${structure.getLayer()}` });
sender.sendMessage({ text: `§7Materials: ${MaterialCounter.getPrintable(name)}` }); sender.sendMessage({ text: `§7Materials: ${MaterialCounter.getPrintable(name)}` });
} }
+1
View File
@@ -4,6 +4,7 @@ import './rules/fastEasyPlace';
// Commands // Commands
import './commands/struct'; import './commands/struct';
import './commands/menu';
// Other // Other
import './classes/BlockInfo'; import './classes/BlockInfo';
+16 -16
View File
@@ -5,11 +5,11 @@ import { structureCollection } from '../classes/StructureCollection';
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
blockIdToItemStackMap } from '../data'; blockIdToItemStackMap } from '../data';
const ARROW_SLOT = 35; const ACTION_SLOT = 35;
const easyPlace = new Rule({ const easyPlace = new Rule({
identifier: 'easyPlace', 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); }, onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); },
onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); }
}) })
@@ -17,31 +17,24 @@ extension.addRule(easyPlace);
function onPlayerPlaceBlock(event) { function onPlayerPlaceBlock(event) {
const { player, block } = event; const { player, block } = event;
if (!player || !block || !hasArrowInCorrectSlot(player)) return; if (!player || !block || !hasActionItemInCorrectSlot(player)) return;
const structureBlock = fetchStructureBlock(block.location); const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location);
if (!structureBlock) if (!structureBlock)
return; return;
tryPlaceBlock(event, player, block, structureBlock); tryPlaceBlock(event, player, block, structureBlock);
} }
function hasArrowInCorrectSlot(player) { function hasActionItemInCorrectSlot(player) {
const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container;
if (!inventory) if (!inventory)
return false; return false;
const arrowSlot = inventory.getSlot(ARROW_SLOT); const actionSlot = inventory.getSlot(ACTION_SLOT);
return arrowSlot.hasItem() && arrowSlot.typeId === 'minecraft:arrow'; 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) { function tryPlaceBlock(event, player, block, structureBlock) {
if (isBannedBlock(player, structureBlock)) return; if (isBannedBlock(player, structureBlock))
preventAction(event, player);
structureBlock = tryConvertBannedToValidBlock(structureBlock); structureBlock = tryConvertBannedToValidBlock(structureBlock);
if (player.getGameMode() === GameMode.creative) { if (player.getGameMode() === GameMode.creative) {
placeBlock(block, structureBlock); 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) { function isBannedBlock(player, structureBlock) {
const blockId = structureBlock.type.id.replace('minecraft:', ''); const blockId = structureBlock.type.id.replace('minecraft:', '');
if (bannedBlocks.includes(blockId)) if (bannedBlocks.includes(blockId))
+8 -6
View File
@@ -8,7 +8,7 @@ import { Raycaster } from '../classes/Raycaster';
let runner = void 0; let runner = void 0;
const easyPlace = new Rule({ const easyPlace = new Rule({
identifier: 'fastEasyPlace', 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); }, onEnableCallback: () => { runner = system.runInterval(onTick, 2); },
onDisableCallback: () => { system.clearRun(runner); } onDisableCallback: () => { system.clearRun(runner); }
}) })
@@ -23,7 +23,7 @@ function onTick() {
} }
function processEasyPlace(player) { function processEasyPlace(player) {
if (!player || !isHoldingArrow(player)) return; if (!player || !isHoldingActionItem(player)) return;
const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true }); const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true });
if (!structureBlock) if (!structureBlock)
return; return;
@@ -31,15 +31,15 @@ function processEasyPlace(player) {
tryPlaceBlock(player, worldBlock, structureBlock.permutation); tryPlaceBlock(player, worldBlock, structureBlock.permutation);
} }
function isHoldingArrow(player) { function isHoldingActionItem(player) {
const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand); const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand);
if (!mainhandItemStack) if (!mainhandItemStack)
return false; return false;
return mainhandItemStack.typeId === 'minecraft:arrow'; return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'easyPlace';
} }
function tryPlaceBlock(player, worldBlock, structureBlock) { function tryPlaceBlock(player, worldBlock, structureBlock) {
if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(worldBlock)) return; if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock)) return;
structureBlock = tryConvertBannedToValidBlock(structureBlock); structureBlock = tryConvertBannedToValidBlock(structureBlock);
if (player.getGameMode() === GameMode.creative) { if (player.getGameMode() === GameMode.creative) {
placeBlock(worldBlock, structureBlock); placeBlock(worldBlock, structureBlock);
@@ -49,11 +49,13 @@ function tryPlaceBlock(player, worldBlock, structureBlock) {
} }
} }
function locationIsPlaceable(worldBlock) { function locationIsPlaceable(player, worldBlock) {
return worldBlock.isAir; return worldBlock.isAir;
} }
function isBannedBlock(player, structureBlock) { function isBannedBlock(player, structureBlock) {
if (!structureBlock)
return true;
const blockId = structureBlock.type.id.replace('minecraft:', ''); const blockId = structureBlock.type.id.replace('minecraft:', '');
if (bannedBlocks.includes(blockId)) if (bannedBlocks.includes(blockId))
return true; return true;
+3 -2
View File
@@ -1,3 +1,4 @@
import { system } from '@minecraft/server';
import { FormCancelationReason } from '@minecraft/server-ui'; import { FormCancelationReason } from '@minecraft/server-ui';
export async function forceShow(player, form, timeout = Infinity) { 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) { while ((system.currentTick - startTick) < timeout) {
const response = await form.show(player); const response = await form.show(player);
if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy) 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) if (response.cancelationReason !== FormCancelationReason.UserBusy)
return response; return response;
} }
throw new Error({ translate: 'commands.canopy.menu.timeout', with: [String(timeout)] }); throw new Error("Menu timed out.");
}; };