Prep for RP
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "StrucTool",
|
||||
"description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.",
|
||||
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
|
||||
"min_engine_version": [1, 21, 70],
|
||||
"version": [1, 0, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"description": "Behavior Pack Module",
|
||||
"type": "data",
|
||||
"uuid": "f4d52ae1-2c26-4938-b8c2-7e455d495620",
|
||||
"version": [1, 0, 0]
|
||||
},
|
||||
{
|
||||
"description": "Gametest Module",
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"entry": "scripts/main.js",
|
||||
"uuid": "8bb9a1e4-0531-4b93-8ade-3880bfe1e2fc",
|
||||
"version": [1, 0, 0]
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"module_name": "@minecraft/server",
|
||||
"version": "2.0.0-beta"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-ui",
|
||||
"version": "2.0.0-beta"
|
||||
},
|
||||
{
|
||||
"uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP
|
||||
"version": [1, 0, 3]
|
||||
},
|
||||
{
|
||||
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
|
||||
"version": [1, 3, 9]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"authors": [ "ForestOfLight" ],
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,55 @@
|
||||
import { GameMode, system, world } from '@minecraft/server';
|
||||
import { Raycaster } from '../classes/Raycaster';
|
||||
import { fetchMatchingItemSlot } from '../utils';
|
||||
|
||||
class BlockInfo {
|
||||
static shownToLastTick = new Set();
|
||||
|
||||
static onTick() {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
if (!player)
|
||||
continue;
|
||||
this.showStructureBlockInfo(player);
|
||||
}
|
||||
}
|
||||
|
||||
static showStructureBlockInfo(player) {
|
||||
const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true, 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(player, block.permutation) });
|
||||
this.shownToLastTick.add(player.id);
|
||||
}
|
||||
|
||||
static getFormattedBlockInfo(player, block) {
|
||||
return 'Structure:' + this.getSupplyMessage(player, block) + '\n' + this.getBlockMessage(block);
|
||||
}
|
||||
|
||||
static getBlockMessage(block) {
|
||||
if (!block)
|
||||
return '§7Unknown';
|
||||
const states = block.getAllStates();
|
||||
if (Object.keys(states).length === 0)
|
||||
return `§a${block.type.id}`;
|
||||
else
|
||||
return `§a${block.type.id}\n§7${this.getFormattedStates(states)}`;
|
||||
}
|
||||
|
||||
static getFormattedStates(states) {
|
||||
return Object.entries(states).map(([key, value]) => `§7${key}: §3${value}`).join('\n');
|
||||
}
|
||||
|
||||
static getSupplyMessage(player, block) {
|
||||
const itemStack = fetchMatchingItemSlot(player, block.getItemStack()?.typeId);
|
||||
const isInSurvival = player.getGameMode() === GameMode.survival;
|
||||
if (!itemStack && isInSurvival)
|
||||
return ' §c[No Supply]';
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
system.runInterval(() => BlockInfo.onTick());
|
||||
@@ -0,0 +1,8 @@
|
||||
export const BlockVerificationLevel = Object.freeze({
|
||||
Unknown: 0,
|
||||
NoMatch: 1,
|
||||
TypeMatch: 2,
|
||||
TypeAndStateMatch: 3,
|
||||
Missing: 4,
|
||||
isAir: 5
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { BlockVerificationLevel } from "./BlockVerificationLevel";
|
||||
|
||||
export class BlockVerifier {
|
||||
constructor(block, instance) {
|
||||
this.block = block;
|
||||
this.instance = instance;
|
||||
this.blockLocationInStructure = instance.toStructureCoords(block.location);
|
||||
}
|
||||
|
||||
verify() {
|
||||
const structPermutation = this.instance.getBlock(this.blockLocationInStructure);
|
||||
return this.evaluatePermutations(this.block.permutation, structPermutation);
|
||||
}
|
||||
|
||||
evaluatePermutations(worldPermutation, structPermutation) {
|
||||
if (this.isCorrectlyAir(worldPermutation, structPermutation))
|
||||
return this.air();
|
||||
if (this.isMissing(worldPermutation, structPermutation))
|
||||
return this.missing();
|
||||
if (this.isExactMatch(worldPermutation, structPermutation))
|
||||
return this.matchingPermutations();
|
||||
if (this.isTypeMatch(worldPermutation, structPermutation))
|
||||
return this.matchingTypes();
|
||||
return this.matchingNone();
|
||||
}
|
||||
|
||||
isCorrectlyAir(worldPermutation, structPermutation) {
|
||||
return worldPermutation.type.id === "minecraft:air" && structPermutation.type.id === "minecraft:air";
|
||||
}
|
||||
|
||||
isMissing(worldPermutation, structPermutation) {
|
||||
return worldPermutation.type.id === "minecraft:air" && structPermutation.type.id !== "minecraft:air";
|
||||
}
|
||||
|
||||
isTypeMatch(worldPermuation, structurePermuation) {
|
||||
return worldPermuation.type.id === structurePermuation.type.id;
|
||||
}
|
||||
|
||||
isExactMatch(worldPermuation, structurePermuation) {
|
||||
return worldPermuation.matches(structurePermuation.type.id, structurePermuation.getAllStates());
|
||||
}
|
||||
|
||||
air() {
|
||||
return BlockVerificationLevel.isAir;
|
||||
}
|
||||
|
||||
missing() {
|
||||
return BlockVerificationLevel.Missing;
|
||||
}
|
||||
|
||||
matchingPermutations() {
|
||||
return BlockVerificationLevel.TypeAndStateMatch;
|
||||
}
|
||||
|
||||
matchingTypes() {
|
||||
return BlockVerificationLevel.TypeMatch;
|
||||
}
|
||||
|
||||
matchingNone() {
|
||||
return BlockVerificationLevel.NoMatch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
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.Statistics,
|
||||
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.Statistics:
|
||||
this.statisticsForm();
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
async statisticsForm() {
|
||||
const form = await InstanceEditFormBuilder.buildStatistics(this.instance)
|
||||
form.show(this.player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
|
||||
import { MenuFormBuilder } from './MenuFormBuilder';
|
||||
import { StructureVerifier } from './StructureVerifier';
|
||||
import { BlockVerificationLevel } from './BlockVerificationLevel';
|
||||
|
||||
export class InstanceEditFormBuilder {
|
||||
static buildInstance(instance, options) {
|
||||
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');
|
||||
}
|
||||
|
||||
static async buildStatistics(instance) {
|
||||
const buildStatisticsForm = new ActionFormData()
|
||||
.title(MenuFormBuilder.menuTitle)
|
||||
let message = '';
|
||||
const structureVerifier = new StructureVerifier(instance);
|
||||
const statistics = await structureVerifier.verifyStructure();
|
||||
message += `§fStatistics for §a${instance.name}§f:\n`;
|
||||
message += `§7Blocks: §2${instance.getTotalVolume() - statistics.correctlyAir}\n`;
|
||||
message += `§7Correct: §a${this.getFormattedStatistic(statistics, BlockVerificationLevel.TypeAndStateMatch)}\n`;
|
||||
message += `§7Block State Incorrect: §e${this.getFormattedStatistic(statistics, BlockVerificationLevel.TypeMatch)}\n`;
|
||||
message += `§7Incorrect: §c${this.getFormattedStatistic(statistics, BlockVerificationLevel.NoMatch)}\n`;
|
||||
message += `§7Missing: §c${this.getFormattedStatistic(statistics, BlockVerificationLevel.Missing)}\n`;
|
||||
buildStatisticsForm.body(message);
|
||||
return buildStatisticsForm;
|
||||
}
|
||||
|
||||
static getFormattedStatistic(statistics, blockVerificationLevel) {
|
||||
return `${statistics[blockVerificationLevel]} (${statistics.percentages[blockVerificationLevel].toFixed(2)}%%)`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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',
|
||||
Statistics: 'Statistics',
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { structureCollection } from '../classes/StructureCollection';
|
||||
|
||||
class MaterialCounter {
|
||||
static getAll(name) {
|
||||
const structure = structureCollection.get(name);
|
||||
const materials = {};
|
||||
for (const block of structure.getBlocks()) {
|
||||
const typeId = block?.getItemStack()?.typeId.replace('minecraft:', '');
|
||||
if (!typeId) continue;
|
||||
if (!materials[typeId]) {
|
||||
materials[typeId] = 0;
|
||||
}
|
||||
materials[typeId]++;
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
static getLayer(name, layer) {
|
||||
const structure = structureCollection.get(name);
|
||||
const materials = {};
|
||||
for (const block of structure.getLayerBlocks(layer)) {
|
||||
const typeId = block?.getItemStack()?.typeId.replace('minecraft:', '');
|
||||
if (!typeId) continue;
|
||||
if (!materials[typeId]) {
|
||||
materials[typeId] = 0;
|
||||
}
|
||||
materials[typeId]++;
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
static getPrintable(name) {
|
||||
const structure = structureCollection.get(name);
|
||||
const materials = {};
|
||||
for (const block of structure.getBlocks()) {
|
||||
const itemStack = block?.getItemStack();
|
||||
const typeId = itemStack?.typeId.replace('minecraft:', '');
|
||||
if (!typeId) continue;
|
||||
if (!materials[typeId]) {
|
||||
materials[typeId] = { count: 0, maxStack: itemStack.maxAmount };
|
||||
}
|
||||
materials[typeId].count++;
|
||||
}
|
||||
let message = [];
|
||||
for (const blockType in materials) {
|
||||
let count = materials[blockType].count;
|
||||
let countStr = '';
|
||||
const maxStack = materials[blockType].maxStack;
|
||||
const fullShulker = 27 * maxStack;
|
||||
if (count >= fullShulker) {
|
||||
countStr = `${Math.floor(count / fullShulker)} sb`;
|
||||
}
|
||||
if (count > fullShulker) {
|
||||
countStr += ' + ';
|
||||
}
|
||||
count %= fullShulker;
|
||||
if (count >= maxStack) {
|
||||
countStr += `${Math.floor(count / maxStack)} stack`;
|
||||
}
|
||||
if (count > maxStack) {
|
||||
countStr += ' + ';
|
||||
}
|
||||
count %= maxStack;
|
||||
if (count > 0) {
|
||||
countStr += count;
|
||||
}
|
||||
message.push(` ${blockType}: ${countStr}`);
|
||||
}
|
||||
return message.sort().join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export { MaterialCounter };
|
||||
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
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;
|
||||
if (response.selection === structureCollection.getWorldStructureIds().length + 1) {
|
||||
MenuFormBuilder.buildHowTo().show(this.player);
|
||||
return;
|
||||
}
|
||||
const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection];
|
||||
return selectedStructureId || this.getOtherStructureId();
|
||||
});
|
||||
}
|
||||
|
||||
getOtherStructureId() {
|
||||
return MenuFormBuilder.buildOtherStructure().show(this.player).then((response) => {
|
||||
if (response.canceled)
|
||||
return;
|
||||
const structureId = response.formValues[0];
|
||||
if (structureId === '')
|
||||
return void 0;
|
||||
return structureId;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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');
|
||||
return allInstanceNameForm;
|
||||
}
|
||||
|
||||
static buildNewInstance() {
|
||||
return new ModalFormData()
|
||||
.title(this.menuTitle)
|
||||
.textField('Enter a name for the new instance:', 'example_instance')
|
||||
.submitButton('Submit');
|
||||
}
|
||||
|
||||
static buildAllStructures() {
|
||||
const allStructuresForm = new ActionFormData()
|
||||
.title(this.menuTitle)
|
||||
.body('Select a structure:');
|
||||
structureCollection.getWorldStructureIds().forEach(structureId => {
|
||||
const structureName = structureId.replace('mystructure:', '');
|
||||
allStructuresForm.button(`§2${structureName}`);
|
||||
});
|
||||
allStructuresForm.button('Other');
|
||||
allStructuresForm.button('How to Add/Remove Structures');
|
||||
return allStructuresForm;
|
||||
}
|
||||
|
||||
static buildOtherStructure() {
|
||||
return new ModalFormData()
|
||||
.title(this.menuTitle)
|
||||
.textField('Enter the Structure ID:', 'example_structure')
|
||||
.submitButton('Submit');
|
||||
}
|
||||
|
||||
static buildHowTo() {
|
||||
let body = "§aHow to Add Structures:\n"
|
||||
body += "§7- Save a structure using a §fstructure block§7 or the §f/structure§7 command.\n"
|
||||
body += "§7§lOR§r\n"
|
||||
body += "§7- Add a §f.mcstructure§7 file to this pack's §fstructures folder§7. When selecting your structure, select the §fOther§7 option and then use the filename (without '.mcstructure') as the §fStructure ID§7. After its first use, it will be added to the list of structures.";
|
||||
body += "\n\n§cHow to Remove Structures:\n"
|
||||
body += "§7- Use the §f/structure delete§7 command to remove a structure from the world.\n"
|
||||
return new ActionFormData()
|
||||
.title(this.menuTitle)
|
||||
.body(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { system, world } from "@minecraft/server";
|
||||
import { Vector } from "../lib/Vector";
|
||||
|
||||
|
||||
export class Outliner {
|
||||
dimension;
|
||||
min = new Vector();
|
||||
max = new Vector();
|
||||
drawParticle = "minecraft:villager_happy";
|
||||
drawFrequency = 8;
|
||||
|
||||
#drawParticles = [];
|
||||
#runner = null;
|
||||
|
||||
constructor(dimension, min, max) {
|
||||
this.dimension = dimension;
|
||||
this.min = new Vector(min.x, min.y, min.z);
|
||||
this.max = new Vector(max.x, max.y, max.z);
|
||||
this.vertices = this.getVertices(min, max);
|
||||
this.startDraw();
|
||||
}
|
||||
|
||||
startDraw() {
|
||||
this.#runner = system.runInterval(() => this.draw(), this.drawFrequency);
|
||||
}
|
||||
|
||||
stopDraw() {
|
||||
system.clearRun(this.#runner);
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.#drawParticles.length = 0;
|
||||
this.#drawParticles.push(...this.getVerticeParticleLocations());
|
||||
this.#drawParticles.push(...this.getCubiodParticleLocations());
|
||||
|
||||
for (const [particleType, location] of this.#drawParticles) {
|
||||
try {
|
||||
this.dimension.spawnParticle(particleType, location);
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVertices(min, max) {
|
||||
return [
|
||||
new Vector(min.x, min.y, min.z),
|
||||
new Vector(max.x, min.y, min.z),
|
||||
new Vector(min.x, max.y, min.z),
|
||||
new Vector(max.x, max.y, min.z),
|
||||
new Vector(min.x, min.y, max.z),
|
||||
new Vector(max.x, min.y, max.z),
|
||||
new Vector(min.x, max.y, max.z),
|
||||
new Vector(max.x, max.y, max.z)
|
||||
];
|
||||
}
|
||||
|
||||
setVertices(dimension, min, max) {
|
||||
this.dimension = dimension;
|
||||
this.min = new Vector(min.x, min.y, min.z);
|
||||
this.max = new Vector(max.x, max.y, max.z);
|
||||
this.vertices = this.getVertices(min, max);
|
||||
}
|
||||
|
||||
getVerticeParticleLocations() {
|
||||
return this.vertices.map((v) => [this.drawParticle, v]);
|
||||
}
|
||||
|
||||
getCubiodParticleLocations() {
|
||||
const edges = [
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[0, 4],
|
||||
[1, 3],
|
||||
[1, 5],
|
||||
[2, 3],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
[4, 5],
|
||||
[4, 6],
|
||||
[5, 7],
|
||||
[6, 7]
|
||||
];
|
||||
const edgePoints = [];
|
||||
for (const edge of edges) {
|
||||
const [startVertex, endVertex] = [this.vertices[edge[0]], this.vertices[edge[1]]];
|
||||
const resolution = Math.min(Math.floor(endVertex.subtract(startVertex).length), 16);
|
||||
for (let i = 1; i < resolution; i++) {
|
||||
const t = i / resolution;
|
||||
edgePoints.push(startVertex.lerp(endVertex, t));
|
||||
}
|
||||
}
|
||||
return edgePoints.map((v) => [this.drawParticle, v]);
|
||||
}
|
||||
|
||||
addStandaloneParticles(locations) {
|
||||
for (const location of locations)
|
||||
this.vertices.push(new Vector(location.x, location.y, location.z));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { structureCollection } from "./StructureCollection";
|
||||
import { world } from "@minecraft/server";
|
||||
|
||||
export class Raycaster {
|
||||
static STEP_SIZE = 0.2;
|
||||
|
||||
static getStructureBlocks(dimension, startLocation, direction, { maxDistance = 7, getFirst = true, collideWithWorldBlocks = true, 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 structure = structureCollection.getStructure(dimension.id, location, { useLayers });
|
||||
if (structure) {
|
||||
const block = structure.getBlock(structure.toStructureCoords(location));
|
||||
if (block?.type.id !== 'minecraft:air') {
|
||||
blocks.push({
|
||||
permutation: block,
|
||||
location: location
|
||||
});
|
||||
if (getFirst)
|
||||
break;
|
||||
}
|
||||
try {
|
||||
if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.name === 'LocationOutOfWorldBoundariesError')
|
||||
break;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
location = {
|
||||
x: location.x + (direction.x*this.STEP_SIZE),
|
||||
y: location.y + (direction.y*this.STEP_SIZE),
|
||||
z: location.z + (direction.z*this.STEP_SIZE)
|
||||
};
|
||||
distance += this.STEP_SIZE;
|
||||
}
|
||||
// world.getDimension('minecraft:overworld').spawnParticle('minecraft:villager_happy', location);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true, 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, useLayers });
|
||||
if (blocks.length === 0)
|
||||
return void 0;
|
||||
return isFirst ? blocks[0] : blocks[blocks.length - 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { StructureInstance } from './StructureInstance';
|
||||
import { world } from '@minecraft/server';
|
||||
|
||||
class StructureCollection {
|
||||
structures;
|
||||
|
||||
constructor() {
|
||||
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.`);
|
||||
const structure = new StructureInstance(instanceName, structureId);
|
||||
this.structures[instanceName] = structure;
|
||||
return structure;
|
||||
}
|
||||
|
||||
get(instanceName) {
|
||||
const structure = this.structures[instanceName];
|
||||
if (!structure) {
|
||||
throw new Error(`Instance ${instanceName} not found.`);
|
||||
}
|
||||
return structure;
|
||||
}
|
||||
|
||||
delete(instanceName) {
|
||||
const struct = this.get(instanceName);
|
||||
struct.delete();
|
||||
delete this.structures[instanceName];
|
||||
}
|
||||
|
||||
getInstanceNames() {
|
||||
return Object.keys(this.structures);
|
||||
}
|
||||
|
||||
getStructures(dimensionId, location, options = {}) {
|
||||
return Object.values(this.structures).filter(structure => {
|
||||
try {
|
||||
return structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options)
|
||||
} catch (e) {
|
||||
if (e.name === 'InvalidStructureError') {
|
||||
structureCollection.delete(structure.name);
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStructure(dimensionId, location, options = {}) {
|
||||
return this.getStructures(dimensionId, location, options)[0];
|
||||
}
|
||||
|
||||
fetchStructureBlock(dimensionId, location) {
|
||||
const structure = this.getStructure(dimensionId, location);
|
||||
if (!structure)
|
||||
return void 0;
|
||||
return structure.getBlock(structure.toStructureCoords(location));
|
||||
}
|
||||
|
||||
getWorldStructureIds() {
|
||||
return world.structureManager.getWorldStructureIds()
|
||||
.filter(id => id.startsWith('mystructure:'))
|
||||
.map(id => id.replace('mystructure:', ''));
|
||||
}
|
||||
|
||||
rename(instanceName, newName) {
|
||||
const structure = this.get(instanceName);
|
||||
if (this.structures[newName])
|
||||
throw new Error(`Instance '${newName}' already exists.`);
|
||||
structure.rename(newName);
|
||||
this.structures[newName] = structure;
|
||||
delete this.structures[instanceName];
|
||||
structure.name = newName;
|
||||
}
|
||||
}
|
||||
|
||||
export const structureCollection = new StructureCollection();
|
||||
|
||||
world.afterEvents.worldLoad.subscribe(() => {
|
||||
structureCollection.loadExistingStructures();
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
import { world } from "@minecraft/server";
|
||||
import { Outliner } from "./Outliner";
|
||||
import { StructureOutliner } from "./StructureOutliner";
|
||||
|
||||
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
|
||||
};
|
||||
outliner = void 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;
|
||||
this.updateOptions();
|
||||
this.outliner = new StructureOutliner(this);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
getDimension() {
|
||||
let dimension;
|
||||
try {
|
||||
dimension = world.getDimension(this.#options.dimensionId);
|
||||
} catch (e) {
|
||||
dimension = world.getDimension("minecraft:overworld");
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
getBlock(structureLocation) {
|
||||
return this.#structure.getBlockPermutation(structureLocation);
|
||||
}
|
||||
|
||||
*getBlocks() {
|
||||
const max = this.#structure.size;
|
||||
for (let y = 0; y < max.y; y++) {
|
||||
yield * this.getLayerBlocks(y);
|
||||
}
|
||||
}
|
||||
|
||||
*getLayerBlocks(y) {
|
||||
const max = this.#structure.size;
|
||||
for (let x = 0; x < max.x; x++) {
|
||||
for (let z = 0; z < max.z; z++) {
|
||||
const blockPermutation = this.#structure.getBlockPermutation({ x, y, z });
|
||||
blockPermutation.location = { x, y, z };
|
||||
yield blockPermutation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
};
|
||||
}
|
||||
|
||||
getTotalVolume() {
|
||||
return this.#structure.size.x * this.#structure.size.y * this.#structure.size.z;
|
||||
}
|
||||
|
||||
rename(newName) {
|
||||
world.setDynamicProperty(`structOptions:${this.name}`, void 0);
|
||||
this.name = newName;
|
||||
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.refreshOutliner();
|
||||
}
|
||||
|
||||
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() {
|
||||
this.outliner.refresh();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
isUsingLayers() {
|
||||
return this.hasLayers() && this.#options.currentLayer !== 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);
|
||||
}
|
||||
|
||||
getDimension() {
|
||||
return world.getDimension(this.#options.dimensionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Outliner } from '../classes/Outliner';
|
||||
|
||||
export class StructureOutliner {
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
this.pullInstanceData();
|
||||
this.outliner = new Outliner(this.dimension, this.bounds.min, this.bounds.max);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
pullInstanceData() {
|
||||
try {
|
||||
this.dimension = this.instance.getDimension();
|
||||
this.bounds = this.instance.getBounds();
|
||||
this.bounds.min = this.instance.toGlobalCoords(this.bounds.min);
|
||||
this.bounds.max = this.instance.toGlobalCoords(this.bounds.max);
|
||||
} catch (e) {
|
||||
if (e.name === 'InvalidStructureError')
|
||||
this.outliner.stopDraw();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.pullInstanceData();
|
||||
this.refreshDraw();
|
||||
}
|
||||
|
||||
refreshDraw() {
|
||||
this.outliner.stopDraw();
|
||||
if (!this.instance.isEnabled())
|
||||
return;
|
||||
if (this.instance.isUsingLayers())
|
||||
this.layeredDraw();
|
||||
else
|
||||
this.boxDraw();
|
||||
this.outliner.startDraw();
|
||||
}
|
||||
|
||||
boxDraw() {
|
||||
this.outliner.setVertices(this.dimension, this.bounds.min, this.bounds.max);
|
||||
}
|
||||
|
||||
layeredDraw() {
|
||||
const { min, max } = this.instance.getLayeredBounds();
|
||||
this.outliner.setVertices(this.dimension, this.instance.toGlobalCoords(min), this.instance.toGlobalCoords(max));
|
||||
this.outliner.addStandaloneParticles(this.getCornerVertices());
|
||||
}
|
||||
|
||||
getCornerVertices() {
|
||||
return this.outliner.getVertices(this.bounds.min, this.bounds.max);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { BlockVerifier } from "./BlockVerifier";
|
||||
import { BlockVerificationLevel } from "./BlockVerificationLevel";
|
||||
|
||||
export class StructureVerifier {
|
||||
constructor(instance) {
|
||||
this.instance = instance;
|
||||
this.blockVerificationLevels = {};
|
||||
this.statistics = {};
|
||||
this.initStatistics();
|
||||
}
|
||||
|
||||
async verifyStructure() {
|
||||
await this.runVerifier();
|
||||
this.parseStatistics();
|
||||
return this.statistics;
|
||||
}
|
||||
|
||||
async runVerifier() {
|
||||
return new Promise((resolve) => {
|
||||
this.verifyBlocks();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
verifyBlocks() {
|
||||
for (const block of this.instance.getBlocks()) {
|
||||
const verificationLevel = this.verifyBlock(block.location);
|
||||
if (verificationLevel !== BlockVerificationLevel.isAir)
|
||||
this.blockVerificationLevels[JSON.stringify(block.location)] = verificationLevel;
|
||||
else
|
||||
this.statistics.correctlyAir++;
|
||||
}
|
||||
}
|
||||
|
||||
verifyBlock(location) {
|
||||
const worldBlock = this.instance.getDimension().getBlock(this.instance.toGlobalCoords(location));
|
||||
if (!worldBlock)
|
||||
throw new Error(`Block at ${JSON.stringify(location)} could not be accessed.`);
|
||||
const blockVerifier = new BlockVerifier(worldBlock, this.instance);
|
||||
return blockVerifier.verify();
|
||||
}
|
||||
|
||||
initStatistics() {
|
||||
for (const verificationlevel of Object.values(BlockVerificationLevel)) {
|
||||
this.statistics[verificationlevel] = 0;
|
||||
}
|
||||
this.statistics.percentages = {};
|
||||
this.statistics.correctlyAir = 0;
|
||||
}
|
||||
|
||||
parseStatistics() {
|
||||
for (const blockVerificationLevel of Object.values(BlockVerificationLevel)) {
|
||||
this.parseStatistic(blockVerificationLevel);
|
||||
}
|
||||
}
|
||||
|
||||
parseStatistic(blockVerificationLevel) {
|
||||
for (const verificationlevel of Object.values(this.blockVerificationLevels)) {
|
||||
if (blockVerificationLevel === verificationlevel) {
|
||||
this.statistics[verificationlevel]++;
|
||||
}
|
||||
}
|
||||
this.statistics.percentages[blockVerificationLevel] = this.statistics[blockVerificationLevel] / (this.instance.getTotalVolume() - this.statistics.correctlyAir) * 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Command } from '../lib/canopy/CanopyExtension';
|
||||
import { extension } from '../config';
|
||||
import { structureCollection } from '../classes/StructureCollection';
|
||||
import { MaterialCounter } from '../classes/MaterialCounter';
|
||||
|
||||
const structCmd = new Command({
|
||||
name: 'struct',
|
||||
description: { text: 'Manages current StrucTool structures.' },
|
||||
usage: 'struct <name> <add/remove/place/layer/info> [args...]',
|
||||
callback: structCommand,
|
||||
args: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'string', name: 'option' },
|
||||
{ type: 'string|number', name: 'arg3' }
|
||||
]
|
||||
});
|
||||
extension.addCommand(structCmd);
|
||||
|
||||
function structCommand(sender, args) {
|
||||
const { name, option, arg3 } = args;
|
||||
switch (option) {
|
||||
case 'add':
|
||||
addStructure(sender, name);
|
||||
break;
|
||||
case 'remove':
|
||||
removeStructure(sender, name);
|
||||
break;
|
||||
case 'place':
|
||||
placeStructure(sender, name);
|
||||
break;
|
||||
case 'layer':
|
||||
setLayer(sender, name, arg3);
|
||||
break;
|
||||
case 'info':
|
||||
printInfo(sender, name);
|
||||
break;
|
||||
default:
|
||||
structCmd.sendUsage(sender);
|
||||
}
|
||||
}
|
||||
|
||||
function addStructure(sender, name) {
|
||||
try {
|
||||
structureCollection.add(name, name);
|
||||
} catch (e) {
|
||||
if (e.message.includes('already exists')) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
|
||||
return;
|
||||
} else {
|
||||
sender.sendMessage({ text: `§cFailed to add structure '${name}'.` });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
sender.sendMessage({ text: `§7Added structure '${name}'` });
|
||||
}
|
||||
|
||||
function removeStructure(sender, name) {
|
||||
try {
|
||||
structureCollection.remove(name);
|
||||
} catch (e) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' not found.` });
|
||||
return;
|
||||
}
|
||||
sender.sendMessage({ text: `Removed structure '${name}'` });
|
||||
}
|
||||
|
||||
function placeStructure(sender, name) {
|
||||
let structure;
|
||||
try {
|
||||
structure = structureCollection.get(name);
|
||||
} catch (e) {
|
||||
try {
|
||||
structure = structureCollection.add(name, name);
|
||||
} catch (e) {
|
||||
if (e.message.includes('already exists')) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' already exists.` });
|
||||
return;
|
||||
} else if (e.message.includes('not found')) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' not found.` });
|
||||
return;
|
||||
} else {
|
||||
sender.sendMessage({ text: `§cFailed to place structure '${name}'.` });
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
structure.place(sender.dimension.id, sender.location);
|
||||
sender.sendMessage({ text: `§7Placed structure '${name}'.` });
|
||||
}
|
||||
|
||||
function setLayer(sender, name, layer) {
|
||||
let structure;
|
||||
try {
|
||||
structure = structureCollection.get(name);
|
||||
} catch (e) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' not found.` });
|
||||
return;
|
||||
}
|
||||
structure.setLayer(layer);
|
||||
sender.sendMessage({ text: `§7Set layer of structure '${name}' to ${layer}.` });
|
||||
}
|
||||
|
||||
function printInfo(sender, name) {
|
||||
let structure;
|
||||
try {
|
||||
structure = structureCollection.get(name);
|
||||
} catch (e) {
|
||||
sender.sendMessage({ text: `§cStructure '${name}' not found.` });
|
||||
return;
|
||||
}
|
||||
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)}` });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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 structoolCmd = new Command({
|
||||
name: 'structool',
|
||||
description: { text: 'Opens the StrucTool Menu. Using a paper will also open the menu.' },
|
||||
usage: 'structool',
|
||||
callback: (sender) => new MenuForm(sender)
|
||||
});
|
||||
extension.addCommand(structoolCmd);
|
||||
|
||||
world.beforeEvents.itemUse.subscribe((event) => {
|
||||
if (!event.source || event.itemStack?.typeId !== ACTION_ITEM) return;
|
||||
event.cancel = true;
|
||||
system.run(() => structoolCmd.getCallback()(event.source));
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { CanopyExtension } from './lib/canopy/CanopyExtension';
|
||||
|
||||
export const extension = new CanopyExtension({
|
||||
author: 'ForestOfLight',
|
||||
name: 'StrucTool',
|
||||
description: 'Survival building extension for §l§aCanopy§r!',
|
||||
version: '1.0.0'
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
export const bannedBlocks = [
|
||||
'air', 'bed', 'piston_arm_collision', 'sticky_piston_arm_collision', "skeleton_skull", 'standing_banner', 'wall_banner',
|
||||
'wooden_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', 'mangrove_door', 'cherry_door', 'pale_oak_door',
|
||||
'bamboo_door', 'iron_door', 'crimson_door', 'warped_door', 'copper_door', 'exposed_copper_door', 'weathered_copper_door', 'oxidized_copper_door',
|
||||
'waxed_copper_door', 'waxed_exposed_copper_door', 'waxed_weathered_copper_door', 'waxed_oxidized_copper_door',
|
||||
'seagrass', 'kelp'
|
||||
];
|
||||
|
||||
export const bannedDimensionBlocks = {
|
||||
'overworld': [],
|
||||
'nether': ['water'],
|
||||
'end': []
|
||||
};
|
||||
|
||||
export const whitelistedBlockStates = {
|
||||
'water': { liquid_depth: 0 },
|
||||
'lava': { liquid_depth: 0 }
|
||||
};
|
||||
|
||||
export const bannedToValidBlockMap = {
|
||||
'lit_furnace': 'furnace',
|
||||
'lit_smoker': 'smoker',
|
||||
'lit_blast_furnace': 'blast_furnace',
|
||||
'lit_redstone_ore': 'redstone_ore',
|
||||
'lit_redstone_lamp': 'redstone_lamp',
|
||||
'unlit_redstone_torch': 'redstone_torch'
|
||||
};
|
||||
|
||||
export const resetToBlockStates = {
|
||||
growth: 0,
|
||||
age: 0,
|
||||
height: 0,
|
||||
bite_counter: 0,
|
||||
fill_level: 0,
|
||||
redstone_signal: 0,
|
||||
cluster_count: 0,
|
||||
respawn_anchor_charge: 0,
|
||||
turtle_egg_count: 0,
|
||||
cluster_count: 0
|
||||
};
|
||||
|
||||
export const blockIdToItemStackMap = {
|
||||
'water': 'water_bucket',
|
||||
'lava': 'lava_bucket',
|
||||
'fire': 'fire_charge',
|
||||
'soul_fire': 'fire_charge',
|
||||
};
|
||||
|
||||
export const specialItemPlacementConversions = {
|
||||
'water_bucket': 'bucket',
|
||||
'lava_bucket': 'bucket'
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Part of ItemStack Database by @gameza_src
|
||||
* Unknown author
|
||||
*/
|
||||
const isVec3Symbol = Symbol("isVec3");
|
||||
export function Vector(x = 0, y = 0, z = 0) {
|
||||
if (new.target) {
|
||||
this.x = Number(x);
|
||||
this.y = Number(y);
|
||||
this.z = Number(z);
|
||||
} else {return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype };}
|
||||
}
|
||||
Vector.magnitude = function magnitude(vec) { return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); }
|
||||
Vector.normalize = function normalize(vec) { const l = Vector.magnitude(vec); return { x: vec.x / l, y: vec.y / l, z: vec.z / l, __proto__: Vector.prototype }; }
|
||||
Vector.cross = function crossProduct(a, b) { return { x: a.y * b.z - a.z * b.y, y: a.x * b.z - a.z * b.x, z: a.x * b.y - a.y * b.x, __proto__: Vector.prototype }; }
|
||||
Vector.dot = function dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
Vector.angleBetween = function angleBetween(a, b) { return Math.acos(Vector.dot(a, b) / (Vector.magnitude(a) * Vector.magnitude(b))); }
|
||||
Vector.subtract = function subtract(a, b) { return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z, __proto__: Vector.prototype } };
|
||||
Vector.add = function add(a, b) { return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z, __proto__: Vector.prototype } };
|
||||
Vector.multiply = function multiply(vec, num) {
|
||||
if (typeof num == "number") return { x: vec.x * num, y: vec.y * num, z: vec.z * num, __proto__: Vector.prototype };
|
||||
return { x: vec.x * num.x, y: vec.y * num.y, z: vec.z * num.z, __proto__: Vector.prototype };
|
||||
}
|
||||
Vector.isVec3 = function isVec3(vec) { return vec[isVec3Symbol] === true; }
|
||||
Vector.floor = function floor(vec) { return { x: Math.floor(vec.x), y: Math.floor(vec.y), z: Math.floor(vec.z), __proto__: Vector.prototype }; }
|
||||
Vector.projection = function projection(a, b) { return Vector.multiply(b, Vector.dot(a, b) / ((b.x * b.x + b.y * b.y + b.z * b.z) ** 2)); }
|
||||
Vector.rejection = function rejection(a, b) { return Vector.subtract(a, Vector.projection(a, b)); }
|
||||
Vector.reflect = function reflect(v, n) { return Vector.subtract(v, Vector.multiply(n, 2 * Vector.dot(v, n))); }
|
||||
Vector.lerp = function lerp(a, b, t) { return Vector.multiply(a, 1 - t).add(Vector.multiply(b, t)); }
|
||||
Vector.distance = function distance(a, b) { return Vector.magnitude(Vector.subtract(a, b)); }
|
||||
Vector.from = function from(object) {
|
||||
if (Vector.isVec3(object)) return object;
|
||||
if (Array.isArray(object)) return new Vector(object[0], object[1], object[2]);
|
||||
const { x = 0, y = 0, z = 0 } = object ?? {};
|
||||
return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype };
|
||||
}
|
||||
Vector.sort = function sort(vec1, vec2) {
|
||||
const [x1, x2] = vec1.x < vec2.x ? [vec1.x, vec2.x] : [vec2.x, vec1.x];
|
||||
const [y1, y2] = vec1.y < vec2.y ? [vec1.y, vec2.y] : [vec2.y, vec1.y];
|
||||
const [z1, z2] = vec1.z < vec2.z ? [vec1.z, vec2.z] : [vec2.z, vec1.z];
|
||||
return [{ x: x1, y: y1, z: z1, __proto__: Vector.prototype }, { x: x2, y: y2, z: z2, __proto__: Vector.prototype }];
|
||||
}
|
||||
Vector.up = { x: 0, y: 1, z: 0, __proto__: Vector.prototype };
|
||||
Vector.down = { x: 0, y: -1, z: 0, __proto__: Vector.prototype };
|
||||
Vector.right = { x: 1, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.left = { x: -1, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.forward = { x: 0, y: 0, z: 1, __proto__: Vector.prototype };
|
||||
Vector.backward = { x: 0, y: 0, z: -1, __proto__: Vector.prototype };
|
||||
Vector.zero = { x: 0, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.prototype = {
|
||||
distance(vec) { return Vector.distance(this, vec); },
|
||||
lerp(vec, t) { return Vector.lerp(this, vec, t); },
|
||||
projection(vec) { return Vector.projection(this, vec); },
|
||||
reflect(vec) { return Vector.reflect(this, vec); },
|
||||
rejection(vec) { return Vector.rejection(this, vec); },
|
||||
cross(vec) { return Vector.cross(this, vec); },
|
||||
dot(vec) { return Vector.dot(this, vec); },
|
||||
floor() { return Vector.floor(this); },
|
||||
add(vec) { return Vector.add(this, vec); },
|
||||
subtract(vec) { return Vector.subtract(this, vec); },
|
||||
multiply(num) { return Vector.multiply(this, num); },
|
||||
get length() { return Vector.magnitude(this); },
|
||||
get normalized() { return Vector.normalize(this); },
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
[isVec3Symbol]: true,
|
||||
toString() { return `<${this.x}, ${this.y}, ${this.z}>`; }
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @license
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 ForestOfLight
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { world } from '@minecraft/server';
|
||||
import IPC from '../../lib/ipc/ipc';
|
||||
import Command from './Command';
|
||||
import Rule from './Rule';
|
||||
import { CommandCallbackRequest, CommandPrefixRequest, Ready, RegisterCommand, RegisterExtension, RegisterRule, RuleValueRequest, RuleValueSet, CommandPrefixResponse, RuleValueResponse } from './extension.ipc';
|
||||
|
||||
class CanopyExtension {
|
||||
name;
|
||||
version;
|
||||
author;
|
||||
description;
|
||||
#commands = {};
|
||||
#rules = {};
|
||||
#isRegistrationReady = false;
|
||||
|
||||
constructor({ name = 'Unnamed', version = '1.0.0', author = 'Unknown', description = { text: '' } }) {
|
||||
this.id = this.#makeID(name);
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.author = author;
|
||||
this.description = description;
|
||||
|
||||
this.#registerExtension();
|
||||
this.#setupCommandPrefix();
|
||||
this.#handleCommandCallbacks();
|
||||
this.#handleRuleValueRequests();
|
||||
this.#handleRuleValueSetters();
|
||||
}
|
||||
|
||||
addCommand(command) {
|
||||
if (!(command instanceof Command))
|
||||
throw new Error('Command must be an instance of Command.');
|
||||
this.#commands[command.getName()] = command;
|
||||
if (this.#isRegistrationReady)
|
||||
this.#registerCommand(command);
|
||||
}
|
||||
|
||||
addRule(rule) {
|
||||
if (!(rule instanceof Rule))
|
||||
throw new Error('Rule must be an instance of Rule.');
|
||||
this.#rules[rule.getID()] = rule;
|
||||
if (this.#isRegistrationReady)
|
||||
this.#registerRule(rule);
|
||||
}
|
||||
|
||||
getRuleValue(ruleID) {
|
||||
return this.#rules[ruleID].getValue();
|
||||
}
|
||||
|
||||
#makeID(name) {
|
||||
if (typeof name !== 'string')
|
||||
throw new Error(`[${name}] Could not register extension. Extension name must be a string.`);
|
||||
const id = name.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/ /g, '_');
|
||||
if (id.length === 0)
|
||||
throw new Error(`[${name}] Could not register extension. Extension name must contain at least one alphanumeric character.`);
|
||||
return id;
|
||||
}
|
||||
|
||||
#registerExtension() {
|
||||
IPC.once('canopyExtension:ready', Ready, () => {
|
||||
IPC.send('canopyExtension:registerExtension', RegisterExtension, {
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
author: this.author,
|
||||
description: this.description
|
||||
});
|
||||
});
|
||||
IPC.once(`canopyExtension:${this.id}:ready`, Ready, () => {
|
||||
this.#isRegistrationReady = true;
|
||||
for (const rule of Object.values(this.#rules))
|
||||
this.#registerRule(rule);
|
||||
for (const command of Object.values(this.#commands))
|
||||
this.#registerCommand(command);
|
||||
});
|
||||
}
|
||||
|
||||
#registerCommand(command) {
|
||||
IPC.send(`canopyExtension:${this.id}:registerCommand`, RegisterCommand, {
|
||||
name: command.getName(),
|
||||
description: command.getDescription(),
|
||||
usage: command.getUsage(),
|
||||
callback: false,
|
||||
args: command.getArgs(),
|
||||
contingentRules: command.getContingentRules(),
|
||||
adminOnly: command.isAdminOnly(),
|
||||
helpEntries: command.getHelpEntries(),
|
||||
helpHidden: command.isHelpHidden(),
|
||||
extensionName: this.name
|
||||
});
|
||||
}
|
||||
|
||||
#handleCommandCallbacks() {
|
||||
IPC.on(`canopyExtension:${this.id}:commandCallbackRequest`, CommandCallbackRequest, (cmdData) => {
|
||||
if (cmdData.senderName === undefined)
|
||||
return;
|
||||
const sender = world.getPlayers({ name: cmdData.senderName })[0];
|
||||
if (!sender)
|
||||
throw new Error(`Sender ${cmdData.senderName} of ${cmdData.commandName} not found.`);
|
||||
const parsedArgs = JSON.parse(cmdData.args);
|
||||
this.#commands[cmdData.commandName].runCallback(sender, parsedArgs);
|
||||
});
|
||||
}
|
||||
|
||||
#registerRule(rule) {
|
||||
IPC.send(`canopyExtension:${this.id}:registerRule`, RegisterRule, {
|
||||
identifier: rule.getID(),
|
||||
description: rule.getDescription(),
|
||||
contingentRules: rule.getContigentRules(),
|
||||
independentRules: rule.getIndependentRules(),
|
||||
extensionName: this.name
|
||||
});
|
||||
if (rule.getValue() === true)
|
||||
rule.onEnable();
|
||||
}
|
||||
|
||||
#handleRuleValueRequests() {
|
||||
IPC.handle(`canopyExtension:${this.id}:ruleValueRequest`, RuleValueRequest, RuleValueResponse, (data) => {
|
||||
const rule = this.#rules[data.ruleID];
|
||||
if (!rule)
|
||||
throw new Error(`Rule ${data.ruleID} not found.`);
|
||||
const value = rule.getValue();
|
||||
return { value };
|
||||
});
|
||||
}
|
||||
|
||||
#handleRuleValueSetters() {
|
||||
IPC.on(`canopyExtension:${this.id}:ruleValueSet`, RuleValueSet, (data) => {
|
||||
const rule = this.#rules[data.ruleID];
|
||||
if (!rule)
|
||||
throw new Error(`Rule ${data.ruleID} not found.`);
|
||||
rule.setValue(data.value);
|
||||
});
|
||||
}
|
||||
|
||||
#setupCommandPrefix() {
|
||||
const prefix = IPC.invoke(`canopyExtension:commandPrefixRequest`, CommandPrefixRequest, void 0, CommandPrefixResponse).then(result => {
|
||||
Command.setPrefix(result.prefix);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { CanopyExtension, Command, Rule };
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 ForestOfLight
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
class Command {
|
||||
#name;
|
||||
#description;
|
||||
#usage;
|
||||
#callback;
|
||||
#args;
|
||||
#contingentRules;
|
||||
#adminOnly;
|
||||
#helpEntries;
|
||||
#helpHidden;
|
||||
static #prefix = '';
|
||||
|
||||
constructor({ name, description = '', usage, callback, args = [], contingentRules = [], adminOnly = false, helpEntries = [], helpHidden = false }) {
|
||||
this.#name = name;
|
||||
this.#description = description;
|
||||
this.#usage = usage;
|
||||
this.#callback = callback;
|
||||
this.#args = args;
|
||||
this.#contingentRules = contingentRules;
|
||||
this.#adminOnly = adminOnly;
|
||||
this.#helpEntries = helpEntries;
|
||||
this.#helpHidden = helpHidden;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this.#description;
|
||||
}
|
||||
|
||||
getUsage() {
|
||||
return this.#usage;
|
||||
}
|
||||
|
||||
getCallback() {
|
||||
return this.#callback;
|
||||
}
|
||||
|
||||
getArgs() {
|
||||
return this.#args;
|
||||
}
|
||||
|
||||
getContingentRules() {
|
||||
return this.#contingentRules;
|
||||
}
|
||||
|
||||
isAdminOnly() {
|
||||
return this.#adminOnly;
|
||||
}
|
||||
|
||||
getHelpEntries() {
|
||||
return this.#helpEntries;
|
||||
}
|
||||
|
||||
isHelpHidden() {
|
||||
return this.#helpHidden;
|
||||
}
|
||||
|
||||
runCallback(sender, args) {
|
||||
this.#callback(sender, args);
|
||||
}
|
||||
|
||||
sendUsage(sender) {
|
||||
sender.sendMessage(`§cUsage: ${Command.#prefix}${this.#usage}`);
|
||||
}
|
||||
|
||||
static setPrefix(prefix) {
|
||||
Command.#prefix = prefix;
|
||||
}
|
||||
|
||||
static getPrefix() {
|
||||
return Command.#prefix;
|
||||
}
|
||||
}
|
||||
|
||||
export default Command;
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2024 ForestOfLight
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { world } from '@minecraft/server';
|
||||
|
||||
class Rule {
|
||||
#identifier;
|
||||
#description;
|
||||
#contingentRules;
|
||||
#independentRules;
|
||||
|
||||
constructor({ identifier, description, contingentRules = [], independentRules = [], onEnableCallback = () => {}, onDisableCallback = () => {} }) {
|
||||
this.#identifier = identifier;
|
||||
this.#description = description;
|
||||
this.#contingentRules = contingentRules;
|
||||
this.#independentRules = independentRules;
|
||||
this.onEnable = onEnableCallback;
|
||||
this.onDisable = onDisableCallback;
|
||||
}
|
||||
|
||||
getID() {
|
||||
return this.#identifier;
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this.#description;
|
||||
}
|
||||
|
||||
getContigentRules() {
|
||||
return this.#contingentRules;
|
||||
}
|
||||
|
||||
getIndependentRules() {
|
||||
return this.#independentRules;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const value = world.getDynamicProperty(this.#identifier);
|
||||
if (String(value) === 'true')
|
||||
return true;
|
||||
if (['false', 'undefined'].includes(String(value)))
|
||||
return false;
|
||||
throw new Error(`Rule ${this.#identifier} has an invalid value: ${value} (${typeof value})`);
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
if (value === true)
|
||||
this.onEnable();
|
||||
else
|
||||
this.onDisable();
|
||||
world.setDynamicProperty(this.#identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
export default Rule;
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable new-cap */
|
||||
import { PROTO } from '../ipc/ipc'
|
||||
|
||||
const description = PROTO.Object({
|
||||
text: PROTO.Optional(PROTO.String),
|
||||
translate: PROTO.Optional(PROTO.String),
|
||||
with: PROTO.Optional(PROTO.Array(PROTO.String))
|
||||
});
|
||||
|
||||
export const Ready = PROTO.Void;
|
||||
|
||||
export const RegisterExtension = PROTO.Object({
|
||||
name: PROTO.String,
|
||||
version: PROTO.String,
|
||||
author: PROTO.String,
|
||||
description: description,
|
||||
isEndstone: PROTO.Boolean
|
||||
});
|
||||
|
||||
export const RegisterCommand = PROTO.Object({
|
||||
name: PROTO.String,
|
||||
description: description,
|
||||
usage: PROTO.String,
|
||||
callback: PROTO.Optional(PROTO.Undefined),
|
||||
args: PROTO.Optional(PROTO.Array(PROTO.Object({
|
||||
type: PROTO.String,
|
||||
name: PROTO.String
|
||||
}))),
|
||||
contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
|
||||
adminOnly: PROTO.Optional(PROTO.Boolean),
|
||||
helpEntries: PROTO.Optional(PROTO.Array(PROTO.Object({
|
||||
usage: PROTO.String,
|
||||
description: description
|
||||
}))),
|
||||
helpHidden: PROTO.Optional(PROTO.Boolean),
|
||||
extensionName: PROTO.Optional(PROTO.String)
|
||||
});
|
||||
|
||||
export const RegisterRule = PROTO.Object({
|
||||
identifier: PROTO.String,
|
||||
description: description,
|
||||
contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
|
||||
independentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
|
||||
extensionName: PROTO.Optional(PROTO.String)
|
||||
});
|
||||
|
||||
export const RuleValueRequest = PROTO.Object({
|
||||
ruleID: PROTO.String
|
||||
});
|
||||
|
||||
export const RuleValueResponse = PROTO.Object({
|
||||
value: PROTO.Boolean
|
||||
});
|
||||
|
||||
export const RuleValueSet = PROTO.Object({
|
||||
ruleID: PROTO.String,
|
||||
value: PROTO.Boolean
|
||||
});
|
||||
|
||||
export const CommandCallbackRequest = PROTO.Object({
|
||||
commandName: PROTO.String,
|
||||
senderName: PROTO.Optional(PROTO.String),
|
||||
args: PROTO.String
|
||||
});
|
||||
|
||||
export const CommandPrefixRequest = PROTO.Void;
|
||||
|
||||
export const CommandPrefixResponse = PROTO.Object({
|
||||
prefix: PROTO.String
|
||||
});
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 OmniacDev
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export declare namespace PROTO {
|
||||
interface Serializable<T> {
|
||||
serialize(value: T, stream: ByteQueue): Generator<void, void, void>;
|
||||
deserialize(stream: ByteQueue): Generator<void, T, void>;
|
||||
}
|
||||
class ByteQueue {
|
||||
private _buffer;
|
||||
private _data_view;
|
||||
private _length;
|
||||
private _offset;
|
||||
get end(): number;
|
||||
get front(): number;
|
||||
get data_view(): DataView;
|
||||
constructor(size?: number);
|
||||
write(...values: number[]): void;
|
||||
read(amount?: number): number[];
|
||||
ensure_capacity(size: number): void;
|
||||
static from_uint8array(array: Uint8Array): ByteQueue;
|
||||
to_uint8array(): Uint8Array;
|
||||
}
|
||||
namespace MIPS {
|
||||
function serialize(byte_queue: PROTO.ByteQueue): Generator<void, string, void>;
|
||||
function deserialize(str: string): Generator<void, PROTO.ByteQueue, void>;
|
||||
}
|
||||
const Void: PROTO.Serializable<void>;
|
||||
const Null: PROTO.Serializable<null>;
|
||||
const Undefined: PROTO.Serializable<undefined>;
|
||||
const Int8: PROTO.Serializable<number>;
|
||||
const Int16: PROTO.Serializable<number>;
|
||||
const Int32: PROTO.Serializable<number>;
|
||||
const UInt8: PROTO.Serializable<number>;
|
||||
const UInt16: PROTO.Serializable<number>;
|
||||
const UInt32: PROTO.Serializable<number>;
|
||||
const UVarInt32: PROTO.Serializable<number>;
|
||||
const Float32: PROTO.Serializable<number>;
|
||||
const Float64: PROTO.Serializable<number>;
|
||||
const String: PROTO.Serializable<string>;
|
||||
const Boolean: PROTO.Serializable<boolean>;
|
||||
const UInt8Array: PROTO.Serializable<Uint8Array>;
|
||||
const Date: PROTO.Serializable<Date>;
|
||||
function Object<T extends object>(obj: {
|
||||
[K in keyof T]: PROTO.Serializable<T[K]>;
|
||||
}): PROTO.Serializable<T>;
|
||||
function Array<T>(value: PROTO.Serializable<T>): PROTO.Serializable<T[]>;
|
||||
function Tuple<T extends any[]>(...values: {
|
||||
[K in keyof T]: PROTO.Serializable<T[K]>;
|
||||
}): PROTO.Serializable<T>;
|
||||
function Optional<T>(value: PROTO.Serializable<T>): PROTO.Serializable<T | undefined>;
|
||||
function Map<K, V>(key: PROTO.Serializable<K>, value: PROTO.Serializable<V>): PROTO.Serializable<Map<K, V>>;
|
||||
function Set<V>(value: PROTO.Serializable<V>): PROTO.Serializable<Set<V>>;
|
||||
type Endpoint = string;
|
||||
type Header = {
|
||||
guid: string;
|
||||
encoding: string;
|
||||
index: number;
|
||||
final: boolean;
|
||||
};
|
||||
const Endpoint: PROTO.Serializable<Endpoint>;
|
||||
const Header: PROTO.Serializable<Header>;
|
||||
}
|
||||
export declare namespace NET {
|
||||
function serialize(byte_queue: PROTO.ByteQueue, max_size?: number): Generator<void, string[], void>;
|
||||
function deserialize(strings: string[]): Generator<void, PROTO.ByteQueue, void>;
|
||||
function emit<S extends PROTO.Serializable<T>, T>(endpoint: string, serializer: S & PROTO.Serializable<T>, value: T): Generator<void, void, void>;
|
||||
function listen<T, S extends PROTO.Serializable<T>>(endpoint: string, serializer: S & PROTO.Serializable<T>, callback: (value: T) => Generator<void, void, void>): () => void;
|
||||
}
|
||||
export declare namespace IPC {
|
||||
/** Sends a message with `args` to `channel` */
|
||||
function send<S extends PROTO.Serializable<T>, T>(channel: string, serializer: S & PROTO.Serializable<T>, value: T): void;
|
||||
/** Sends an `invoke` message through IPC, and expects a result asynchronously. */
|
||||
function invoke<TS extends PROTO.Serializable<T>, T, RS extends PROTO.Serializable<R>, R>(channel: string, serializer: TS & PROTO.Serializable<T>, value: T, deserializer: RS & PROTO.Serializable<R>): Promise<R>;
|
||||
/** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */
|
||||
function on<S extends PROTO.Serializable<T>, T>(channel: string, deserializer: S & PROTO.Serializable<T>, listener: (value: T) => void): () => void;
|
||||
/** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */
|
||||
function once<S extends PROTO.Serializable<T>, T>(channel: string, deserializer: S & PROTO.Serializable<T>, listener: (value: T) => void): () => void;
|
||||
/** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */
|
||||
function handle<TS extends PROTO.Serializable<T>, T, RS extends PROTO.Serializable<R>, R>(channel: string, deserializer: TS & PROTO.Serializable<T>, serializer: RS & PROTO.Serializable<R>, listener: (value: T) => R): () => void;
|
||||
}
|
||||
export default IPC;
|
||||
@@ -0,0 +1,593 @@
|
||||
/**
|
||||
* @license
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 OmniacDev
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import { ScriptEventSource, system, world } from '@minecraft/server';
|
||||
export var PROTO;
|
||||
(function (PROTO) {
|
||||
class ByteQueue {
|
||||
get end() {
|
||||
return this._length + this._offset;
|
||||
}
|
||||
get front() {
|
||||
return this._offset;
|
||||
}
|
||||
get data_view() {
|
||||
return this._data_view;
|
||||
}
|
||||
constructor(size = 256) {
|
||||
this._buffer = new Uint8Array(size);
|
||||
this._data_view = new DataView(this._buffer.buffer);
|
||||
this._length = 0;
|
||||
this._offset = 0;
|
||||
}
|
||||
write(...values) {
|
||||
this.ensure_capacity(values.length);
|
||||
this._buffer.set(values, this.end);
|
||||
this._length += values.length;
|
||||
}
|
||||
read(amount = 1) {
|
||||
if (this._length > 0) {
|
||||
const max_amount = amount > this._length ? this._length : amount;
|
||||
const values = this._buffer.subarray(this._offset, this._offset + max_amount);
|
||||
this._length -= max_amount;
|
||||
this._offset += max_amount;
|
||||
return globalThis.Array.from(values);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
ensure_capacity(size) {
|
||||
if (this.end + size > this._buffer.length) {
|
||||
const larger_buffer = new Uint8Array((this.end + size) * 2);
|
||||
larger_buffer.set(this._buffer.subarray(this._offset, this.end), 0);
|
||||
this._buffer = larger_buffer;
|
||||
this._offset = 0;
|
||||
this._data_view = new DataView(this._buffer.buffer);
|
||||
}
|
||||
}
|
||||
static from_uint8array(array) {
|
||||
const byte_queue = new ByteQueue();
|
||||
byte_queue._buffer = array;
|
||||
byte_queue._length = array.length;
|
||||
byte_queue._offset = 0;
|
||||
byte_queue._data_view = new DataView(array.buffer);
|
||||
return byte_queue;
|
||||
}
|
||||
to_uint8array() {
|
||||
return this._buffer.subarray(this._offset, this.end);
|
||||
}
|
||||
}
|
||||
PROTO.ByteQueue = ByteQueue;
|
||||
let MIPS;
|
||||
(function (MIPS) {
|
||||
function* serialize(byte_queue) {
|
||||
const uint8array = byte_queue.to_uint8array();
|
||||
let str = '(0x';
|
||||
for (let i = 0; i < uint8array.length; i++) {
|
||||
const hex = uint8array[i].toString(16).padStart(2, '0').toUpperCase();
|
||||
str += hex;
|
||||
yield;
|
||||
}
|
||||
str += ')';
|
||||
return str;
|
||||
}
|
||||
MIPS.serialize = serialize;
|
||||
function* deserialize(str) {
|
||||
if (str.startsWith('(0x') && str.endsWith(')')) {
|
||||
const result = [];
|
||||
const hex_str = str.slice(3, str.length - 1);
|
||||
for (let i = 0; i < hex_str.length; i++) {
|
||||
const hex = hex_str[i] + hex_str[++i];
|
||||
result.push(parseInt(hex, 16));
|
||||
yield;
|
||||
}
|
||||
return ByteQueue.from_uint8array(new Uint8Array(result));
|
||||
}
|
||||
return new ByteQueue();
|
||||
}
|
||||
MIPS.deserialize = deserialize;
|
||||
})(MIPS = PROTO.MIPS || (PROTO.MIPS = {}));
|
||||
PROTO.Void = {
|
||||
*serialize() { },
|
||||
*deserialize() { }
|
||||
};
|
||||
PROTO.Null = {
|
||||
*serialize() { },
|
||||
*deserialize() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
PROTO.Undefined = {
|
||||
*serialize() { },
|
||||
*deserialize() {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
PROTO.Int8 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 1;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setInt8(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getInt8(stream.front);
|
||||
stream.read(1);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.Int16 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 2;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setInt16(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getInt16(stream.front);
|
||||
stream.read(2);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.Int32 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 4;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setInt32(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getInt32(stream.front);
|
||||
stream.read(4);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.UInt8 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 1;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setUint8(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getUint8(stream.front);
|
||||
stream.read(1);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.UInt16 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 2;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setUint16(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getUint16(stream.front);
|
||||
stream.read(2);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.UInt32 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 4;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setUint32(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getUint32(stream.front);
|
||||
stream.read(4);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.UVarInt32 = {
|
||||
*serialize(value, stream) {
|
||||
while (value >= 0x80) {
|
||||
stream.write((value & 0x7f) | 0x80);
|
||||
value >>= 7;
|
||||
yield;
|
||||
}
|
||||
stream.write(value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
let value = 0;
|
||||
let size = 0;
|
||||
let byte;
|
||||
do {
|
||||
byte = stream.read()[0];
|
||||
value |= (byte & 0x7f) << (size * 7);
|
||||
size += 1;
|
||||
yield;
|
||||
} while ((byte & 0x80) !== 0 && size < 10);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.Float32 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 4;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setFloat32(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getFloat32(stream.front);
|
||||
stream.read(4);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.Float64 = {
|
||||
*serialize(value, stream) {
|
||||
const length = 8;
|
||||
stream.write(...globalThis.Array(length).fill(0));
|
||||
stream.data_view.setFloat64(stream.end - length, value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.data_view.getFloat64(stream.front);
|
||||
stream.read(8);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.String = {
|
||||
*serialize(value, stream) {
|
||||
yield* PROTO.UVarInt32.serialize(value.length, stream);
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
yield* PROTO.UVarInt32.serialize(code, stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const length = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
let value = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const code = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
value += globalThis.String.fromCharCode(code);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
PROTO.Boolean = {
|
||||
*serialize(value, stream) {
|
||||
stream.write(value ? 1 : 0);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const value = stream.read()[0];
|
||||
return value === 1;
|
||||
}
|
||||
};
|
||||
PROTO.UInt8Array = {
|
||||
*serialize(value, stream) {
|
||||
yield* PROTO.UVarInt32.serialize(value.length, stream);
|
||||
stream.write(...value);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const length = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
return new Uint8Array(stream.read(length));
|
||||
}
|
||||
};
|
||||
PROTO.Date = {
|
||||
*serialize(value, stream) {
|
||||
yield* PROTO.Float64.serialize(value.getTime(), stream);
|
||||
},
|
||||
*deserialize(stream) {
|
||||
return new globalThis.Date(yield* PROTO.Float64.deserialize(stream));
|
||||
}
|
||||
};
|
||||
function Object(obj) {
|
||||
return {
|
||||
*serialize(value, stream) {
|
||||
for (const key in obj) {
|
||||
yield* obj[key].serialize(value[key], stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
result[key] = yield* obj[key].deserialize(stream);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Object = Object;
|
||||
function Array(value) {
|
||||
return {
|
||||
*serialize(array, stream) {
|
||||
yield* PROTO.UVarInt32.serialize(array.length, stream);
|
||||
for (const item of array) {
|
||||
yield* value.serialize(item, stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const result = [];
|
||||
const length = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result[i] = yield* value.deserialize(stream);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Array = Array;
|
||||
function Tuple(...values) {
|
||||
return {
|
||||
*serialize(tuple, stream) {
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
yield* values[i].serialize(tuple[i], stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const result = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
result[i] = yield* values[i].deserialize(stream);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Tuple = Tuple;
|
||||
function Optional(value) {
|
||||
return {
|
||||
*serialize(optional, stream) {
|
||||
yield* PROTO.Boolean.serialize(optional !== undefined, stream);
|
||||
if (optional !== undefined) {
|
||||
yield* value.serialize(optional, stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const defined = yield* PROTO.Boolean.deserialize(stream);
|
||||
if (defined) {
|
||||
return yield* value.deserialize(stream);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Optional = Optional;
|
||||
function Map(key, value) {
|
||||
return {
|
||||
*serialize(map, stream) {
|
||||
yield* PROTO.UVarInt32.serialize(map.size, stream);
|
||||
for (const [k, v] of map.entries()) {
|
||||
yield* key.serialize(k, stream);
|
||||
yield* value.serialize(v, stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const size = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
const result = new globalThis.Map();
|
||||
for (let i = 0; i < size; i++) {
|
||||
const k = yield* key.deserialize(stream);
|
||||
const v = yield* value.deserialize(stream);
|
||||
result.set(k, v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Map = Map;
|
||||
function Set(value) {
|
||||
return {
|
||||
*serialize(set, stream) {
|
||||
yield* PROTO.UVarInt32.serialize(set.size, stream);
|
||||
for (const [_, v] of set.entries()) {
|
||||
yield* value.serialize(v, stream);
|
||||
}
|
||||
},
|
||||
*deserialize(stream) {
|
||||
const size = yield* PROTO.UVarInt32.deserialize(stream);
|
||||
const result = new globalThis.Set();
|
||||
for (let i = 0; i < size; i++) {
|
||||
const v = yield* value.deserialize(stream);
|
||||
result.add(v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
PROTO.Set = Set;
|
||||
PROTO.Endpoint = PROTO.String;
|
||||
PROTO.Header = PROTO.Object({
|
||||
guid: PROTO.String,
|
||||
encoding: PROTO.String,
|
||||
index: PROTO.UVarInt32,
|
||||
final: PROTO.Boolean
|
||||
});
|
||||
})(PROTO || (PROTO = {}));
|
||||
export var NET;
|
||||
(function (NET) {
|
||||
const FRAG_MAX = 2048;
|
||||
const ENCODING = 'mcbe-ipc:v3';
|
||||
const ENDPOINTS = new Map();
|
||||
function* serialize(byte_queue, max_size = Infinity) {
|
||||
const uint8array = byte_queue.to_uint8array();
|
||||
const result = [];
|
||||
let acc_str = '';
|
||||
let acc_size = 0;
|
||||
for (let i = 0; i < uint8array.length; i++) {
|
||||
const char_code = uint8array[i] | (uint8array[++i] << 8);
|
||||
const utf16_size = char_code <= 0x7f ? 1 : char_code <= 0x7ff ? 2 : char_code <= 0xffff ? 3 : 4;
|
||||
const char_size = char_code > 0xff ? utf16_size : 2;
|
||||
if (acc_size + char_size > max_size) {
|
||||
result.push(acc_str);
|
||||
acc_str = '';
|
||||
acc_size = 0;
|
||||
}
|
||||
if (char_code > 0xff) {
|
||||
acc_str += String.fromCharCode(char_code);
|
||||
acc_size += utf16_size;
|
||||
}
|
||||
else {
|
||||
acc_str += char_code.toString(16).padStart(2, '0').toUpperCase();
|
||||
acc_size += 2;
|
||||
}
|
||||
yield;
|
||||
}
|
||||
result.push(acc_str);
|
||||
return result;
|
||||
}
|
||||
NET.serialize = serialize;
|
||||
function* deserialize(strings) {
|
||||
const result = [];
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
const str = strings[i];
|
||||
for (let j = 0; j < str.length; j++) {
|
||||
const char_code = str.charCodeAt(j);
|
||||
if (char_code <= 0xff) {
|
||||
const hex = str[j] + str[++j];
|
||||
const hex_code = parseInt(hex, 16);
|
||||
result.push(hex_code & 0xff);
|
||||
result.push(hex_code >> 8);
|
||||
}
|
||||
else {
|
||||
result.push(char_code & 0xff);
|
||||
result.push(char_code >> 8);
|
||||
}
|
||||
yield;
|
||||
}
|
||||
yield;
|
||||
}
|
||||
return PROTO.ByteQueue.from_uint8array(new Uint8Array(result));
|
||||
}
|
||||
NET.deserialize = deserialize;
|
||||
system.afterEvents.scriptEventReceive.subscribe(event => {
|
||||
system.runJob((function* () {
|
||||
const [serialized_endpoint, serialized_header] = event.id.split(':');
|
||||
const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint);
|
||||
const endpoint = yield* PROTO.Endpoint.deserialize(endpoint_stream);
|
||||
const listeners = ENDPOINTS.get(endpoint);
|
||||
if (event.sourceType === ScriptEventSource.Server && listeners) {
|
||||
const header_stream = yield* PROTO.MIPS.deserialize(serialized_header);
|
||||
const header = yield* PROTO.Header.deserialize(header_stream);
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
yield* listeners[i](header, event.message);
|
||||
}
|
||||
}
|
||||
})());
|
||||
});
|
||||
function create_listener(endpoint, listener) {
|
||||
let listeners = ENDPOINTS.get(endpoint);
|
||||
if (!listeners) {
|
||||
listeners = new Array();
|
||||
ENDPOINTS.set(endpoint, listeners);
|
||||
}
|
||||
listeners.push(listener);
|
||||
return () => {
|
||||
const idx = listeners.indexOf(listener);
|
||||
if (idx !== -1)
|
||||
listeners.splice(idx, 1);
|
||||
if (listeners.length === 0) {
|
||||
ENDPOINTS.delete(endpoint);
|
||||
}
|
||||
};
|
||||
}
|
||||
function generate_id() {
|
||||
const r = (Math.random() * 0x100000000) >>> 0;
|
||||
return ((r & 0xff).toString(16).padStart(2, '0') +
|
||||
((r >> 8) & 0xff).toString(16).padStart(2, '0') +
|
||||
((r >> 16) & 0xff).toString(16).padStart(2, '0') +
|
||||
((r >> 24) & 0xff).toString(16).padStart(2, '0')).toUpperCase();
|
||||
}
|
||||
function* emit(endpoint, serializer, value) {
|
||||
const guid = generate_id();
|
||||
const endpoint_stream = new PROTO.ByteQueue();
|
||||
yield* PROTO.Endpoint.serialize(endpoint, endpoint_stream);
|
||||
const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream);
|
||||
const RUN = function* (header, serialized_packet) {
|
||||
const header_stream = new PROTO.ByteQueue();
|
||||
yield* PROTO.Header.serialize(header, header_stream);
|
||||
const serialized_header = yield* PROTO.MIPS.serialize(header_stream);
|
||||
world
|
||||
.getDimension('overworld')
|
||||
.runCommand(`scriptevent ${serialized_endpoint}:${serialized_header} ${serialized_packet}`);
|
||||
};
|
||||
const packet_stream = new PROTO.ByteQueue();
|
||||
yield* serializer.serialize(value, packet_stream);
|
||||
const serialized_packets = yield* serialize(packet_stream, FRAG_MAX);
|
||||
for (let i = 0; i < serialized_packets.length; i++) {
|
||||
const serialized_packet = serialized_packets[i];
|
||||
yield* RUN({ guid, encoding: ENCODING, index: i, final: i === serialized_packets.length - 1 }, serialized_packet);
|
||||
}
|
||||
}
|
||||
NET.emit = emit;
|
||||
function listen(endpoint, serializer, callback) {
|
||||
const buffer = new Map();
|
||||
const listener = function* (payload, serialized_packet) {
|
||||
let fragment = buffer.get(payload.guid);
|
||||
if (!fragment) {
|
||||
fragment = { size: -1, serialized_packets: [], data_size: 0 };
|
||||
buffer.set(payload.guid, fragment);
|
||||
}
|
||||
if (payload.final) {
|
||||
fragment.size = payload.index + 1;
|
||||
}
|
||||
fragment.serialized_packets[payload.index] = serialized_packet;
|
||||
fragment.data_size += payload.index + 1;
|
||||
if (fragment.size !== -1 && fragment.data_size === (fragment.size * (fragment.size + 1)) / 2) {
|
||||
const stream = yield* deserialize(fragment.serialized_packets);
|
||||
const value = yield* serializer.deserialize(stream);
|
||||
yield* callback(value);
|
||||
buffer.delete(payload.guid);
|
||||
}
|
||||
};
|
||||
return create_listener(endpoint, listener);
|
||||
}
|
||||
NET.listen = listen;
|
||||
})(NET || (NET = {}));
|
||||
export var IPC;
|
||||
(function (IPC) {
|
||||
/** Sends a message with `args` to `channel` */
|
||||
function send(channel, serializer, value) {
|
||||
system.runJob(NET.emit(`ipc:${channel}:send`, serializer, value));
|
||||
}
|
||||
IPC.send = send;
|
||||
/** Sends an `invoke` message through IPC, and expects a result asynchronously. */
|
||||
function invoke(channel, serializer, value, deserializer) {
|
||||
system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value));
|
||||
return new Promise(resolve => {
|
||||
const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value) {
|
||||
resolve(value);
|
||||
terminate();
|
||||
});
|
||||
});
|
||||
}
|
||||
IPC.invoke = invoke;
|
||||
/** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */
|
||||
function on(channel, deserializer, listener) {
|
||||
return NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
|
||||
listener(value);
|
||||
});
|
||||
}
|
||||
IPC.on = on;
|
||||
/** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */
|
||||
function once(channel, deserializer, listener) {
|
||||
const terminate = NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
|
||||
listener(value);
|
||||
terminate();
|
||||
});
|
||||
return terminate;
|
||||
}
|
||||
IPC.once = once;
|
||||
/** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */
|
||||
function handle(channel, deserializer, serializer, listener) {
|
||||
return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value) {
|
||||
const result = listener(value);
|
||||
yield* NET.emit(`ipc:${channel}:handle`, serializer, result);
|
||||
});
|
||||
}
|
||||
IPC.handle = handle;
|
||||
})(IPC || (IPC = {}));
|
||||
export default IPC;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Rules
|
||||
import './rules/easyPlace';
|
||||
import './rules/fastEasyPlace';
|
||||
|
||||
// Commands
|
||||
import './commands/structool';
|
||||
|
||||
// Other
|
||||
import './classes/BlockInfo';
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Rule } from '../lib/canopy/CanopyExtension';
|
||||
import { extension } from '../config';
|
||||
import { BlockPermutation, EntityComponentTypes, GameMode, ItemStack, system, world } from '@minecraft/server';
|
||||
import { structureCollection } from '../classes/StructureCollection';
|
||||
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
|
||||
blockIdToItemStackMap } from '../data';
|
||||
import { fetchMatchingItemSlot } from '../utils';
|
||||
|
||||
const ACTION_SLOT = 35;
|
||||
|
||||
const easyPlace = new Rule({
|
||||
identifier: 'easyPlace',
|
||||
description: { text: "Automatically places the correct block in a structure (paper named 'easyPlace' in bottom right inventory slot)." },
|
||||
onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); },
|
||||
onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); }
|
||||
})
|
||||
extension.addRule(easyPlace);
|
||||
|
||||
function onPlayerPlaceBlock(event) {
|
||||
const { player, block, permutationBeingPlaced } = event;
|
||||
if (!player || !block || !hasActionItemInCorrectSlot(player)) return;
|
||||
const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location);
|
||||
if (!structureBlock)
|
||||
return;
|
||||
tryPlaceBlock(event, player, block, structureBlock);
|
||||
}
|
||||
|
||||
function hasActionItemInCorrectSlot(player) {
|
||||
const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container;
|
||||
if (!inventory)
|
||||
return false;
|
||||
const actionSlot = inventory.getSlot(ACTION_SLOT);
|
||||
return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace';
|
||||
}
|
||||
|
||||
function tryPlaceBlock(event, player, block, structureBlock) {
|
||||
if (shouldPreventAction(player, structureBlock))
|
||||
return preventAction(event, player);
|
||||
structureBlock = tryConvertBannedToValidBlock(structureBlock);
|
||||
if (player.getGameMode() === GameMode.creative) {
|
||||
placeBlock(block, structureBlock);
|
||||
} else if (player.getGameMode() === GameMode.survival) {
|
||||
structureBlock = tryConvertToDefaultState(structureBlock);
|
||||
tryPlaceBlockSurvival(event, player, block, structureBlock);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldPreventAction(player, structureBlock) {
|
||||
return isBannedBlock(player, 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))
|
||||
return true;
|
||||
if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId))
|
||||
return true;
|
||||
const allowedStates = whitelistedBlockStates[blockId];
|
||||
if (allowedStates) {
|
||||
for (const [stateKey, stateValue] of Object.entries(allowedStates)) {
|
||||
if (structureBlock.getState(stateKey) !== stateValue)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryConvertBannedToValidBlock(structureBlock) {
|
||||
const blockId = structureBlock.type.id.replace('minecraft:', '');
|
||||
if (Object.keys(bannedToValidBlockMap).includes(blockId))
|
||||
return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates());
|
||||
return structureBlock;
|
||||
}
|
||||
|
||||
function tryConvertToDefaultState(structureBlock) {
|
||||
const newStates = {};
|
||||
for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) {
|
||||
if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey])
|
||||
newStates[stateKey] = resetToBlockStates[stateKey];
|
||||
else
|
||||
newStates[stateKey] = stateValue;
|
||||
}
|
||||
return BlockPermutation.resolve(structureBlock.type.id, newStates);
|
||||
}
|
||||
|
||||
function tryPlaceBlockSurvival(event, player, block, structureBlock) {
|
||||
const placeableItemStack = getPlaceableItemStack(structureBlock);
|
||||
// console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`);
|
||||
const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId);
|
||||
if (itemSlotToUse) {
|
||||
event.cancel = true;
|
||||
placeBlock(block, structureBlock, itemSlotToUse);
|
||||
} else {
|
||||
preventAction(event, player);
|
||||
}
|
||||
}
|
||||
|
||||
function getPlaceableItemStack(structureBlock) {
|
||||
const blockId = structureBlock.type.id.replace('minecraft:', '');
|
||||
const newItemId = blockIdToItemStackMap[blockId];
|
||||
return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack();
|
||||
}
|
||||
|
||||
function placeBlock(block, structureBlock, itemSlot) {
|
||||
system.run(() => {
|
||||
if (itemSlot) {
|
||||
consumeItem(itemSlot);
|
||||
}
|
||||
block.setPermutation(structureBlock);
|
||||
});
|
||||
}
|
||||
|
||||
function consumeItem(itemSlot) {
|
||||
if (specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]) {
|
||||
consumeSpecial(itemSlot);
|
||||
} else {
|
||||
if (itemSlot.amount === 1)
|
||||
itemSlot.setItem(void 0);
|
||||
else
|
||||
itemSlot.amount--;
|
||||
}
|
||||
}
|
||||
|
||||
function consumeSpecial(itemSlot) {
|
||||
itemSlot.setItem(new ItemStack(specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]));
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Rule } from '../lib/canopy/CanopyExtension';
|
||||
import { extension } from '../config';
|
||||
import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server';
|
||||
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
|
||||
blockIdToItemStackMap } from '../data';
|
||||
import { Raycaster } from '../classes/Raycaster';
|
||||
|
||||
let runner = void 0;
|
||||
const easyPlace = new Rule({
|
||||
identifier: 'fastEasyPlace',
|
||||
description: { text: "Looking at a structure block with a paper named 'easyPlace' in your hand will place it." },
|
||||
onEnableCallback: () => { runner = system.runInterval(onTick, 2); },
|
||||
onDisableCallback: () => { system.clearRun(runner); }
|
||||
})
|
||||
extension.addRule(easyPlace);
|
||||
|
||||
function onTick() {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
if (!player)
|
||||
continue;
|
||||
processEasyPlace(player);
|
||||
}
|
||||
}
|
||||
|
||||
function processEasyPlace(player) {
|
||||
if (!player || !isHoldingActionItem(player)) return;
|
||||
const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true });
|
||||
if (!structureBlock)
|
||||
return;
|
||||
const worldBlock = player.dimension.getBlock(structureBlock.location);
|
||||
tryPlaceBlock(player, worldBlock, structureBlock.permutation);
|
||||
}
|
||||
|
||||
function isHoldingActionItem(player) {
|
||||
const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand);
|
||||
if (!mainhandItemStack)
|
||||
return false;
|
||||
return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'easyPlace';
|
||||
}
|
||||
|
||||
function tryPlaceBlock(player, worldBlock, structureBlock) {
|
||||
if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(player, worldBlock)) return;
|
||||
structureBlock = tryConvertBannedToValidBlock(structureBlock);
|
||||
if (player.getGameMode() === GameMode.creative) {
|
||||
placeBlock(worldBlock, structureBlock);
|
||||
} else if (player.getGameMode() === GameMode.survival) {
|
||||
structureBlock = tryConvertToDefaultState(structureBlock);
|
||||
tryPlaceBlockSurvival(player, worldBlock, structureBlock);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId))
|
||||
return true;
|
||||
const allowedStates = whitelistedBlockStates[blockId];
|
||||
if (allowedStates) {
|
||||
for (const [stateKey, stateValue] of Object.entries(allowedStates)) {
|
||||
if (structureBlock.getState(stateKey) !== stateValue)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryConvertBannedToValidBlock(structureBlock) {
|
||||
const blockId = structureBlock.type.id.replace('minecraft:', '');
|
||||
if (Object.keys(bannedToValidBlockMap).includes(blockId))
|
||||
return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates());
|
||||
return structureBlock;
|
||||
}
|
||||
|
||||
function tryConvertToDefaultState(structureBlock) {
|
||||
const newStates = {};
|
||||
for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) {
|
||||
if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey])
|
||||
newStates[stateKey] = resetToBlockStates[stateKey];
|
||||
else
|
||||
newStates[stateKey] = stateValue;
|
||||
}
|
||||
return BlockPermutation.resolve(structureBlock.type.id, newStates);
|
||||
}
|
||||
|
||||
function tryPlaceBlockSurvival(player, block, structureBlock) {
|
||||
const placeableItemStack = getPlaceableItemStack(structureBlock);
|
||||
// console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`);
|
||||
const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId);
|
||||
if (itemSlotToUse) {
|
||||
placeBlock(block, structureBlock, itemSlotToUse);
|
||||
}
|
||||
}
|
||||
|
||||
function getPlaceableItemStack(structureBlock) {
|
||||
const blockId = structureBlock.type.id.replace('minecraft:', '');
|
||||
const newItemId = blockIdToItemStackMap[blockId];
|
||||
return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack();
|
||||
}
|
||||
|
||||
function fetchMatchingItemSlot(player, itemToMatchId) {
|
||||
if (!itemToMatchId)
|
||||
return void 0;
|
||||
const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container;
|
||||
if (!inventory)
|
||||
return void 0;
|
||||
for (let index = 0; index < inventory.size; index++) {
|
||||
const itemSlot = inventory.getSlot(index);
|
||||
if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatchId)
|
||||
return itemSlot;
|
||||
}
|
||||
}
|
||||
|
||||
function placeBlock(block, structureBlock, itemSlot) {
|
||||
system.run(() => {
|
||||
if (itemSlot) {
|
||||
consumeItem(itemSlot);
|
||||
}
|
||||
block.setPermutation(structureBlock);
|
||||
});
|
||||
}
|
||||
|
||||
function consumeItem(itemSlot) {
|
||||
if (specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]) {
|
||||
consumeSpecial(itemSlot);
|
||||
} else {
|
||||
if (itemSlot.amount === 1)
|
||||
itemSlot.setItem(void 0);
|
||||
else
|
||||
itemSlot.amount--;
|
||||
}
|
||||
}
|
||||
|
||||
function consumeSpecial(itemSlot) {
|
||||
itemSlot.setItem(new ItemStack(specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { system, EntityComponentTypes } from '@minecraft/server';
|
||||
import { FormCancelationReason } from '@minecraft/server-ui';
|
||||
|
||||
export async function forceShow(player, form, timeout = Infinity) {
|
||||
const startTick = system.currentTick;
|
||||
while ((system.currentTick - startTick) < timeout) {
|
||||
const response = await form.show(player);
|
||||
if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy)
|
||||
player.sendMessage("§8Close your chat window to access the menu.");
|
||||
if (response.cancelationReason !== FormCancelationReason.UserBusy)
|
||||
return response;
|
||||
}
|
||||
throw new Error("Menu timed out.");
|
||||
};
|
||||
|
||||
export function fetchMatchingItemSlot(entity, itemToMatchId) {
|
||||
if (!itemToMatchId)
|
||||
return void 0;
|
||||
const inventory = entity.getComponent(EntityComponentTypes.Inventory)?.container;
|
||||
if (!inventory)
|
||||
return void 0;
|
||||
for (let index = 0; index < inventory.size; index++) {
|
||||
const itemSlot = inventory.getSlot(index);
|
||||
if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatchId)
|
||||
return itemSlot;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user