Merge pull request #32 from ForestOfLight/dev

v1.1.0
This commit is contained in:
Forest
2026-06-16 16:10:24 -07:00
committed by GitHub
Unverified
63 changed files with 2744 additions and 141 deletions
+3
View File
@@ -1,3 +1,6 @@
.DS_Store .DS_Store
/build /build
/.regolith /.regolith
docs/superpowers
.claude
node_modules/
+6 -1
View File
@@ -6,8 +6,9 @@
[![GitHub Downloads](https://img.shields.io/github/downloads/ForestOfLight/Construct/total?label=Github%20downloads&logo=github)](https://github.com/ForestOfLight/Construct/releases) [![GitHub Downloads](https://img.shields.io/github/downloads/ForestOfLight/Construct/total?label=Github%20downloads&logo=github)](https://github.com/ForestOfLight/Construct/releases)
[![Curseforge Downloads](https://cf.way2muchnoise.eu/full_1283139_downloads.svg)](https://www.curseforge.com/minecraft-bedrock/addons/construct) [![Curseforge Downloads](https://cf.way2muchnoise.eu/full_1283139_downloads.svg)](https://www.curseforge.com/minecraft-bedrock/addons/construct)
[![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.20_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) [![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.30_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs)
[![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm) [![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm)
[![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://buymeacoffee.com/forestoflight)
</div> </div>
--- ---
@@ -78,3 +79,7 @@ If you have any issues or suggestions, please don't hesitate to open an issue on
### Adding Translations ### Adding Translations
Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out! Construct currently supports American English and Chinese (thanks to [wed150](https://github.com/wed150) & [EndrTrekker](https://github.com/EndrTrekker)). If you would like to contribute a translation, please join our Discord and reach out!
### Donate
If you appreciate my work here and would like to support the future development of my addons, please consider donating to me on [BuyMeACoffee](https://buymeacoffee.com/forestoflight). Your support is greatly appreciated!
+16
View File
@@ -0,0 +1,16 @@
# API Reference
This section documents the public API of Construct, including all endpoints that can be accessed by other addons or scripts. This is intended for advanced users who want to extend or integrate with Construct beyond the provided CLI commands and GUI features.
# API Overview
Construct's API can be accessed via the [AddonAPIKit](https://github.com/ForestOflight/addonapikit) package. Instructions for importing the API into your project can be found in the AddonAPIKit documentation.
You'll need to include Construct's Data Model definitions in your project to work with the API effectively. These data models define the structure of the data passed to and from the API endpoints. They can be found at [`packs/BP/scripts/API/ConstructAPIModel.js`](https://github.com/ForestOfLight/Construct/blob/main/packs/BP/scripts/API/ConstructAPIModel.js). Copy the file into your project and import the models as needed.
Feedback on the Construct API is very welcome! If you have suggestions for new endpoints, improvements to existing ones, or any other feedback, please open a GitHub issue.
Detailed information about the available API endpoints can be found in the following pages:
- [Endpoints](./API/Endpoints.md)
- [Data Models](./API/DataModels.md)
+56
View File
@@ -0,0 +1,56 @@
# Data Models
The following data models are used to represent the format of the data passed by the API endpoints. More information about the API endpoints can be found in the [Endpoints](./Endpoints.md) page.
You'll need to include Construct's Data Model definitions in your project to work with the API effectively. These data models define the structure of the data passed to and from the API endpoints. They can be found at [`packs/BP/scripts/API/ConstructAPIModel.js`](https://github.com/ForestOfLight/Construct/blob/main/packs/BP/scripts/API/ConstructAPIModel.js). Copy the file into your project and import the models as needed.
## Instance
An `Instance` represents a single structure instance in the world, along with its properties and state.
```typescript
interface Instance {
name: PROTO.String,
structureId: PROTO.String,
isEnabled: PROTO.Boolean,
dimensionId: PROTO.Optional(PROTO.String),
location: PROTO.Optional({
x: PROTO.Float64,
y: PROTO.Float64,
z: PROTO.Float64
}),
bounds: PROTO.Optional(PROTO.Object({
min: {
x: PROTO.Float64,
y: PROTO.Float64,
z: PROTO.Float64
},
max: {
x: PROTO.Float64,
y: PROTO.Float64,
z: PROTO.Float64
}
})),
currentLayer: PROTO.Int16,
maxLayer: PROTO.Int16,
verifier: PROTO.Object({
isEnabled: PROTO.Boolean,
trackPlayerDistance: PROTO.Int8,
particleLifetime: PROTO.Int32
})
}
```
## Builder
A `Builder` represents a single builder (Construct's name for Players) in the world, along with its settings and properties.
```typescript
interface Builder {
playerId: PROTO.String,
easyPlace: PROTO.Boolean,
fastEasyPlace: PROTO.Boolean,
materialGrabber: PROTO.Boolean,
materialInstanceName: PROTO.String
}
```
+75
View File
@@ -0,0 +1,75 @@
# Endpoints
This page documents all the available API endpoints that can be accessed by other addons or scripts. The API is designed so that entire objects are passed at once, rather than making multiple calls to edit or query individual properties. This allows for fewer API calls and easier access to data.
## Instances
### `construct:instances`
Get a list of all registered instance names.
- **Parameters**: `void`
- **Returns**: `string[]`
---
### `construct:instance:get`
Get the full data object for a specific instance.
- **Parameters**: `instanceName: string`
- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance))
---
### `construct:instance:add`
Create a new instance with a given name and structure ID.
- **Parameters**: `instanceName: string`, `structureId: string`
- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance))
---
### `construct:instance:edit`
Edit properties of an existing instance (e.g. enabled state, position).
- **Parameters**: `instanceName: string`, `properties: Instance`
- **Returns**: `Instance` (see [Data Model](./DataModels.md#instance))
---
### `construct:instance:delete`
Permanently delete an instance.
- **Parameters**: `instanceName: string`
- **Returns**: `void`
---
### `construct:instance:materials`
Get a list of materials required to build the active section of an instance. Respects the active layer.
- **Parameters**: `instanceName: string`
- **Returns**: `Map<string, number>` (material name to quantity)
## Builders
### `construct:builder:get`
Get the full data object for a specific builder (player).
- **Parameters**: `playerId: string`
- **Returns**: `Builder` (see [Data Model](./DataModels.md#builder))
---
### `construct:builder:edit`
Edit properties of an existing builder.
- **Parameters**: `playerId: string`, `properties: Builder`
- **Returns**: `Builder` (see [Data Model](./DataModels.md#builder))
+149
View File
@@ -0,0 +1,149 @@
# CLI Reference
All commands are prefixed with `construct:`. Arguments in angle brackets (`<arg>`) are required, while those in square brackets (`[arg]`) are optional. Assume commands can be run from any source (player, entity, block, or server) unless otherwise noted.
## Instance Management
### `construct:create <instanceName> <structureId>`
Create a new instance bound to a structure.
| Argument | Description |
|---|---|
| `<instanceName>` | Unique name for this instance. |
| `<structureId>` | ID of a structure saved in the world (without the `mystructure:` prefix). |
> Corresponds to the "create new instance" flow in the main menu. Errors if `instanceName` is already taken or `structureId` does not exist.
### `construct:delete <instanceName>`
Permanently delete an instance.
| Argument | Description |
|---|---|
| `<instanceName>` | Name of the instance to delete. |
> Calls `structureCollection.delete()`. Also disables the instance and clears its saved dynamic properties before removal.
### `construct:rename <instanceName> <newInstanceName>`
Rename an existing instance.
| Argument | Description |
|---|---|
| `<instanceName>` | Current instance name. |
| `<newInstanceName>` | Desired new name. Errors if already in use. |
### `construct:list`
List all registered instances and their status.
> Prints each instance name, its bound structure ID, enabled/disabled state, and placed location (if any). Useful for scripting and quick inspection without opening the GUI.
## Placement & Movement
### `construct:place <instanceName> <x y z>`
Enable and place an instance at a location.
| Argument | Description |
|---|---|
| `<instanceName>` | Instance to place. |
| `<x y z>` | World coordinates. Errors if omitted. Supports tilde (`~`) notation. |
> Equivalent to the "Place" button in the instance menu — enables the instance and calls `move()` in one step. If the instance already has a location, this is a move, not a fresh place.
### `construct:move <instanceName> [x y z]`
Reposition a placed instance without toggling its enabled state.
| Argument | Description |
|---|---|
| `<instanceName>` | Name of a placed instance. |
| `<x y z>` | Target world coordinates. |
## Enable / Disable
### `construct:enable <instanceName>`
Enable a placed instance.
> Requires the instance to have a saved location. Refreshes the outliner, verifier, and materials cache.
### `construct:disable <instanceName>`
Disable an active instance.
> Tears down outliner rendering and pauses the verifier. The instance retains its saved location and can be re-enabled.
## Layer Control
### `construct:layer <instanceName> <layer>`
Set the active layer of an instance.
| Argument | Description |
|---|---|
| `<instanceName>` | Name of the instance. |
| `<layer>` | Integer layer index. `0` = whole structure (no layer selected). Valid range: `0` to `structure.height`. |
> Errors if `layer` is out of bounds. Only meaningful for structures with height > 1.
### `construct:nextlayer <instanceName>`
Step the layer up by one (wraps from max back to `0`).
> Mirrors the "Next layer" button. Wrapping from max → `0` restores the whole-structure view.
### `construct:prevlayer <instanceName>`
Step the layer down by one (wraps from `0` back to max).
> Mirrors the "Previous layer" button.
## Settings
### `construct:verifier <instanceName> true|false`
Toggle the structure verifier for an instance.
| Argument | Description |
|---|---|
| `<instanceName>` | Name of the instance. |
| `true\|false` | Whether to run the verifier. Corresponds to the "validation" toggle in Settings. |
### `construct:option <optionId> true|false`
Enable or disable a per-player builder option. Must be run as a player source.
| Argument | Description |
|---|---|
| `<optionId>` | One of: `easyPlace`, `fastEasyPlace`, `materialGrabber`. |
| `true\|false` | Desired state. Runs the option's enable/disable callback (gives or removes the action item). |
> Replaces the toggles in the Builder Options form. State is saved per-player via dynamic properties.
## Information
### `construct:info <instanceName>`
Print instance details to chat.
> Outputs: bound structure ID, enabled state, placed location and dimension, current layer, verifier enabled, structure bounds (min/max). Equivalent to the data shown in the instance menu body.
### `construct:stats <instanceName>`
Run the structure verifier and print statistics.
> Triggers a standalone `StructureVerifier` pass (same as the "Statistics" button) and sends the result to chat. Errors if a verifier is already running on this instance.
### `construct:materials <instanceName> [missing]`
Print the material list for an instance.
| Argument | Description |
|---|---|
| `<instanceName>` | Name of the instance. |
| `missing` | When present, show only materials the player does not have in their inventory (mirrors the "missing only" toggle). Can only be used by a player source. |
> Respects the active layer: if a layer is set, only that layer's material counts are shown.
## Utility
### `construct:item`
Give yourself the Construct menu item.
> Already implemented as a native custom command. Needs to be refactored to fit the new command pipeline.
### `construct:tag <instanceName>`
Rename the held Construct item to an instance name for quick-open. Errors if the item is not a construct item or if the instance name is not registered.
| Argument | Description |
|---|---|
| `<instanceName>` | Instance name to embed in the item's `nameTag`. Using the item in-world will jump straight to that instance's menu. |
> The item-use handler in `construct.js` already checks `itemStack.nameTag` against known instance names; this command just makes it easy to tag an item without renaming it in an anvil.
+47
View File
@@ -0,0 +1,47 @@
{
"name": "Construct",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@minecraft/server": "^2.9.0-beta.1.26.30-stable",
"@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable"
}
},
"node_modules/@minecraft/common": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@minecraft/common/-/common-1.3.0.tgz",
"integrity": "sha512-GLT8USFhvEyeTTFHZAgszbrnoT007hmXmK+aO4l+2A1up9/zwZ+4e8R8F0KcKrCWDjLEqkOJtPow0hQCcNJ++Q==",
"license": "MIT",
"peer": true
},
"node_modules/@minecraft/server": {
"version": "2.9.0-rc.1.26.40-preview.24",
"resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.9.0-rc.1.26.40-preview.24.tgz",
"integrity": "sha512-/DH82sRkYjADbVh5lEzpQQYKy+oRuQqLLOMGI1Yml1Q/fd3nIHedveoGpU1GkL0hzNFTJGG/pi7D2pPJEhfGzw==",
"license": "MIT",
"peerDependencies": {
"@minecraft/common": "^1.2.0",
"@minecraft/vanilla-data": ">=1.20.70 || 1.26.40-preview.24"
}
},
"node_modules/@minecraft/server-ui": {
"version": "2.2.0-beta.1.26.30-stable",
"resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.2.0-beta.1.26.30-stable.tgz",
"integrity": "sha512-OMkGdrU5w/g/oIHR6ltpxbTNzEDYcDoHI56jW5FOBK+U6UwBO5wQteAPVvKKcBqD8KsY0R72xw1cKNfVwwJFIg==",
"license": "MIT",
"peerDependencies": {
"@minecraft/common": "^1.0.0",
"@minecraft/server": "^2.0.0 || ^2.9.0-beta.1.26.30-stable"
}
},
"node_modules/@minecraft/vanilla-data": {
"version": "1.26.21",
"resolved": "https://registry.npmjs.org/@minecraft/vanilla-data/-/vanilla-data-1.26.21.tgz",
"integrity": "sha512-bDmqSIjZBoaChpAdK2H3SVzhIod4/kXwY+viEA3AgftAbda1X1dkjMiBbxZcmwzRrVwT9embIB3zsEkYn1rSNA==",
"license": "MIT",
"peer": true
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"@minecraft/server": "^2.9.0-beta.1.26.30-stable",
"@minecraft/server-ui": "^2.2.0-beta.1.26.30-stable"
}
}
+7 -7
View File
@@ -1,11 +1,11 @@
{ {
"format_version": 2, "format_version": 2,
"header": { "header": {
"name": "Construct [BP] v1.0.9", "name": "Construct [BP] v1.1.0",
"description": "Survival building addon by §aForestOfLight§r.", "description": "Survival building addon by §aForestOfLight§r.",
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"min_engine_version": [1, 26, 20], "min_engine_version": [1, 26, 30],
"version": [1, 0, 9] "version": [1, 1, 0]
}, },
"modules": [ "modules": [
{ {
@@ -26,15 +26,15 @@
"dependencies": [ "dependencies": [
{ {
"module_name": "@minecraft/server", "module_name": "@minecraft/server",
"version": "2.8.0-beta" "version": "2.9.0-beta"
}, },
{ {
"module_name": "@minecraft/server-ui", "module_name": "@minecraft/server-ui",
"version": "2.1.0-beta" "version": "2.2.0-beta"
}, },
{ {
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", // Construct RP "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4",
"version": [1, 0, 9] "version": [1, 1, 0]
} }
], ],
"metadata": { "metadata": {
+18
View File
@@ -0,0 +1,18 @@
import { AddonAPIServer } from "../lib/AddonAPIKit";
import { PACK_IDENTIFIER } from "../consts";
import { InstancesController } from "./controllers/InstancesController";
import { structureCollection } from "../classes/Structure/StructureCollection";
import { BuildersController } from "./controllers/BuildersController";
import { Builders } from "../classes/Builder/Builders";
class ConstructAPI extends AddonAPIServer {
constructor(version) {
super(PACK_IDENTIFIER, version);
const instancesController = new InstancesController(structureCollection);
this.setupController(instancesController);
const buildersController = new BuildersController(Builders);
this.setupController(buildersController);
}
}
export const constructAPI = new ConstructAPI("1.0.0");
+60
View File
@@ -0,0 +1,60 @@
import { PROTO } from "../lib/AddonAPIKit";
// Instances
const LocationModel = PROTO.Object({
x: PROTO.Float64,
y: PROTO.Float64,
z: PROTO.Float64
});
export const InstanceModel = PROTO.Object({
name: PROTO.String,
structureId: PROTO.String,
isEnabled: PROTO.Boolean,
dimensionId: PROTO.Optional(PROTO.String),
location: PROTO.Optional(LocationModel),
bounds: PROTO.Optional(PROTO.Object({
min: LocationModel,
max: LocationModel
})),
currentLayer: PROTO.Int16,
maxLayer: PROTO.Int16,
verifier: PROTO.Object({
isEnabled: PROTO.Boolean,
trackPlayerDistance: PROTO.Int8,
particleLifetime: PROTO.Int32
})
});
export const InstancesModel = PROTO.Array(InstanceModel);
export const StructureMaterialsModel = PROTO.Map(PROTO.String, PROTO.Int32);
export const InstanceNameParameterModel = PROTO.Object({
instanceName: PROTO.String
});
export const AddInstanceParameterModel = PROTO.Object({
instanceName: PROTO.String,
structureId: PROTO.String
});
export const EditInstanceParameterModel = PROTO.Object({
instanceName: PROTO.String,
instance: InstanceModel
});
// Builders
export const BuilderModel = PROTO.Object({
playerId: PROTO.String,
easyPlace: PROTO.Boolean,
fastEasyPlace: PROTO.Boolean,
materialGrabber: PROTO.Boolean,
materialInstanceName: PROTO.String
});
export const BuilderIdParameterModel = PROTO.Object({
playerId: PROTO.String
});
@@ -0,0 +1,36 @@
import { BuilderNotFoundError } from "../../classes/Errors/BuilderNotFoundError";
import { APICallerError, APIController } from "../../lib/AddonAPIKit";
import { BuilderIdParameterModel, BuilderModel } from "../ConstructAPIModel";
export class BuildersController extends APIController {
#context;
constructor(context) {
super();
this.addEndpoint("builder:get", this.getBuilder, BuilderIdParameterModel, BuilderModel);
this.addEndpoint("builder:edit", this.editBuilder, BuilderModel, BuilderModel);
this.#context = context;
}
getBuilder(playerId) {
try {
const builder = this.#context.get(playerId);
return builder.asPacket();
} catch(error) {
if (error instanceof BuilderNotFoundErrors)
throw new APICallerError(error);
throw error;
}
}
editBuilder(builderOptions) {
try {
const builder = this.#context.get(builderOptions.playerId);
builder.setOptions(builderOptions);
} catch(error) {
if (error instanceof BuilderNotFoundError)
throw new APICallerError(error);
throw error;
}
}
}
@@ -0,0 +1,80 @@
import { InstanceExistsError } from "../../classes/Errors/InstanceExistsError";
import { InstanceNotFoundError } from "../../classes/Errors/InstanceNotFoundError";
import { StructureNotFoundError } from "../../classes/Errors/StructureNotFoundError";
import { APICallerError, VoidModel, APIController } from "../../lib/AddonAPIKit";
import { AddInstanceParameterModel, EditInstanceParameterModel, InstanceModel, InstanceNameParameterModel, InstancesModel, StructureMaterialsModel } from "../ConstructAPIModel";
export class InstancesController extends APIController {
#context;
constructor(context) {
super();
this.addEndpoint("instances", this.getInstances, VoidModel, InstancesModel);
this.addEndpoint("instance:get", this.getInstance, InstanceNameParameterModel, InstanceModel);
this.addEndpoint("instance:add", this.addInstance, AddInstanceParameterModel, InstanceModel);
this.addEndpoint("instance:edit", this.editInstance, EditInstanceParameterModel, InstanceModel);
this.addEndpoint("instance:delete", this.deleteInstance, InstanceNameParameterModel, VoidModel);
this.addEndpoint("instance:materials", this.getMaterials, InstanceNameParameterModel, StructureMaterialsModel);
this.#context = context;
}
getInstances() {
return this.#context.getInstanceNames();
}
getInstance(instanceName) {
try {
const instance = this.#context.get(instanceName);
return instance.asPacket();
} catch(error) {
if (error instanceof InstanceNotFoundError)
throw new APICallerError(error);
throw error;
}
}
addInstance(instanceName, structureId) {
try {
const instance = this.#context.add(instanceName, structureId);
return instance.asPacket();
} catch(error) {
if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError)
throw new APICallerError(error);
throw error;
}
}
editInstance(instanceName, instanceOptions) {
try {
const instance = this.#context.get(instanceName);
instance.setOptions(instanceOptions);
return instance.asPacket();
} catch(error) {
if (error instanceof InstanceNotFoundError || error instanceof InstanceExistsError || error instanceof StructureNotFoundError)
throw new APICallerError(error);
throw error;
}
}
deleteInstance(instanceName) {
try {
this.#context.delete(instanceName);
} catch (error) {
if (error instanceof InstanceNotFoundError)
throw new APICallerError(error);
throw error;
}
}
getMaterials(instanceName) {
try {
const instance = this.#context.get(instanceName);
const structureMaterials = instance.getActiveMaterials();
return structureMaterials.allMaterials;
} catch(error) {
if (error instanceof InstanceNotFoundError)
throw new APICallerError(error);
throw error;
}
}
}
@@ -20,4 +20,21 @@ export class Builder {
isFlexibleInstanceMoving() { isFlexibleInstanceMoving() {
return this.flexibleInstanceMovement !== void 0; return this.flexibleInstanceMovement !== void 0;
} }
asPacket() {
return {
playerId: this.playerId,
easyPlace: this.isOptionEnabled('easyPlace'),
fastEasyPlace: this.isOptionEnabled('fastEasyPlace'),
materialGrabber: this.isOptionEnabled('materialGrabber'),
materialInstanceName: this.materialInstanceName
};
}
setOptions(builderOptions) {
this.setOption('easyPlace', builderOptions.easyPlace);
this.setOption('fastEasyPlace', builderOptions.fastEasyPlace);
this.setOption('materialGrabber', builderOptions.materialGrabber);
this.materialInstanceName = builderOptions.materialInstanceName;
}
} }
+14 -6
View File
@@ -1,29 +1,37 @@
import { world } from "@minecraft/server"; import { world } from "@minecraft/server";
import { Builder } from "./Builder"; import { Builder } from "./Builder";
import { BuilderNotFoundError } from "../Errors/BuilderNotFoundError";
export class Builders { export class Builders {
static builders = {}; static builders = {};
static add(playerId) { static add(playerId) {
if (this.builders[playerId]) if (Builders.builders[playerId])
return; return;
this.builders[playerId] = new Builder(playerId); Builders.builders[playerId] = new Builder(playerId);
} }
static remove(playerId) { static remove(playerId) {
delete this.builders[playerId]; delete Builders.builders[playerId];
} }
static get(id) { static get(id) {
return this.builders[id]; const builder = Builders.builders[id];
if (builder === void 0)
throw new BuilderNotFoundError(id);
return builder;
} }
static onJoin(playerId) { static onJoin(playerId) {
this.add(playerId); Builders.add(playerId);
} }
static onLeave(playerId) { static onLeave(playerId) {
this.remove(playerId); Builders.remove(playerId);
}
static getIds() {
return Object.keys(Builders.builders);
} }
} }
@@ -0,0 +1,12 @@
import { CommandOrigin } from "./CommandOrigin";
import { FeedbackMessageType } from "./FeedbackMessageType";
export class BlockCommandOrigin extends CommandOrigin {
getSource() {
return this.source.sourceBlock;
}
sendMessage() {
return FeedbackMessageType.None;
}
}
@@ -0,0 +1,117 @@
import { CustomCommandParamType, CustomCommandSource, CustomCommandStatus, Player, RawMessageError, system } from "@minecraft/server";
import { Commands } from "./Commands.js";
import { BlockCommandOrigin } from "./BlockCommandOrigin";
import { EntityCommandOrigin } from "./EntityCommandOrigin";
import { ServerCommandOrigin } from "./ServerCommandOrigin";
import { PlayerCommandOrigin } from "./PlayerCommandOrigin";
import { PACK_IDENTIFIER } from "../../consts.js";
export class Command {
customCommand;
static resolveCommandOrigin(origin) {
switch (origin.sourceType) {
case CustomCommandSource.Block:
return new BlockCommandOrigin(origin);
case CustomCommandSource.Entity:
if (origin.sourceEntity instanceof Player)
return new PlayerCommandOrigin(origin);
return new EntityCommandOrigin(origin);
case CustomCommandSource.Server:
return new ServerCommandOrigin(origin);
default:
throw new Error("Unknown command source: " + origin?.sourceType);
}
}
constructor(customCommand) {
this.customCommand = customCommand;
this.#setDefaultArgs();
Commands.register(this);
system.beforeEvents.startup.subscribe(this.setupForRegistry.bind(this));
}
getName() {
return this.customCommand.name.replace(/^[^:]+:/, '');
}
isCheatsRequired() {
return this.customCommand.cheatsRequired;
}
setupForRegistry(startupEvent) {
this.#registerCommand(startupEvent.customCommandRegistry);
system.beforeEvents.startup.unsubscribe(this.setupForRegistry.bind(this));
}
#registerCommand(customCommandRegistry) {
this.#addPreCallback();
this.#registerEnums(customCommandRegistry);
this.#registerSingleCommand(customCommandRegistry);
this.#registerAliasCommands(customCommandRegistry);
}
#setDefaultArgs() {
if (this.customCommand.cheatsRequired === void 0)
this.customCommand.cheatsRequired = false;
}
#addPreCallback() {
this.callback = (origin, ...args) => {
const source = Command.resolveCommandOrigin(origin);
if (this.#commandSourceIsNotAllowed(source))
return { status: CustomCommandStatus.Failure, message: 'construct.error.invalidCommandSource' };
try {
return this.customCommand.callback(source, ...args);
} catch (error) {
if (error instanceof RawMessageError)
error.sendTo(source);
else
throw error;
}
}
}
#registerEnums(customCommandRegistry) {
if (this.customCommand.enums) {
for (const customEnum of this.customCommand.enums)
customCommandRegistry.registerEnum(`${PACK_IDENTIFIER}:${customEnum.name}`, customEnum.values);
}
}
#registerSingleCommand(customCommandRegistry, name = this.customCommand.name) {
customCommandRegistry.registerCommand({
name: `${PACK_IDENTIFIER}:${name}`,
description: this.customCommand.description,
permissionLevel: this.customCommand.permissionLevel,
mandatoryParameters: this.#prepParameters(this.customCommand.mandatoryParameters),
optionalParameters: this.#prepParameters(this.customCommand.optionalParameters),
cheatsRequired: this.customCommand.cheatsRequired
}, this.callback);
}
#prepParameters(parameters) {
if (!parameters)
return [];
for (const parameter of parameters) {
if (parameter.name)
parameter.name = `${parameter.name}`;
if (parameter.type === CustomCommandParamType.Enum)
parameter.name = `${PACK_IDENTIFIER}:${parameter.name}`;
}
return parameters;
}
#registerAliasCommands(customCommandRegistry) {
if (this.customCommand.aliases) {
for (const alias of this.customCommand.aliases)
this.#registerSingleCommand(customCommandRegistry, alias);
}
}
#commandSourceIsNotAllowed(source) {
if (!this.customCommand.allowedSources)
return false;
return !this.customCommand.allowedSources.includes(source.constructor);
}
}
@@ -0,0 +1,20 @@
import { FeedbackMessageType } from "./FeedbackMessageType";
export class CommandOrigin {
constructor(source) {
this.source = source;
}
getType() {
return this.source.sourceType;
}
getSource() {
throw new Error("getSource() not implemented");
}
sendMessage(message) {
console.error(`Unknown source type: ${this.source.sourceType}`, message);
return FeedbackMessageType.ConsoleError;
}
}
@@ -0,0 +1,15 @@
export class Commands {
static #commands = [];
static register(command) {
this.#commands.push(command);
}
static getAll() {
return [...this.#commands];
}
static clear() {
this.#commands = [];
}
}
@@ -0,0 +1,13 @@
import { CommandOrigin } from "./CommandOrigin";
import { FeedbackMessageType } from "./FeedbackMessageType";
export class EntityCommandOrigin extends CommandOrigin {
getSource() {
return this.source.sourceEntity;
}
sendMessage(message) {
this.getSource().sendMessage(message);
return FeedbackMessageType.ChatMessage;
}
}
@@ -0,0 +1,7 @@
export const FeedbackMessageType = Object.freeze({
None: "none",
ConsoleInfo: "info",
ConsoleWarn: "warn",
ConsoleError: "error",
ChatMessage: "message"
});
@@ -0,0 +1,17 @@
import { CommandOrigin } from "./CommandOrigin";
import { FeedbackMessageType } from "./FeedbackMessageType";
export class PlayerCommandOrigin extends CommandOrigin {
getType() {
return "Player";
}
getSource() {
return this.source.sourceEntity;
}
sendMessage(message) {
this.getSource().sendMessage(message);
return FeedbackMessageType.ChatMessage;
}
}
@@ -0,0 +1,13 @@
import { CommandOrigin } from "./CommandOrigin";
import { FeedbackMessageType } from "./FeedbackMessageType";
export class ServerCommandOrigin extends CommandOrigin {
getSource() {
return this.source.sourceType;
}
sendMessage(message) {
console.log(message);
return FeedbackMessageType.ConsoleInfo;
}
}
@@ -0,0 +1,6 @@
export class BuilderNotFoundError extends Error {
constructor(builderId) {
super(`§cBuilder "${builderId}" not found.`);
this.name = 'BuilderNotFoundError';
}
}
@@ -0,0 +1,14 @@
export class CommandResponseError extends Error {
constructor(message) {
super(message);
this.name = 'CommandResponseError';
}
getRawMessage() {
throw new Error('getRawMessage() must be implemented by subclasses of CommandResponseError');
}
sendTo(origin) {
origin.sendMessage(this.getRawMessage());
}
}
@@ -0,0 +1,15 @@
import { CommandResponseError } from "./CommandResponseError";
export class InstanceExistsError extends CommandResponseError {
instanceName;
constructor(instanceName) {
super(`An instance with the name "${instanceName}" already exists.`);
this.name = 'InstanceExistsError';
this.instanceName = instanceName;
}
getRawMessage() {
return { translate: 'construct.error.instanceExists', with: [this.instanceName] };
}
}
@@ -0,0 +1,15 @@
import { CommandResponseError } from "./CommandResponseError";
export class InstanceNotFoundError extends CommandResponseError {
instanceName;
constructor(instanceName) {
super(`§cInstance "${instanceName}" not found.`);
this.name = 'InstanceNotFoundError';
this.instanceName = instanceName;
}
getRawMessage() {
return { translate: 'construct.error.instanceNotFound', with: [this.instanceName] };
}
}
@@ -1,6 +0,0 @@
export class InvalidInstanceError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidInstanceError';
}
}
@@ -1,6 +0,0 @@
export class InvalidStructureError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidStructureError';
}
}
@@ -0,0 +1,12 @@
import { CommandResponseError } from "./CommandResponseError";
export class NotAPlayerError extends CommandResponseError {
constructor(message = 'Command requires a player source.') {
super(message);
this.name = 'NotAPlayerError';
}
getRawMessage() {
return { translate: 'construct.commands.error.notAPlayer' };
}
}
@@ -0,0 +1,15 @@
import { CommandResponseError } from "./CommandResponseError";
export class StructureNotFoundError extends CommandResponseError {
structureId;
constructor(structureId) {
super(`Structure with ID "${structureId}" not found.`);
this.name = 'StructureNotFoundError';
this.structureId = structureId;
}
getRawMessage() {
return { translate: 'construct.error.structureNotFound', with: [this.structureId] };
}
}
@@ -1,6 +1,6 @@
import { InputPermissionCategory, world, system } from "@minecraft/server"; import { InputPermissionCategory, world, system } from "@minecraft/server";
import { Outliner } from "../Outliner"; import { Outliner } from "../Outliner";
import { MENU_ITEM } from "../../commands/construct"; import { MENU_ITEM } from "../../consts";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { PlayerMovement } from "../PlayerMovement"; import { PlayerMovement } from "../PlayerMovement";
import { Builders } from "../Builder/Builders"; import { Builders } from "../Builder/Builders";
@@ -26,6 +26,7 @@ export class InstanceOptions extends Option {
this.instanceName = instanceName; this.instanceName = instanceName;
this.structureId = structureId; this.structureId = structureId;
this.load(); this.load();
this.save();
} }
save() { save() {
@@ -45,13 +46,8 @@ export class InstanceOptions extends Option {
return world.getDimension(this.dimensionId); return world.getDimension(this.dimensionId);
} }
enable() { setEnabled(enable) {
this.isEnabled = true; this.isEnabled = enable;
this.save();
}
disable() {
this.isEnabled = false;
this.save(); this.save();
} }
@@ -68,7 +64,7 @@ export class InstanceOptions extends Option {
} }
setLayer(layer) { setLayer(layer) {
this.currentLayer = layer; this.currentLayer = Math.floor(layer);
this.save(); this.save();
} }
@@ -81,4 +77,9 @@ export class InstanceOptions extends Option {
this.verifier.trackPlayerDistance = distance; this.verifier.trackPlayerDistance = distance;
this.save(); this.save();
} }
setVerifierParticleLifetime(lifetime) {
this.verifier.particleLifetime = lifetime;
this.save();
}
} }
@@ -7,6 +7,8 @@ import { world, system, TicksPerSecond } from "@minecraft/server";
import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError"; import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError";
import { StructureMaterials } from "../Materials/StructureMaterials"; import { StructureMaterials } from "../Materials/StructureMaterials";
import { VerificationRenderer } from "../Render/VerificationRenderer"; import { VerificationRenderer } from "../Render/VerificationRenderer";
import { structureCollection } from "../Structure/StructureCollection";
import { InstanceExistsError } from "../Errors/InstanceExistsError";
export class StructureInstance { export class StructureInstance {
options; options;
@@ -185,19 +187,26 @@ export class StructureInstance {
} }
enable() { enable() {
this.options.enable(); this.options.setEnabled(true);
this.refreshBox(); this.refreshBox();
} }
disable() { disable() {
this.options.disable(); this.options.setEnabled(false);
this.refreshBox(); this.refreshBox();
} }
rename(newName) { rename(newName) {
if (structureCollection.has(newName))
throw new InstanceExistsError(newName);
this.options.rename(newName); this.options.rename(newName);
} }
setStructure(structureId) {
this.options.structureId = structureId;
this.structure = new Structure(structureId);
}
place(dimensionId, worldLocation) { place(dimensionId, worldLocation) {
this.enable(); this.enable();
this.move(dimensionId, worldLocation); this.move(dimensionId, worldLocation);
@@ -277,4 +286,38 @@ export class StructureInstance {
toStructureCoords(worldLocation) { toStructureCoords(worldLocation) {
return Vector.from(worldLocation).subtract(this.options.worldLocation); return Vector.from(worldLocation).subtract(this.options.worldLocation);
} }
asPacket() {
const dimensionLocation = this.getLocation();
return {
name: this.getName(),
structureId: this.getStructureId(),
isEnabled: this.isEnabled(),
dimensionId: dimensionLocation.dimensionId,
location: { x: dimensionLocation.location.x, y: dimensionLocation.location.y, z: dimensionLocation.location.z },
bounds: this.getBounds(),
currentLayer: this.getLayer(),
maxLayer: this.getMaxLayer(),
verifier: {
isEnabled: this.options.verifier.isEnabled,
trackPlayerDistance: this.options.verifier.trackPlayerDistance,
particleLifetime: this.options.verifier.particleLifetime
}
};
}
setOptions(newOptions) {
const newVerifierOptions = newOptions.verifier;
if (newOptions.name !== this.getName())
structureCollection.rename(this.getName(), newOptions.name);
this.setStructure(newOptions.structureId);
this.options.setEnabled(newOptions.isEnabled);
this.options.move(newOptions.dimensionId, newOptions.location);
this.options.setLayer(newOptions.currentLayer);
this.options.setVerifierEnabled(newVerifierOptions.isEnabled);
this.options.setVerifierDistance(newVerifierOptions.trackPlayerDistance);
this.options.setVerifierParticleLifetime(newVerifierOptions.particleLifetime);
this.options.save();
this.refreshBox();
}
} }
@@ -1,5 +1,6 @@
import { ItemStack, system } from "@minecraft/server"; import { ItemStack, system } from "@minecraft/server";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { InstanceNotPlacedError } from "../Errors/InstanceNotPlacedError";
class StructureMaterials { class StructureMaterials {
instance; instance;
@@ -21,14 +22,18 @@ class StructureMaterials {
system.runJob(this.populateActive()); system.runJob(this.populateActive());
else else
system.runJob(this.populateAll()); system.runJob(this.populateAll());
} catch (e) { } catch (error) {
if (e.name === 'InstanceNotPlacedError') if (error instanceof InstanceNotPlacedError)
this.clear(); this.clear();
else else
throw e; throw error;
} }
} }
get materials() {
return this.materials;
}
get(itemType) { get(itemType) {
return this.materials[itemType]; return this.materials[itemType];
} }
+9 -11
View File
@@ -3,6 +3,8 @@ import { structureCollection } from './Structure/StructureCollection';
import { MenuFormBuilder } from './MenuFormBuilder'; import { MenuFormBuilder } from './MenuFormBuilder';
import { InstanceForm } from './Instance/InstanceForm'; import { InstanceForm } from './Instance/InstanceForm';
import { BuilderForm } from './Builder/BuilderForm'; import { BuilderForm } from './Builder/BuilderForm';
import { InstanceExistsError } from './Errors/InstanceExistsError';
import { StructureNotFoundError } from './Errors/StructureNotFoundError';
export class MenuForm { export class MenuForm {
constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) { constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) {
@@ -43,12 +45,12 @@ export class MenuForm {
return selectedInstanceName || this.createNewInstance(); return selectedInstanceName || this.createNewInstance();
} }
}); });
} catch (e) { } catch (error) {
if (e.message === 'Menu timed out.') { if (error.message === 'Menu timed out.') {
this.player.sendMessage({ translate: 'construct.menu.open.timeout' }); this.player.sendMessage({ translate: 'construct.menu.open.timeout' });
return void 0; return void 0;
} }
throw e; throw error;
} }
} }
@@ -64,16 +66,12 @@ export class MenuForm {
return void 0; return void 0;
try { try {
structureCollection.add(instanceName, structureId); structureCollection.add(instanceName, structureId);
} catch (e) { } catch (error) {
if (e.name === 'InvalidInstanceError') { if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError) {
this.player.sendMessage({ translate: 'construct.mainmenu.instance.exists', with: [instanceName] }); error.sendTo(this.player);
return void 0; return void 0;
} }
if (e.name === 'InvalidStructureError') { throw error;
this.player.sendMessage({ translate: 'construct.mainmenu.instance.notfound', with: [structureId] });
return void 0;
}
throw e;
} }
return instanceName; return instanceName;
}); });
@@ -0,0 +1,27 @@
import { world, system } from '@minecraft/server';
import { MENU_ITEM } from '../consts';
import { MenuForm } from './MenuForm';
import { structureCollection } from './Structure/StructureCollection';
import { Builders } from './Builder/Builders';
world.beforeEvents.itemUse.subscribe((event) => {
if (!event.source || event.itemStack?.typeId !== MENU_ITEM) return;
event.cancel = true;
const builder = Builders.get(event.source.id);
system.run(() => {
if (builder.isFlexibleInstanceMoving())
return;
openMenu(event.source, event);
});
});
function openMenu(player, event = void 0) {
const options = { jumpToInstance: true };
if (event) {
const instanceNames = structureCollection.getInstanceNames();
const instanceName = event.itemStack?.nameTag;
if (instanceNames.includes(instanceName))
options.instanceName = instanceName;
}
new MenuForm(player, options);
}
@@ -1,3 +1,4 @@
import { StructureNotFoundError } from '../Errors/StructureNotFoundError';
import { Outliner } from '../Outliner'; import { Outliner } from '../Outliner';
export class StructureOutliner { export class StructureOutliner {
@@ -13,11 +14,11 @@ export class StructureOutliner {
this.bounds = this.instance.getBounds(); this.bounds = this.instance.getBounds();
this.bounds.min = this.instance.toGlobalCoords(this.bounds.min); this.bounds.min = this.instance.toGlobalCoords(this.bounds.min);
this.bounds.max = this.instance.toGlobalCoords(this.bounds.max); this.bounds.max = this.instance.toGlobalCoords(this.bounds.max);
} catch (e) { } catch (error) {
if (e.name === 'InvalidStructureError') if (error instanceof StructureNotFoundError)
this.outliner.stopDraw(); this.outliner.stopDraw();
else else
throw e; throw error;
} }
} }
@@ -1,6 +1,6 @@
import { world } from "@minecraft/server"; import { world } from "@minecraft/server";
import { Vector } from "../../lib/Vector"; import { Vector } from "../../lib/Vector";
import { InvalidStructureError } from "../Errors/InvalidStructureError"; import { StructureNotFoundError } from "../Errors/StructureNotFoundError";
export class Structure { export class Structure {
structureId; structureId;
@@ -10,7 +10,7 @@ export class Structure {
this.structureId = structureId; this.structureId = structureId;
this.#structure = world.structureManager.get(structureId); this.#structure = world.structureManager.get(structureId);
if (!this.#structure) if (!this.#structure)
throw new InvalidStructureError(`[Construct] Structure '${structureId}' not found on world.`); throw new StructureNotFoundError(structureId);
this.#structure.saveToWorld(); this.#structure.saveToWorld();
} }
@@ -1,7 +1,9 @@
import { InvalidInstanceError } from '../Errors/InvalidInstanceError'; import { InstanceExistsError } from '../Errors/InstanceExistsError';
import { InstanceNotFoundError } from '../Errors/InstanceNotFoundError';
import { StructureNotFoundError } from '../Errors/StructureNotFoundError';
import { InstanceOptions } from '../Instance/InstanceOptions'; import { InstanceOptions } from '../Instance/InstanceOptions';
import { StructureInstance } from '../Instance/StructureInstance'; import { StructureInstance } from '../Instance/StructureInstance';
import { world } from '@minecraft/server'; import { InvalidStructureError, world } from '@minecraft/server';
class StructureCollection { class StructureCollection {
structures; structures;
@@ -27,7 +29,7 @@ class StructureCollection {
add(instanceName, structureId) { add(instanceName, structureId) {
if (this.structures[instanceName]) if (this.structures[instanceName])
throw new InvalidInstanceError(`Instance ${instanceName} already exists.`); throw new InstanceExistsError(instanceName);
const structure = new StructureInstance(instanceName, structureId); const structure = new StructureInstance(instanceName, structureId);
this.structures[instanceName] = structure; this.structures[instanceName] = structure;
return structure; return structure;
@@ -36,7 +38,7 @@ class StructureCollection {
get(instanceName) { get(instanceName) {
const structure = this.structures[instanceName]; const structure = this.structures[instanceName];
if (!structure) if (!structure)
throw new InvalidInstanceError(`Instance ${instanceName} not found.`); throw new InstanceNotFoundError(instanceName);
return structure; return structure;
} }
@@ -58,12 +60,12 @@ class StructureCollection {
return Object.values(this.structures).filter(structure => { return Object.values(this.structures).filter(structure => {
try { try {
return structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options) return structure.isLocationActive(dimensionId, structure.toStructureCoords(location), options)
} catch (e) { } catch (error) {
if (e.name === 'InvalidStructureError') { if (error instanceof StructureNotFoundError || error instanceof InvalidStructureError) {
structureCollection.delete(structure.name); this.delete(structure.name);
return false; return false;
} else { } else {
throw e; throw error;
} }
} }
}); });
@@ -101,7 +103,7 @@ class StructureCollection {
rename(instanceName, newName) { rename(instanceName, newName) {
const structure = this.get(instanceName); const structure = this.get(instanceName);
if (this.structures[newName]) if (this.structures[newName])
throw new Error(`Instance '${newName}' already exists.`); throw new InstanceExistsError(newName);
structure.rename(newName); structure.rename(newName);
this.structures[newName] = structure; this.structures[newName] = structure;
delete this.structures[instanceName]; delete this.structures[instanceName];
@@ -0,0 +1,38 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { BuilderOptions } from '../classes/Builder/BuilderOptions';
export class BuilderCommand extends Command {
constructor() {
super({
name: 'builder',
description: 'construct.commands.builder',
allowedSources: [PlayerCommandOrigin],
mandatoryParameters: [
{ name: 'builderOption', type: CustomCommandParamType.Enum },
{ name: 'state', type: CustomCommandParamType.Boolean }
],
enums: [
{ name: 'builderOption', values: ['easyPlace', 'fastEasyPlace', 'materialGrabber'] }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, builderOption, state) => this.run(origin, builderOption, state)
});
}
run(origin, builderOption, state) {
if (!BuilderOptions.get(builderOption)) {
origin.sendMessage({ translate: 'construct.commands.builder.unknownOption', with: [builderOption] });
return void 0;
}
system.run(() => {
const player = origin.getSource();
BuilderOptions.setValue(builderOption, player.id, state);
origin.sendMessage({ translate: 'construct.commands.builder.success', with: [builderOption, String(state)] });
});
return { status: CustomCommandStatus.Success };
}
}
export const builderCommand = new BuilderCommand();
@@ -0,0 +1,37 @@
import { CommandPermissionLevel, CustomCommandStatus, EntityComponentTypes, ItemStack, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { MENU_ITEM } from '../consts';
export class ConstructCommand extends Command {
constructor() {
super({
name: 'construct',
description: 'construct.commands.construct',
cheatsRequired: false,
allowedSources: [PlayerCommandOrigin],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin) => this.run(origin)
});
}
run(origin) {
const player = origin.getSource();
system.run(() => {
this.giveMenuItem(player);
});
return { status: CustomCommandStatus.Success };
}
giveMenuItem(player) {
const inventoryComponent = player.getComponent(EntityComponentTypes.Inventory);
const inventoryContainer = inventoryComponent?.container;
const remaining = inventoryContainer?.addItem(new ItemStack(MENU_ITEM));
if (remaining)
player.sendMessage({ translate: 'construct.commands.construct.fail' });
else
player.sendMessage({ translate: 'construct.commands.construct.success' });
}
}
export const constructCommand = new ConstructCommand();
@@ -0,0 +1,50 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { InstanceExistsError } from '../classes/Errors/InstanceExistsError';
import { StructureNotFoundError } from '../classes/Errors/StructureNotFoundError';
export class CreateCommand extends Command {
constructor() {
super({
name: 'create',
description: 'construct.commands.create',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'structureId', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, structureId) => this.run(origin, instanceName, structureId)
});
}
run(origin, instanceName, structureId) {
this.tryAddStructure(origin, instanceName, structureId);
return { status: CustomCommandStatus.Success };
}
tryAddStructure(origin, instanceName, structureId) {
system.run(() => {
try {
this.addStructure(origin, instanceName, structureId);
} catch (error) {
this.handleStructureAdditionErrors(origin, error);
}
});
}
addStructure(origin, instanceName, structureId) {
structureCollection.add(instanceName, structureId);
origin.sendMessage({ translate: 'construct.commands.create.success', with: [instanceName, structureId] });
}
handleStructureAdditionErrors(origin, error) {
if (error instanceof InstanceExistsError || error instanceof StructureNotFoundError)
error.sendTo(origin);
else
throw error;
}
}
export const createCommand = new CreateCommand();
@@ -0,0 +1,28 @@
import { Command } from '../classes/Commands/Command';
import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class DeleteCommand extends Command {
constructor() {
super({
name: 'delete',
description: 'construct.commands.delete',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
system.run(() => {
structureCollection.delete(instanceName);
origin.sendMessage({ translate: 'construct.commands.delete.success', with: [instanceName] });
});
return { status: CustomCommandStatus.Success };
}
}
export const deleteCommand = new DeleteCommand();
@@ -0,0 +1,40 @@
import { Command } from '../classes/Commands/Command';
import { CustomCommandParamType, CustomCommandStatus, CommandPermissionLevel, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class EnableCommand extends Command {
constructor() {
super({
name: 'enable',
description: 'construct.commands.enable',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'state', type: CustomCommandParamType.Boolean }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, state) => this.run(origin, instanceName, state)
});
}
run(origin, instanceName, state) {
const instance = structureCollection.get(instanceName);
if (state && !instance.hasLocation()) {
origin.sendMessage({ translate: 'construct.commands.error.noLocation', with: [instanceName] });
return void 0;
}
system.run(() => {
if (state)
instance.enable();
else
instance.disable();
this.sendFeedback(origin, instanceName, state);
});
return { status: CustomCommandStatus.Success };
}
sendFeedback(origin, instanceName, state) {
origin.sendMessage({ translate: state ? 'construct.commands.enable.true' : 'construct.commands.enable.false', with: [instanceName] });
}
}
export const enableCommand = new EnableCommand();
@@ -0,0 +1,74 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { Vector } from '../lib/Vector';
export class InstanceInfoCommand extends Command {
constructor() {
super({
name: 'instanceinfo',
description: 'construct.commands.instanceinfo',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
const message = { rawtext: [
this.getHeaderText(instance),
{ text: '\n' },
this.getStructureIdText(instance),
{ text: '\n' },
this.getLocationText(instance),
{ text: '\n' },
this.getEnabledText(instance),
{ text: '\n' },
this.getLayerText(instance),
{ text: '\n' },
this.getVerifierText(instance),
{ text: '\n' },
this.getSizeText(instance)
]};
origin.sendMessage(message);
return { status: CustomCommandStatus.Success };
}
getHeaderText(instance) {
return { translate: 'construct.commands.instanceinfo.header', with: [instance.getName()] };
}
getStructureIdText(instance) {
return { translate: 'construct.commands.instanceinfo.structure', with: [instance.getStructureId()] };
}
getLocationText(instance) {
if (!instance.hasLocation())
return { translate: 'construct.commands.instanceinfo.noLocation' };
const { dimensionId, location } = instance.getLocation();
return { translate: 'construct.commands.instanceinfo.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] };
}
getEnabledText(instance) {
return { translate: 'construct.commands.instanceinfo.enabled', with: [String(instance.isEnabled())] };
}
getLayerText(instance) {
return { translate: 'construct.commands.instanceinfo.layer', with: [String(instance.getLayer()), String(instance.getMaxLayer())] };
}
getVerifierText(instance) {
const verifier = instance.options.verifier;
return { translate: 'construct.commands.instanceinfo.verifier', with: [String(verifier.isEnabled)] };
}
getSizeText(instance) {
const bounds = instance.getBounds();
return { translate: 'construct.commands.instanceinfo.size', with: [bounds.max.toString(), Vector.volume(bounds.min, bounds.max).toString()] };
}
}
export const instanceInfoCommand = new InstanceInfoCommand();
@@ -0,0 +1,52 @@
import { CommandPermissionLevel, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class InstancesCommand extends Command {
constructor() {
super({
name: 'instances',
description: 'construct.commands.instances',
permissionLevel: CommandPermissionLevel.Any,
callback: (origin) => this.run(origin)
});
}
run(origin) {
const names = structureCollection.getInstanceNames();
if (names.length === 0)
return { status: CustomCommandStatus.Success, message: 'construct.commands.instances.empty' };
const rawtext = [
{ translate: 'construct.commands.instances.header', with: [String(names.length)] },
{ text: '\n' }
];
for (const name of names) {
const instance = structureCollection.get(name);
const status = this.formatStatus(instance);
rawtext.push({
translate: 'construct.commands.instances.row',
with: { rawtext: [{ text: name }, { text: instance.getStructureId() }, status] }
});
rawtext.push({ text: '\n' });
}
origin.sendMessage({ rawtext });
return { status: CustomCommandStatus.Success };
}
formatStatus(instance) {
const statusMessage = { rawtext: [] };
if (instance.isEnabled())
statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.enabled' });
else
statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.disabled' });
if (instance.hasLocation()) {
const { dimensionId, location } = instance.getLocation();
statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.location', with: [location.toString(), dimensionId.replace('minecraft:', '')] });
} else {
statusMessage.rawtext.push({ translate: 'construct.commands.instances.row.nolocation' });
}
return statusMessage;
}
}
export const instancesCommand = new InstancesCommand();
+32
View File
@@ -0,0 +1,32 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class LayerCommand extends Command {
constructor() {
super({
name: 'layer',
description: 'construct.commands.layer',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'layer', type: CustomCommandParamType.Integer }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, layer) => this.run(origin, instanceName, layer)
});
}
run(origin, instanceName, layer) {
const instance = structureCollection.get(instanceName);
const max = instance.getMaxLayer();
if (layer < 0 || layer > max) {
origin.sendMessage({ translate: 'construct.commands.layer.outOfBounds', with: [String(layer), instanceName, String(max)] });
return void 0;
}
instance.setLayer(layer);
origin.sendMessage({ translate: 'construct.commands.layer.success', with: [instanceName, String(layer)] });
return { status: CustomCommandStatus.Success };
}
}
export const layerCommand = new LayerCommand();
@@ -0,0 +1,60 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { NotAPlayerError } from '../classes/Errors/NotAPlayerError';
export class MaterialsCommand extends Command {
constructor() {
super({
name: 'materials',
description: 'construct.commands.materials',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
optionalParameters: [
{ name: 'missing', type: CustomCommandParamType.Boolean }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, missing) => this.run(origin, instanceName, missing)
});
}
run(origin, instanceName, missing) {
const instance = structureCollection.get(instanceName);
const onlyMissing = missing === true;
if (onlyMissing)
this.assertIsPlayer(origin);
const headerKey = onlyMissing ? 'construct.commands.materials.headerMissing' : 'construct.commands.materials.headerAll';
const rawtext = [
{ translate: headerKey, with: [instanceName] },
{ text: '\n' }
];
const list = this.getMaterialList(origin, instance, onlyMissing);
if (!list.rawtext || list.rawtext.length === 0)
rawtext.push({ translate: 'construct.commands.materials.empty' });
else
rawtext.push(list);
origin.sendMessage({ rawtext });
return { status: CustomCommandStatus.Success };
}
assertIsPlayer(origin) {
if (!(origin instanceof PlayerCommandOrigin))
throw new NotAPlayerError();
}
getMaterialList(origin, instance, onlyMissing) {
const materials = instance.getActiveMaterials();
let container;
if (onlyMissing) {
const player = origin.getSource();
const inventoryComponent = player?.getComponent(EntityComponentTypes.Inventory);
container = inventoryComponent?.container;
}
const materialsMap = onlyMissing ? materials.getMaterialsDifference(container) : void 0;
return materials.formatString(materialsMap);
}
}
export const materialsCommand = new MaterialsCommand();
+47
View File
@@ -0,0 +1,47 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, world } from '@minecraft/server';
import { Vector } from '../lib/Vector';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
export class MoveCommand extends Command {
constructor() {
super({
name: 'move',
description: 'construct.commands.move',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
optionalParameters: [
{ name: 'dimensionId', type: CustomCommandParamType.Enum }, // Enum defined in PlaceCommand.js
{ name: 'location', type: CustomCommandParamType.Location }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, dimensionId, location) => this.run(origin, instanceName, dimensionId, location)
});
}
run(origin, instanceName, dimensionId, location) {
const instance = structureCollection.get(instanceName);
if (dimensionId === void 0 || location === void 0) {
if (!(origin instanceof PlayerCommandOrigin))
return { status: CustomCommandStatus.Failure, message: 'construct.commands.move.locationRequired' };
const player = origin.getSource();
location = player.location;
dimensionId = player.dimension.id;
}
this.assertDimensionExists(dimensionId);
const flooredLocation = Vector.from(location).floor();
system.run(() => {
instance.move(dimensionId, flooredLocation);
origin.sendMessage({ translate: 'construct.commands.move.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] });
});
return { status: CustomCommandStatus.Success };
}
assertDimensionExists(dimensionId) {
return world.getDimension(dimensionId) !== void 0;
}
}
export const moveCommand = new MoveCommand();
@@ -0,0 +1,26 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class NextLayerCommand extends Command {
constructor() {
super({
name: 'nextlayer',
description: 'construct.commands.nextlayer',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
instance.increaseLayer();
origin.sendMessage({ translate: 'construct.commands.nextlayer.success', with: [instanceName, String(instance.getLayer())] });
return { status: CustomCommandStatus.Success };
}
}
export const nextLayerCommand = new NextLayerCommand();
+39
View File
@@ -0,0 +1,39 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, DimensionTypes, system, world } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { InstanceExistsError } from '../classes/Errors/InstanceExistsError';
import { Vector } from '../lib/Vector';
export class PlaceCommand extends Command {
constructor() {
super({
name: 'place',
description: 'construct.commands.place',
enums: [ { name: 'dimensionId', values: Object.values(DimensionTypes.getAll().map(d => d.typeId)) } ],
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'dimensionId', type: CustomCommandParamType.Enum },
{ name: 'location', type: CustomCommandParamType.Location }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, dimensionId, location) => this.run(origin, instanceName, dimensionId, location)
});
}
run(origin, instanceName, dimensionId, location) {
const instance = structureCollection.get(instanceName);
const flooredLocation = Vector.from(location).floor();
this.assertDimensionExists(dimensionId);
system.run(() => {
instance.place(dimensionId, flooredLocation);
origin.sendMessage({ translate: 'construct.commands.place.success', with: [instanceName, flooredLocation.toString(), dimensionId.replace('minecraft:', '')] });
});
return { status: CustomCommandStatus.Success };
}
assertDimensionExists(dimensionId) {
return world.getDimension(dimensionId) !== void 0;
}
}
export const placeCommand = new PlaceCommand();
@@ -0,0 +1,26 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class PrevLayerCommand extends Command {
constructor() {
super({
name: 'prevlayer',
description: 'construct.commands.prevlayer',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
instance.decreaseLayer();
origin.sendMessage({ translate: 'construct.commands.prevlayer.success', with: [instanceName, String(instance.getLayer())] });
return { status: CustomCommandStatus.Success };
}
}
export const prevLayerCommand = new PrevLayerCommand();
@@ -0,0 +1,31 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class RenameCommand extends Command {
constructor() {
super({
name: 'rename',
description: 'construct.commands.rename',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'newName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, newName) => this.run(origin, instanceName, newName)
});
}
run(origin, instanceName, newName) {
const instance = structureCollection.get(instanceName);
if (structureCollection.has(newName)) {
origin.sendMessage({ translate: 'construct.error.instanceExists', with: [newName] });
return void 0;
}
structureCollection.rename(instanceName, newName);
origin.sendMessage({ translate: 'construct.commands.rename.success', with: [instanceName, newName] });
return { status: CustomCommandStatus.Success };
}
}
export const renameCommand = new RenameCommand();
+42
View File
@@ -0,0 +1,42 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system, TicksPerSecond } from '@minecraft/server';
import { InstanceFormBuilder } from '../classes/Instance/InstanceFormBuilder';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { StructureVerifier } from '../classes/Verifier/StructureVerifier';
import { StructureStatistics } from '../classes/Structure/StructureStatistics';
export class StatsCommand extends Command {
constructor() {
super({
name: 'stats',
description: 'construct.commands.stats',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
if (this.structureVerifier)
return { status: CustomCommandStatus.Failure, error: 'construct.commands.stats.alreadyRunning' };
system.run(async () => {
origin.sendMessage(await this.getStatsMessage(instance));
});
return { status: CustomCommandStatus.Success };
}
async getStatsMessage(instance) {
const verifierOptions = { isEnabled: true, particleLifetime: 1*TicksPerSecond, isStandalone: true };
this.structureVerifier = new StructureVerifier(instance, verifierOptions);
const verification = await this.structureVerifier.verifyStructure(true);
const statistics = new StructureStatistics(instance, verification);
const statsMessage = statistics.getMessage();
this.structureVerifier = void 0;
return statsMessage;
}
}
export const statsCommand = new StatsCommand();
+37
View File
@@ -0,0 +1,37 @@
import { Command } from '../classes/Commands/Command';
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, EntityComponentTypes, EquipmentSlot, system } from '@minecraft/server';
import { PlayerCommandOrigin } from '../classes/Commands/PlayerCommandOrigin';
import { MENU_ITEM } from '../consts';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class TagCommand extends Command {
constructor() {
super({
name: 'tag',
description: 'construct.commands.tag',
allowedSources: [PlayerCommandOrigin],
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName) => this.run(origin, instanceName)
});
}
run(origin, instanceName) {
const instance = structureCollection.get(instanceName);
const player = origin.getSource();
const equipment = player.getComponent(EntityComponentTypes.Equippable);
const itemStack = equipment?.getEquipment(EquipmentSlot.Mainhand);
if (itemStack?.typeId !== MENU_ITEM)
return { status: CustomCommandStatus.Failure, message: 'construct.commands.tag.notHoldingItem' };
system.run(() => {
itemStack.nameTag = instanceName;
equipment.setEquipment(EquipmentSlot.Mainhand, itemStack);
origin.sendMessage({ translate: 'construct.commands.tag.success', with: [instanceName] });
});
return { status: CustomCommandStatus.Success };
}
}
export const tagCommand = new TagCommand();
@@ -0,0 +1,34 @@
import { CommandPermissionLevel, CustomCommandParamType, CustomCommandStatus, system } from '@minecraft/server';
import { Command } from '../classes/Commands/Command';
import { structureCollection } from '../classes/Structure/StructureCollection';
export class VerifierCommand extends Command {
constructor() {
super({
name: 'verifier',
description: 'construct.commands.verifier',
mandatoryParameters: [
{ name: 'instanceName', type: CustomCommandParamType.String },
{ name: 'state', type: CustomCommandParamType.Boolean }
],
permissionLevel: CommandPermissionLevel.Any,
callback: (origin, instanceName, state) => this.run(origin, instanceName, state)
});
}
run(origin, instanceName, state) {
const instance = structureCollection.get(instanceName);
if (state)
instance.setVerifierEnabled(true);
else
instance.setVerifierEnabled(false);
this.sendFeedback(origin, instanceName, state);
return { status: CustomCommandStatus.Success };
}
sendFeedback(origin, instanceName, state) {
origin.sendMessage({ translate: state ? 'construct.commands.verifier.enabled' : 'construct.commands.verifier.disabled', with: [instanceName] });
}
}
export const verifierCommand = new VerifierCommand();
-52
View File
@@ -1,52 +0,0 @@
import { world, system, EntityComponentTypes, ItemStack, CommandPermissionLevel, CustomCommandStatus, Player } from '@minecraft/server';
import { MenuForm } from '../classes/MenuForm';
import { structureCollection } from '../classes/Structure/StructureCollection'
import { Builders } from '../classes/Builder/Builders';
export const MENU_ITEM = 'construct:menu';
system.beforeEvents.startup.subscribe((event) => {
const command = {
name: 'construct:construct',
description: 'construct.commands.construct',
permissionLevel: CommandPermissionLevel.Any,
cheatsRequired: false
};
event.customCommandRegistry.registerCommand(command, givePlayerConstructItem);
});
function givePlayerConstructItem(origin) {
const player = origin.sourceEntity;
if (player instanceof Player === false)
return { status: CustomCommandStatus.Failure, message: 'construct.commands.construct.denyorigin' };
system.run(() => {
const givenItemStack = player.getComponent(EntityComponentTypes.Inventory)?.container?.addItem(new ItemStack(MENU_ITEM));
if (givenItemStack)
player.sendMessage({ translate: 'construct.commands.construct.fail' });
else
player.sendMessage({ translate: 'construct.commands.construct.success' });
});
return { status: CustomCommandStatus.Success };
}
world.beforeEvents.itemUse.subscribe((event) => {
if (!event.source || event.itemStack?.typeId !== MENU_ITEM) return;
event.cancel = true;
const builder = Builders.get(event.source.id);
system.run(() => {
if (builder.isFlexibleInstanceMoving())
return;
openMenu(event.source, event);
});
});
function openMenu(player, event = void 0) {
const options = { jumpToInstance: true }
if (event) {
const instanceNames = structureCollection.getInstanceNames();
const instanceName = event.itemStack?.nameTag;
if (instanceNames.includes(instanceName))
options.instanceName = instanceName;
}
new MenuForm(player, options);
}
+2
View File
@@ -0,0 +1,2 @@
export const PACK_IDENTIFIER = 'construct';
export const MENU_ITEM = 'construct:menu';
+914
View File
@@ -0,0 +1,914 @@
/** @license MIT
* AddonAPIKit - Copyright (c) 2026 ForestOfLight
* MCBE-IPC - Copyright (c) 2026 OmniacDev
* See LICENSE for details.
*/
// src/MCBE-IPC/ipc.js
import { ScriptEventSource, system } from "@minecraft/server";
var UTIL;
(function(UTIL2) {
function generate_id() {
const r = Math.random() * 4294967296 >>> 0;
return r.toString(16).padStart(8, "0").toUpperCase();
}
UTIL2.generate_id = generate_id;
})(UTIL || (UTIL = {}));
var PROTO;
(function(PROTO2) {
class Buffer {
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;
}
reserve(amount) {
this.ensure_capacity(amount);
const end = this.end;
this._length += amount;
return end;
}
consume(amount) {
if (amount > this._length)
throw new Error("not enough bytes");
const front = this.front;
this._length -= amount;
this._offset += amount;
return front;
}
write(input) {
if (typeof input === "number") {
const offset = this.reserve(1);
this._buffer[offset] = input;
} else {
const offset = this.reserve(input.length);
this._buffer.set(input, offset);
}
}
read(amount) {
if (amount === void 0) {
const offset = this.consume(1);
return this._buffer[offset];
} else {
const offset = this.consume(amount);
return this._buffer.slice(offset, offset + amount);
}
}
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 buffer = new Buffer();
buffer._buffer = array;
buffer._length = array.length;
buffer._offset = 0;
buffer._data_view = new DataView(array.buffer);
return buffer;
}
to_uint8array() {
return this._buffer.subarray(this._offset, this.end);
}
}
PROTO2.Buffer = Buffer;
let MIPS;
(function(MIPS2) {
function is_valid(str) {
return str.startsWith("(0x") && str.endsWith(")");
}
MIPS2.is_valid = is_valid;
function* serialize(stream) {
const uint8array = stream.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;
}
MIPS2.serialize = serialize;
function* deserialize(str) {
if (is_valid(str)) {
const buffer = new Buffer();
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];
buffer.write(parseInt(hex, 16));
yield;
}
return buffer;
}
return new Buffer();
}
MIPS2.deserialize = deserialize;
})(MIPS = PROTO2.MIPS || (PROTO2.MIPS = {}));
PROTO2.Void = {
*serialize() {
},
*deserialize() {
}
};
PROTO2.Null = {
*serialize() {
},
*deserialize() {
return null;
}
};
PROTO2.Undefined = {
*serialize() {
},
*deserialize() {
return void 0;
}
};
PROTO2.Int8 = {
*serialize(value, stream) {
stream.data_view.setInt8(stream.reserve(1), value);
},
*deserialize(stream) {
return stream.data_view.getInt8(stream.consume(1));
}
};
PROTO2.Int16 = {
*serialize(value, stream) {
stream.data_view.setInt16(stream.reserve(2), value);
},
*deserialize(stream) {
return stream.data_view.getInt16(stream.consume(2));
}
};
PROTO2.Int32 = {
*serialize(value, stream) {
stream.data_view.setInt32(stream.reserve(4), value);
},
*deserialize(stream) {
return stream.data_view.getInt32(stream.consume(4));
}
};
PROTO2.UInt8 = {
*serialize(value, stream) {
stream.data_view.setUint8(stream.reserve(1), value);
},
*deserialize(stream) {
return stream.data_view.getUint8(stream.consume(1));
}
};
PROTO2.UInt16 = {
*serialize(value, stream) {
stream.data_view.setUint16(stream.reserve(2), value);
},
*deserialize(stream) {
return stream.data_view.getUint16(stream.consume(2));
}
};
PROTO2.UInt32 = {
*serialize(value, stream) {
stream.data_view.setUint32(stream.reserve(4), value);
},
*deserialize(stream) {
return stream.data_view.getUint32(stream.consume(4));
}
};
PROTO2.UVarInt32 = {
*serialize(value, stream) {
value >>>= 0;
while (value >= 128) {
stream.write(value & 127 | 128);
value >>>= 7;
yield;
}
stream.write(value);
},
*deserialize(stream) {
let value = 0;
for (let size = 0; size < 5; size++) {
const byte = stream.read();
value |= (byte & 127) << size * 7;
yield;
if ((byte & 128) == 0)
break;
}
return value >>> 0;
}
};
PROTO2.VarInt32 = {
*serialize(value, stream) {
const zigzag = value << 1 ^ value >> 31;
yield* PROTO2.UVarInt32.serialize(zigzag, stream);
},
*deserialize(stream) {
const zigzag = yield* PROTO2.UVarInt32.deserialize(stream);
return zigzag >>> 1 ^ -(zigzag & 1);
}
};
PROTO2.Float32 = {
*serialize(value, stream) {
stream.data_view.setFloat32(stream.reserve(4), value);
},
*deserialize(stream) {
return stream.data_view.getFloat32(stream.consume(4));
}
};
PROTO2.Float64 = {
*serialize(value, stream) {
stream.data_view.setFloat64(stream.reserve(8), value);
},
*deserialize(stream) {
return stream.data_view.getFloat64(stream.consume(8));
}
};
PROTO2.String = {
*serialize(value, stream) {
yield* PROTO2.UVarInt32.serialize(value.length, stream);
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
yield* PROTO2.UVarInt32.serialize(code, stream);
}
},
*deserialize(stream) {
const length = yield* PROTO2.UVarInt32.deserialize(stream);
let value = "";
for (let i = 0; i < length; i++) {
const code = yield* PROTO2.UVarInt32.deserialize(stream);
value += globalThis.String.fromCharCode(code);
}
return value;
}
};
PROTO2.Boolean = {
*serialize(value, stream) {
stream.write(value ? 1 : 0);
},
*deserialize(stream) {
return stream.read() !== 0;
}
};
PROTO2.UInt8Array = {
*serialize(value, stream) {
yield* PROTO2.UVarInt32.serialize(value.length, stream);
stream.write(value);
},
*deserialize(stream) {
const length = yield* PROTO2.UVarInt32.deserialize(stream);
return stream.read(length);
}
};
PROTO2.Date = {
*serialize(value, stream) {
yield* PROTO2.Float64.serialize(value.getTime(), stream);
},
*deserialize(stream) {
return new globalThis.Date(yield* PROTO2.Float64.deserialize(stream));
}
};
function Object2(s) {
return {
*serialize(value, stream) {
for (const key in s) {
yield* s[key].serialize(value[key], stream);
}
},
*deserialize(stream) {
const result = {};
for (const key in s) {
result[key] = yield* s[key].deserialize(stream);
}
return result;
}
};
}
PROTO2.Object = Object2;
function Array2(s) {
return {
*serialize(value, stream) {
yield* PROTO2.UVarInt32.serialize(value.length, stream);
for (const item of value) {
yield* s.serialize(item, stream);
}
},
*deserialize(stream) {
const result = [];
const length = yield* PROTO2.UVarInt32.deserialize(stream);
for (let i = 0; i < length; i++) {
result[i] = yield* s.deserialize(stream);
}
return result;
}
};
}
PROTO2.Array = Array2;
function Tuple(...s) {
return {
*serialize(value, stream) {
for (let i = 0; i < s.length; i++) {
yield* s[i].serialize(value[i], stream);
}
},
*deserialize(stream) {
const result = [];
for (let i = 0; i < s.length; i++) {
result[i] = yield* s[i].deserialize(stream);
}
return result;
}
};
}
PROTO2.Tuple = Tuple;
function Optional(s) {
return {
*serialize(value, stream) {
const def = value !== void 0;
yield* PROTO2.Boolean.serialize(def, stream);
if (def)
yield* s.serialize(value, stream);
},
*deserialize(stream) {
const def = yield* PROTO2.Boolean.deserialize(stream);
if (def)
return yield* s.deserialize(stream);
return void 0;
}
};
}
PROTO2.Optional = Optional;
function Map2(kS, vS) {
return {
*serialize(value, stream) {
yield* PROTO2.UVarInt32.serialize(value.size, stream);
for (const [k, v] of value) {
yield* kS.serialize(k, stream);
yield* vS.serialize(v, stream);
}
},
*deserialize(stream) {
const size = yield* PROTO2.UVarInt32.deserialize(stream);
const result = new globalThis.Map();
for (let i = 0; i < size; i++) {
const k = yield* kS.deserialize(stream);
const v = yield* vS.deserialize(stream);
result.set(k, v);
}
return result;
}
};
}
PROTO2.Map = Map2;
function Set(s) {
return {
*serialize(set, stream) {
yield* PROTO2.UVarInt32.serialize(set.size, stream);
for (const v of set) {
yield* s.serialize(v, stream);
}
},
*deserialize(stream) {
const size = yield* PROTO2.UVarInt32.deserialize(stream);
const result = new globalThis.Set();
for (let i = 0; i < size; i++) {
const v = yield* s.deserialize(stream);
result.add(v);
}
return result;
}
};
}
PROTO2.Set = Set;
function Cached(s, depth = 16) {
const cache = new globalThis.Map();
return {
*serialize(value, stream) {
const hit = cache.get(value);
if (hit !== void 0) {
stream.write(hit);
cache.delete(value);
cache.set(value, hit);
} else {
const buffer = new PROTO2.Buffer();
yield* s.serialize(value, buffer);
const bytes = buffer.to_uint8array();
stream.write(bytes);
cache.set(value, bytes);
if (cache.size > depth) {
const first = cache.keys().next().value;
cache.delete(first);
}
}
},
*deserialize(stream) {
return yield* s.deserialize(stream);
}
};
}
PROTO2.Cached = Cached;
})(PROTO || (PROTO = {}));
var NET;
(function(NET2) {
const Endpoint = PROTO.String;
const Meta = PROTO.Object({
guid: PROTO.String,
signature: PROTO.String
});
const Header = PROTO.Object({
meta: Meta,
index: PROTO.UVarInt32,
final: PROTO.Boolean
});
const LISTENERS = /* @__PURE__ */ new Map();
NET2.SIGNATURE = "mcbe-ipc:v3";
NET2.FRAG_MAX = 2048;
function* serialize(buffer, max_size = Infinity) {
const uint8array = buffer.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 <= 127 ? 1 : char_code <= 2047 ? 2 : char_code <= 65535 ? 3 : 4;
const char_size = char_code > 255 ? utf16_size : 2;
if (acc_size + char_size > max_size) {
result.push(acc_str);
acc_str = "";
acc_size = 0;
}
if (char_code > 255) {
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;
}
NET2.serialize = serialize;
function* deserialize(strings) {
const buffer = new PROTO.Buffer();
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 <= 255) {
const hex = str[j] + str[++j];
const hex_code = parseInt(hex, 16);
buffer.write(hex_code & 255);
buffer.write(hex_code >> 8);
} else {
buffer.write(char_code & 255);
buffer.write(char_code >> 8);
}
yield;
}
yield;
}
return buffer;
}
NET2.deserialize = deserialize;
system.afterEvents.scriptEventReceive.subscribe((event) => {
system.runJob((function* () {
if (event.sourceType !== ScriptEventSource.Server)
return;
const [serialized_endpoint, serialized_header] = event.id.split(":");
if (!PROTO.MIPS.is_valid(serialized_endpoint))
return;
const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint);
const endpoint = yield* Endpoint.deserialize(endpoint_stream);
const listeners = LISTENERS.get(endpoint);
if (listeners !== void 0 && PROTO.MIPS.is_valid(serialized_header)) {
const header_stream = yield* PROTO.MIPS.deserialize(serialized_header);
const header = yield* Header.deserialize(header_stream);
for (const listener of [...listeners]) {
try {
yield* listener(header, event.message);
} catch (e) {
console.error(`[MCBE-IPC] listener error while handling packet on "${endpoint}":`, e);
}
}
}
})());
});
function register(endpoint, listener) {
let listeners = LISTENERS.get(endpoint);
if (listeners === void 0) {
listeners = new Array();
LISTENERS.set(endpoint, listeners);
}
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx !== -1)
listeners.splice(idx, 1);
if (listeners.length === 0) {
LISTENERS.delete(endpoint);
}
};
}
function* emit(endpoint, serializer, value, options) {
const guid = options?.metaOverride?.guid ?? UTIL.generate_id();
const signature = options?.metaOverride?.signature ?? NET2.SIGNATURE;
const endpoint_stream = new PROTO.Buffer();
yield* Endpoint.serialize(endpoint, endpoint_stream);
const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream);
const packet_stream = new PROTO.Buffer();
yield* serializer.serialize(value, packet_stream);
const serialized_packets = yield* serialize(packet_stream, NET2.FRAG_MAX);
for (let i = 0; i < serialized_packets.length; i++) {
const serialized_packet = serialized_packets[i];
const header = {
meta: { guid, signature },
index: i,
final: i === serialized_packets.length - 1
};
const header_stream = new PROTO.Buffer();
yield* Header.serialize(header, header_stream);
const serialized_header = yield* PROTO.MIPS.serialize(header_stream);
system.sendScriptEvent(`${serialized_endpoint}:${serialized_header}`, serialized_packet);
}
}
NET2.emit = emit;
function listen(endpoint, deserializer, callback, options) {
const buffer = /* @__PURE__ */ new Map();
const listener = function* (header, fragment) {
let packet = buffer.get(header.meta.guid);
if (packet === void 0) {
if (options?.filter?.(header.meta) === false)
return;
packet = { size: -1, fragments: [], received: 0 };
buffer.set(header.meta.guid, packet);
}
if (header.final) {
packet.size = header.index + 1;
}
if (packet.fragments[header.index] === void 0) {
packet.fragments[header.index] = fragment;
packet.received++;
} else {
throw new Error(`received duplicate fragment ${header.index} for packet ${header.meta.guid}`);
}
if (packet.size !== -1 && packet.size === packet.received) {
const stream = yield* deserialize(packet.fragments);
const value = yield* deserializer.deserialize(stream);
yield* callback(value, header.meta);
buffer.delete(header.meta.guid);
}
};
return register(endpoint, listener);
}
NET2.listen = listen;
})(NET || (NET = {}));
var IPC;
(function(IPC2) {
function send(channel, serializer, value) {
system.runJob(NET.emit(`ipc:${channel}:send`, serializer, value));
}
IPC2.send = send;
function invoke(channel, serializer, value, deserializer) {
const id = UTIL.generate_id();
return new Promise((resolve) => {
const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value2, meta) {
if (meta.signature.includes(`+correlation`) && meta.guid !== id)
return;
resolve(value2);
terminate();
}, {
filter: (meta) => !meta.signature.includes(`+correlation`) || meta.guid === id
});
system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value, {
metaOverride: {
guid: id,
signature: `${NET.SIGNATURE}+correlation`
}
}));
});
}
IPC2.invoke = invoke;
function on(channel, deserializer, listener) {
return NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
listener(value);
});
}
IPC2.on = on;
function once(channel, deserializer, listener) {
const terminate = NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
listener(value);
terminate();
});
return terminate;
}
IPC2.once = once;
function handle(channel, deserializer, serializer, listener) {
return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value, meta) {
const result = listener(value);
yield* NET.emit(`ipc:${channel}:handle`, serializer, result, {
metaOverride: meta.signature.includes(`+correlation`) ? {
guid: meta.guid,
signature: `${NET.SIGNATURE}+correlation`
} : void 0
});
});
}
IPC2.handle = handle;
})(IPC || (IPC = {}));
// src/Errors/APIErrorEnum.js
var APIErrorEnum = Object.freeze({
Unknown: 0,
Success: 1,
Caller: 2,
Server: 3
});
// src/Errors/APICallerError.js
var APICallerError = class extends Error {
constructor(error) {
const message = error.name + ": " + error.message;
super(message);
this.thrownError = error;
this.errorCode = APIErrorEnum.Caller;
this.name = "APICallerError";
}
};
// src/Errors/APIServerError.js
var APIServerError = class extends Error {
constructor(error) {
const message = error.name + ": " + error.message;
super(message);
this.thrownError = error;
this.errorCode = APIErrorEnum.Server;
this.name = "APIServerError";
}
};
// src/Errors/APIVersionMismatchError.js
var APIVersionMismatchError = class extends Error {
constructor(serverApiVersion, callerApiVersion) {
super(`API version numbers do not match (${callerApiVersion} != ${serverApiVersion}). Please use API version ${serverApiVersion}.`);
this.name = "APIVersionMismatchError";
}
};
// src/APIModels.js
var VoidModel = PROTO.Optional(PROTO.Void);
var ErrorModel = PROTO.Optional(PROTO.Object({
code: PROTO.Int8,
name: PROTO.Optional(PROTO.String),
message: PROTO.Optional(PROTO.String)
}));
var ReturnModelShell = {
apiVersion: PROTO.String,
data: void 0,
error: ErrorModel
};
var CallModelShell = {
apiVersion: PROTO.String,
parameterMap: void 0
};
var EndpointModel = PROTO.String;
var EndpointsModel = PROTO.Array(EndpointModel);
// src/APIController.js
var APIController = class {
#endpoints = {};
get endpoints() {
return this.#endpoints;
}
addEndpoint(endpoint, callback, parameterModel, returnModel) {
this.#endpoints[endpoint] = { callback, parameterModel, returnModel };
}
};
// src/EndpointsController.js
var EndpointsController = class extends APIController {
#api;
constructor(api) {
super();
this.#api = api;
this.addEndpoint("endpoints", this.getEndpoints, VoidModel, EndpointsModel);
this.addEndpoint("endpoints:has", this.hasEndpoint, EndpointModel, PROTO.Boolean);
}
getEndpoints() {
return this.#api.endpoints;
}
hasEndpoint(endpoint) {
return this.getEndpoints().includes(endpoint);
}
};
// src/AddonAPIServer.js
var AddonAPIServer = class {
#name;
#version;
#allEndpoints = [];
constructor(name, version) {
this.#name = name;
this.#version = version;
const endpointsController = new EndpointsController(this);
this.setupController(endpointsController);
}
get name() {
return this.#name;
}
get version() {
return this.#version;
}
get endpointBase() {
return this.#name + ":";
}
get endpoints() {
return this.#allEndpoints;
}
setupController(apiController) {
for (const [endpoint, features] of Object.entries(apiController.endpoints)) {
const { callback, parameterModel, returnModel } = features;
const boundCallback = callback.bind(apiController);
this.#setupEndpoint(endpoint, boundCallback, parameterModel, returnModel);
}
}
#setupEndpoint(endpoint, callback, parameterModel, returnDataModel) {
const callPacketModel = this.#resolveCallModel(parameterModel);
const returnPacketModel = this.#resolveReturnModel(returnDataModel);
const endpointPath = this.endpointBase + endpoint;
IPC.handle(endpointPath, callPacketModel, returnPacketModel, (callPacket) => {
console.info(`Received at ${endpointPath}: ${JSON.stringify(callPacket)}`);
const apiVersion = callPacket.apiVersion;
const parameters = this.#resolveParameters(callPacket);
const returnPacket = this.#handleCallback(apiVersion, callback, parameters);
console.info(`Replying ${JSON.stringify(returnPacket)}`);
return returnPacket;
});
this.#allEndpoints.push(endpointPath);
}
#handleCallback(apiVersion, callback, parameters) {
try {
this.#assertVersionsMatch(apiVersion);
const returnValue = callback(...parameters);
return this.#bundleReturnPacket({ code: APIErrorEnum.Success }, returnValue);
} catch (error) {
if (error instanceof APICallerError) {
const errorPacket2 = this.#resolveErrorPacket(error);
return this.#bundleReturnPacket(errorPacket2);
}
console.error(error, error.stack);
const apiError = new APIServerError(error);
const errorPacket = this.#resolveErrorPacket(apiError);
return this.#bundleReturnPacket(errorPacket);
}
}
#assertVersionsMatch(versionToCheck) {
if (versionToCheck !== this.version) {
const apiVersionMismatchError = new APIVersionMismatchError(this.version, versionToCheck);
throw new APICallerError(apiVersionMismatchError);
}
}
#resolveCallModel(parameterModel) {
return PROTO.Object({ ...CallModelShell, parameterMap: parameterModel });
}
#resolveReturnModel(returnDataModel) {
return PROTO.Object({ ...ReturnModelShell, data: PROTO.Optional(returnDataModel) });
}
#resolveParameters(callPacket) {
if (callPacket.parameterMap === void 0)
return [];
return Object.values(callPacket.parameterMap);
}
#bundleReturnPacket(errorPacket, returnValue = void 0) {
return {
apiVersion: this.version,
data: returnValue,
error: errorPacket
};
}
#resolveErrorPacket(error) {
return {
code: error.errorCode,
name: error.thrownError.name,
message: error.thrownError.message
};
}
};
// src/AddonAPICaller.js
import { system as system2 } from "@minecraft/server";
// src/Errors/APIEndpointNotFoundError.js
var APIEndpointNotFoundError = class extends Error {
constructor(endpoint) {
super(`Endpoint "${endpoint}" was not found.`);
this.name = "APIEndpointNotFoundError";
}
};
// src/AddonAPICaller.js
var AddonAPICaller = class {
#name;
#version;
#validEndpointCache = [];
constructor(name, version) {
this.#name = name;
this.#version = version;
}
async call(endpoint, parameterMapModel, parameterMap, returnDataModel) {
await this.#tryPopulateEndpointCache(endpoint);
if (this.#endpointExists(endpoint))
return this.#callDirect(endpoint, parameterMapModel, parameterMap, returnDataModel);
else
throw new APIEndpointNotFoundError(endpoint);
}
async #tryPopulateEndpointCache(endpoint) {
if (this.#validEndpointCache.length === 0) {
const endpointBase = endpoint.split(":")[0];
const validEndpoints = await this.#callDirect(endpointBase + ":endpoints", VoidModel, void 0, EndpointsModel);
this.#validEndpointCache.push(...validEndpoints);
}
}
async #callDirect(endpoint, parameterMapModel, parameterMap, returnDataModel) {
const parameterPacket = { apiVersion: this.#version, parameterMap };
const parameterModel = this.#resolveParameterModel(parameterMapModel);
const returnModel = this.#resolveReturnModel(returnDataModel);
console.info(`Sending to ${endpoint}: ${JSON.stringify(parameterPacket)}`);
const returnPacket = await IPC.invoke(endpoint, parameterModel, parameterPacket, returnModel);
console.info(`Received from ${endpoint}: ${JSON.stringify(returnPacket)}`);
return this.#unwrapReturnPacket(returnPacket);
}
#endpointExists(endpoint) {
return this.#validEndpointCache.includes(endpoint);
}
#resolveParameterModel(parameterMapModel) {
return PROTO.Object({ ...CallModelShell, parameterMap: parameterMapModel });
}
#resolveReturnModel(returnDataModel) {
return PROTO.Object({ ...ReturnModelShell, data: PROTO.Optional(returnDataModel) });
}
#unwrapReturnPacket(packet) {
const { data, error } = packet;
if (error.code === APIErrorEnum.Success)
return data;
else
this.#throwAPIError(packet.error);
}
#throwAPIError(errorData) {
switch (errorData.code) {
case APIErrorEnum.Caller:
throw new APICallerError(errorData);
case APIErrorEnum.Server:
throw new APIServerError(errorData);
case APIErrorEnum.Unknown:
default:
throw new Error(errorData.message);
}
}
};
export {
APICallerError,
APIController,
APIEndpointNotFoundError,
APIErrorEnum,
AddonAPICaller,
AddonAPIServer,
PROTO,
VoidModel
};
/**
* @license
* MIT License
*
* Copyright (c) 2026 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.
*/
+23 -1
View File
@@ -6,8 +6,30 @@ import './options/easyPlace';
import './options/fastEasyPlace'; import './options/fastEasyPlace';
import './options/materialGrabber'; import './options/materialGrabber';
// Menu item handler
import './classes/MenuItemHandler';
// Commands // Commands
import './commands/construct'; import './commands/ConstructCommand';
import './commands/CreateCommand';
import './commands/DeleteCommand';
import './commands/RenameCommand';
import './commands/InstancesCommand';
import './commands/PlaceCommand';
import './commands/MoveCommand';
import './commands/EnableCommand';
import './commands/LayerCommand';
import './commands/NextLayerCommand';
import './commands/PrevLayerCommand';
import './commands/VerifierCommand';
import './commands/BuilderCommand';
import './commands/InstanceInfoCommand';
import './commands/StatsCommand';
import './commands/MaterialsCommand';
import './commands/TagCommand';
// API
import './API/ConstructAPI';
// Other // Other
import './classes/BlockInfo'; import './classes/BlockInfo';
+5 -5
View File
@@ -1,11 +1,11 @@
{ {
"format_version": 2, "format_version": 2,
"header": { "header": {
"name": "Construct [RP] v1.0.9", "name": "Construct [RP] v1.1.0",
"description": "Survival building addon by §aForestOfLight§r.", "description": "Survival building addon by §aForestOfLight§r.",
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", "uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4",
"version": [1, 0, 9], "version": [1, 1, 0],
"min_engine_version": [1, 26, 20] "min_engine_version": [1, 26, 30]
}, },
"modules": [ "modules": [
{ {
@@ -16,8 +16,8 @@
], ],
"dependencies": [ "dependencies": [
{ {
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", // Construct BP "uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"version": [1, 0, 9] "version": [1, 1, 0]
} }
], ],
"capabilities": [ "capabilities": [
+107 -15
View File
@@ -57,15 +57,6 @@ construct.structure.statistics.stateincorrect=§7Block State Incorrect: §e%s ##
construct.structure.statistics.incorrect=§7Incorrect: §c%s ## Insert string: number of incorrectly placed blocks construct.structure.statistics.incorrect=§7Incorrect: §c%s ## Insert string: number of incorrectly placed blocks
construct.structure.statistics.missing=§7Missing: §3%s ## Insert string: number of missing blocks construct.structure.statistics.missing=§7Missing: §3%s ## Insert string: number of missing blocks
## Structure Block Info Display
construct.blockinfo.header=Structure:
construct.blockinfo.none=§7None
construct.blockinfo.nosupply= §c[No Supply] ## When the targeted block is not in the player's inventory
construct.blockinfo.unknown=§7Unknown
construct.blockinfo.waterlogged=§7isWaterlogged: §3true
construct.mainmenu.instance.exists=§cInstance '%s' already exists. Try again with a new name. ## Insert string: instance name
construct.mainmenu.instance.notfound=§cStructure ID '%s' not found. If you're looking for a structure that you put in the structures folder, please restart your world and try again. ## Insert string: structure name
construct.mainmenu.title=§l§2Construct ## This is the name of the pack. construct.mainmenu.title=§l§2Construct ## This is the name of the pack.
construct.mainmenu.selectinstance=Select an instance: construct.mainmenu.selectinstance=Select an instance:
construct.mainmenu.howto=How to Add/Remove Structures construct.mainmenu.howto=How to Add/Remove Structures
@@ -84,13 +75,14 @@ construct.mainmenu.howto.add.mcstructure=§7To transfer structures §fbetween wo
construct.mainmenu.howto.remove.header=§cHow to Remove Structures: construct.mainmenu.howto.remove.header=§cHow to Remove Structures:
construct.mainmenu.howto.remove.body=§7- Use the §f/structure delete§7 command to remove a structure from the world. construct.mainmenu.howto.remove.body=§7- Use the §f/structure delete§7 command to remove a structure from the world.
## Commands ## Structure Block Info Display
construct.commands.construct=Gives you the Construct item. Use it to open the Construct menu. construct.blockinfo.header=Structure:
construct.commands.construct.denyorigin=This command can only be used by players. construct.blockinfo.none=§7None
construct.commands.construct.fail=§cFailed to give you the Construct item. construct.blockinfo.nosupply= §c[No Supply] ## When the targeted block is not in the player's inventory
construct.commands.construct.success=§aYou recieved the Construct item! Use it to open the Construct menu. construct.blockinfo.unknown=§7Unknown
construct.blockinfo.waterlogged=§7isWaterlogged: §3true
## Options ## Builder Options
construct.option.enabled= is now enabled! construct.option.enabled= is now enabled!
construct.option.disabled= is now disabled. construct.option.disabled= is now disabled.
construct.option.easyplace.name=Easy Place construct.option.easyplace.name=Easy Place
@@ -106,3 +98,103 @@ construct.option.materialgrabber.howto=Interact with inventories using the Mater
construct.option.materialgrabber.grabbed.zero=§7Grabbed 0 items. construct.option.materialgrabber.grabbed.zero=§7Grabbed 0 items.
construct.option.materialgrabber.grabbed.one=§aGrabbed 1 item. construct.option.materialgrabber.grabbed.one=§aGrabbed 1 item.
construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert string: number of items transferred to the player construct.option.materialgrabber.grabbed.many=§aGrabbed %s items. ## Insert string: number of items transferred to the player
## Commands
construct.commands.construct=Gives you the Construct item. Use it to open the Construct menu.
construct.commands.construct.fail=§cFailed to give you the Construct item.
construct.commands.construct.success=§aYou recieved the Construct item! Use it to open the Construct menu.
## construct:create
construct.commands.create=Create a new Construct instance bound to a structure.
construct.commands.create.success=§aCreated instance "%1" bound to structure "%2". ## Insert strings: instance name, structure name
construct.commands.create.unknownStructure=§cNo structure with id "%1" found in the world. ## Insert string: structure name
## construct:delete
construct.commands.delete=Permanently delete a Construct instance.
construct.commands.delete.success=§aDeleted instance "%1". ## Insert string: instance name
## construct:rename
construct.commands.rename=Rename an existing Construct instance.
construct.commands.rename.success=§aRenamed instance "%1" to "%2". ## Insert strings: old instance name, new instance name
## construct:instances
construct.commands.instances=List all registered Construct instances.
construct.commands.instances.empty=§7No instances registered.
construct.commands.instances.header=§aRegistered instances (%1):§r ## Insert string: number of registered instances
construct.commands.instances.row=§a%1§r §8[§r%2§8]§r §7%3§r ## Insert strings: instance name, enabled/disabled, location (if available)
construct.commands.instances.row.enabled=enabled
construct.commands.instances.row.disabled=disabled
construct.commands.instances.row.location= at %1 in %2 ## Insert strings: coordinates, dimension
construct.commands.instances.row.nolocation= (no location)
## construct:place
construct.commands.place=Place a Construct instance in the world.
construct.commands.place.success=§aPlaced instance "%1" at %2 in %3. ## Insert strings: instance name, coordinates, dimension
## construct:move
construct.commands.move=Reposition a Construct instance without toggling its enabled state.
construct.commands.move.success=§aMoved instance "%1" to %2 in %3. ## Insert strings: instance name, coordinates, dimension
construct.commands.move.locationRequired=§cMust provide a dimension and coordinates when not running as a player.
## construct:enable
construct.commands.enable=Enable or disable a Construct instance.
construct.commands.enable.true=§aEnabled instance "%1". ## Insert string: instance name
construct.commands.enable.false=§aDisabled instance "%1". ## Insert string: instance name
construct.commands.error.noLocation=§cInstance "%1" has no saved location.
## construct:layer
construct.commands.layer=Set the active layer of a Construct instance (Use 0 for the whole structure).
construct.commands.layer.success=§aSet instance "%1" layer to %2. ## Insert strings: instance name, layer number
construct.commands.layer.outOfBounds=§cLayer %1 is out of bounds for instance "%2" (max %3). ## Insert strings: layer number, instance name, max layer number
## construct:nextlayer
construct.commands.nextlayer=Step a Construct instance one layer up.
construct.commands.nextlayer.success=§aInstance "%1" advanced to layer %2. ## Insert strings: instance name, layer number
## construct:prevlayer
construct.commands.prevlayer=Step a Construct instance one layer down.
construct.commands.prevlayer.success=§aInstance "%1" stepped back to layer %2. ## Insert strings: instance name, layer number
## construct:verifier
construct.commands.verifier=Toggle block validation overlay for a Construct instance.
construct.commands.verifier.enabled=§aEnabled verifier for instance "%1". ## Insert string: instance name
construct.commands.verifier.disabled=§aDisabled verifier for instance "%1". ## Insert string: instance name
## construct:builder
construct.commands.builder=Toggle your Construct builder options.
construct.commands.builder.success=§aSet option "%1" to %2. ## Insert strings: option name, option value
construct.commands.builder.unknownOption=§cUnknown option "%1". ## Insert string: option name
## construct:instanceinfo
construct.commands.instanceinfo=Print Construct instance details to chat.
construct.commands.instanceinfo.header=§a=== Instance Info for "%1" ===§r ## Insert string: instance name
construct.commands.instanceinfo.structure=§7Structure:§r %1 ## Insert string: structure name
construct.commands.instanceinfo.location=§7Location:§r %1 in %2 ## Insert strings: coordinates, dimension
construct.commands.instanceinfo.noLocation=§7Location:§r (none) ## Used when the instance has no saved location
construct.commands.instanceinfo.enabled=§7Enabled:§r %1 ## Insert string: true/false indicating whether the instance is enabled
construct.commands.instanceinfo.layer=§7Layer:§r %1 / %2 ## Insert strings: current layer, max layer
construct.commands.instanceinfo.verifier=§7Verifier running:§r %1 ## Insert string: true/false indicating whether the verifier is running
construct.commands.instanceinfo.size=§7Size:§r %1 (%2 blocks) ## Insert strings: size (x, y, z), total block volume count
## construct:stats
construct.commands.stats=Run the Construct structure verifier and print statistics.
construct.commands.stats.alreadyRunning=§cA verification is already in progress. Please wait until it finishes.
## construct:materials
construct.commands.materials=Print the material list for a Construct instance.
construct.commands.materials.headerAll=§aMaterials for "%1":§r ## Insert string: instance name
construct.commands.materials.headerMissing=§aMissing materials for "%1":§r ## Insert string: instance name
construct.commands.materials.empty=§7(no materials)
## construct:tag
construct.commands.tag=Rename the held Construct item to an instance name for quick-open.
construct.commands.tag.success=§aTagged held Construct item with instance "%1". ## Insert string: instance name
construct.commands.tag.notHoldingItem=§cYou must be holding a Construct item.
## Errors
construct.error.invalidCommandSource=§cThis command cannot be run from this source.
construct.error.instanceNotFound=§cInstance "%1" not found. ## Insert string: instance name
construct.error.instanceExists=§cInstance "%1" already exists. Try again with a new name. ## Insert string: instance name
construct.error.structureNotFound=§cStructure ID "%1" not found. If you're looking for a structure that you put in the structures folder, please restart your world and try again. ## Insert string: structure name
construct.error.dimensionNotFound=§cDimension "%1" not found. ## Insert string: dimension name
construct.error.notAPlayer=§cThis command can only be used by players.