Initial commit

This commit is contained in:
Forest
2025-03-10 19:24:48 -07:00
committed by GitHub
Unverified
commit f52f82b867
13 changed files with 1344 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
.vscode/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 ForestOfLight
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+73
View File
@@ -0,0 +1,73 @@
<div align="center">
<a href="./pack_icon.png">
<img src="./pack_icon.png" alt="Canopy Extension Example Icon" width="100" height="100">
</a>
<p><b>Canopy Extension Example</b></p>
[![Codacy Quality](https://app.codacy.com/project/badge/Grade/10040a714ad84a2f912d4dae9d3f6e57)](https://app.codacy.com/gh/ForestOfLight/Canopy-Extension-Example/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[![Minecraft - Version](https://img.shields.io/badge/Minecraft-v1.21.60_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs)
[![GitHub License](https://img.shields.io/github/license/forestoflight/canopy-extension-example)](LICENSE)
[![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm)
</div>
---
Extensions?! In a behavior pack?! Yep! This repo contains an example pack designed to teach you how to create your own extensions for **Canopy**. All extensions must be added to the same world as **Canopy** (the dependencies in the `manifest.json` should ensure that).
## How this example pack functions ingame
What **Canopy** handles:
**`./canopy <rule> [true/false]`**
**Canopy** will automatically include the names of your rules in the `./canopy` command.
**`./help`**
**Canopy** will automatically include a Rules and a Commands help page for your extension.
![Help Page](./example_help.png)
What the extension handles:
**`./example [message]`**
This command sends a message to the chat. It also has a default message if no message is provided. (Alias: `./ex`)
**`commandExample`**
This rule will enable the `./example` command.
**`exampleRule`**
This rule will enable a message in chat when you push a button.
## How to create an extension
1. Clone this repo.
```sh
git clone https://github.com/ForestOfLight/canopy-extension-example.git
```
2. Remove all files that are not the `pack_icon.png`, the `manifest.json` file, or in the `scripts/` folder.
3. Rename the folder to your extension's name.
```sh
mv canopy-extension-example your-extension-name
```
4. Open the folder in your favorite code editor.
5. Edit the `manifest.json` file to match your extension's information.
6. Replace the `pack_icon.png` with your extension's icon.
7. Edit the `main.js` file to add your extension's functionality.
> [!TIP]
> Most information about how to code a **Canopy Extension** can be found in the `main.js` file.
## How to use an extension
1. Add the extension pack to the same world as the **Canopy** RP & BP.
2. Profit.
**Canopy** will automatically detect the extension and load it!
## How does this work?
**Canopy** and its extensions all communicate using scriptevents! The vanilla `/scriptevent` command allows for sending strings to Behavior Packs and the `ScriptEventCommandMessage` event allows for recieving them. **Canopy** uses [MCBE-IPC](https://https://github.com/OmniacDev/MCBE-IPC), a library that streamlines the process of sending and recieving scriptevents.
**Canopy Extensions** use these to communicate with **Canopy** when new rules and commands are added. **Canopy** uses these as well to get rule data, set rule data, and call command callbacks from extensions.
## Issues & Suggestions
If you have any issues or suggestions, please open an issue on this repo. Additionally, if you're interested in contributing to the project, feel free to open a pull request!
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+40
View File
@@ -0,0 +1,40 @@
{
"format_version": 2,
"header": {
"name": "Canopy Extension Example",
"description": "Example extension for §l§aCanopy§r by §aForestOfLight§r.",
"uuid": "e4e844d5-f45b-4623-82a7-534d44bb4c6f",
"min_engine_version": [1, 21, 60],
"version": [1, 0, 0]
},
"modules": [
{
"description": "Behavior Pack Module",
"type": "data",
"uuid": "1ce9eab8-0d4a-4e40-a899-575791a1b40d",
"version": [1, 0, 0]
},
{
"description": "Gametest Module",
"type": "script",
"language": "javascript",
"entry": "scripts/main.js",
"uuid": "839a937e-b4f9-4484-84a4-0db984822f25",
"version": [1, 0, 0]
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.18.0-beta"
},
{
"uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP
"version": [1, 0, 2]
},
{
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
"version": [1, 3, 8]
}
]
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+166
View File
@@ -0,0 +1,166 @@
/**
* @license
* MIT License
*
* Copyright (c) 2024 ForestOfLight
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { world } from '@minecraft/server';
import IPC from '../../lib/ipc/ipc';
import Command from './Command';
import Rule from './Rule';
import { CommandCallbackRequest, CommandPrefixRequest, Ready, RegisterCommand, RegisterExtension, RegisterRule, RuleValueRequest, RuleValueSet, CommandPrefixResponse, RuleValueResponse } from './extension.ipc';
class CanopyExtension {
name;
version;
author;
description;
#commands = {};
#rules = {};
#isRegistrationReady = false;
constructor({ name = 'Unnamed', version = '1.0.0', author = 'Unknown', description = { text: '' } }) {
this.id = this.#makeID(name);
this.name = name;
this.version = version;
this.author = author;
this.description = description;
this.#registerExtension();
this.#setupCommandPrefix();
this.#handleCommandCallbacks();
this.#handleRuleValueRequests();
this.#handleRuleValueSetters();
}
addCommand(command) {
if (!(command instanceof Command))
throw new Error('Command must be an instance of Command.');
this.#commands[command.getName()] = command;
if (this.#isRegistrationReady)
this.#registerCommand(command);
}
addRule(rule) {
if (!(rule instanceof Rule))
throw new Error('Rule must be an instance of Rule.');
this.#rules[rule.getID()] = rule;
if (this.#isRegistrationReady)
this.#registerRule(rule);
}
getRuleValue(ruleID) {
return this.#rules[ruleID].getValue();
}
#makeID(name) {
if (typeof name !== 'string')
throw new Error(`[${name}] Could not register extension. Extension name must be a string.`);
const id = name.toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/ /g, '_');
if (id.length === 0)
throw new Error(`[${name}] Could not register extension. Extension name must contain at least one alphanumeric character.`);
return id;
}
#registerExtension() {
IPC.once('canopyExtension:ready', Ready, () => {
IPC.send('canopyExtension:registerExtension', RegisterExtension, {
name: this.name,
version: this.version,
author: this.author,
description: this.description
});
});
IPC.once(`canopyExtension:${this.id}:ready`, Ready, () => {
this.#isRegistrationReady = true;
for (const rule of Object.values(this.#rules))
this.#registerRule(rule);
for (const command of Object.values(this.#commands))
this.#registerCommand(command);
});
}
#registerCommand(command) {
IPC.send(`canopyExtension:${this.id}:registerCommand`, RegisterCommand, {
name: command.getName(),
description: command.getDescription(),
usage: command.getUsage(),
callback: false,
args: command.getArgs(),
contingentRules: command.getContingentRules(),
adminOnly: command.isAdminOnly(),
helpEntries: command.getHelpEntries(),
helpHidden: command.isHelpHidden(),
extensionName: this.name
});
}
#handleCommandCallbacks() {
IPC.on(`canopyExtension:${this.id}:commandCallbackRequest`, CommandCallbackRequest, (cmdData) => {
if (cmdData.senderName === undefined)
return;
const sender = world.getPlayers({ name: cmdData.senderName })[0];
if (!sender)
throw new Error(`Sender ${cmdData.senderName} of ${cmdData.commandName} not found.`);
const parsedArgs = JSON.parse(cmdData.args);
this.#commands[cmdData.commandName].runCallback(sender, parsedArgs);
});
}
#registerRule(rule) {
IPC.send(`canopyExtension:${this.id}:registerRule`, RegisterRule, {
identifier: rule.getID(),
description: rule.getDescription(),
contingentRules: rule.getContigentRules(),
independentRules: rule.getIndependentRules(),
extensionName: this.name
});
if (rule.getValue() === true)
rule.onEnable();
}
#handleRuleValueRequests() {
IPC.handle(`canopyExtension:${this.id}:ruleValueRequest`, RuleValueRequest, RuleValueResponse, (data) => {
const rule = this.#rules[data.ruleID];
if (!rule)
throw new Error(`Rule ${data.ruleID} not found.`);
const value = rule.getValue();
return { value };
});
}
#handleRuleValueSetters() {
IPC.on(`canopyExtension:${this.id}:ruleValueSet`, RuleValueSet, (data) => {
const rule = this.#rules[data.ruleID];
if (!rule)
throw new Error(`Rule ${data.ruleID} not found.`);
rule.setValue(data.value);
});
}
#setupCommandPrefix() {
const prefix = IPC.invoke(`canopyExtension:commandPrefixRequest`, CommandPrefixRequest, void 0, CommandPrefixResponse).then(result => {
Command.setPrefix(result.prefix);
});
}
}
export { CanopyExtension, Command, Rule };
+103
View File
@@ -0,0 +1,103 @@
/**
* @license
* MIT License
*
* Copyright (c) 2024 ForestOfLight
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
class Command {
#name;
#description;
#usage;
#callback;
#args;
#contingentRules;
#adminOnly;
#helpEntries;
#helpHidden;
static #prefix = '';
constructor({ name, description = '', usage, callback, args = [], contingentRules = [], adminOnly = false, helpEntries = [], helpHidden = false }) {
this.#name = name;
this.#description = description;
this.#usage = usage;
this.#callback = callback;
this.#args = args;
this.#contingentRules = contingentRules;
this.#adminOnly = adminOnly;
this.#helpEntries = helpEntries;
this.#helpHidden = helpHidden;
}
getName() {
return this.#name;
}
getDescription() {
return this.#description;
}
getUsage() {
return this.#usage;
}
getCallback() {
return this.#callback;
}
getArgs() {
return this.#args;
}
getContingentRules() {
return this.#contingentRules;
}
isAdminOnly() {
return this.#adminOnly;
}
getHelpEntries() {
return this.#helpEntries;
}
isHelpHidden() {
return this.#helpHidden;
}
runCallback(sender, args) {
this.#callback(sender, args);
}
sendUsage(sender) {
sender.sendMessage(`§cUsage: ${Command.#prefix}${this.#usage}`);
}
static setPrefix(prefix) {
Command.#prefix = prefix;
}
static getPrefix() {
return Command.#prefix;
}
}
export default Command;
+76
View File
@@ -0,0 +1,76 @@
/**
* @license
* MIT License
*
* Copyright (c) 2024 ForestOfLight
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { world } from '@minecraft/server';
class Rule {
#identifier;
#description;
#contingentRules;
#independentRules;
constructor({ identifier, description, contingentRules = [], independentRules = [], onEnableCallback = () => {}, onDisableCallback = () => {} }) {
this.#identifier = identifier;
this.#description = description;
this.#contingentRules = contingentRules;
this.#independentRules = independentRules;
this.onEnable = onEnableCallback;
this.onDisable = onDisableCallback;
}
getID() {
return this.#identifier;
}
getDescription() {
return this.#description;
}
getContigentRules() {
return this.#contingentRules;
}
getIndependentRules() {
return this.#independentRules;
}
getValue() {
const value = world.getDynamicProperty(this.#identifier);
if (String(value) === 'true')
return true;
if (['false', 'undefined'].includes(String(value)))
return false;
throw new Error(`Rule ${this.#identifier} has an invalid value: ${value} (${typeof value})`);
}
setValue(value) {
if (value === true)
this.onEnable();
else
this.onDisable();
world.setDynamicProperty(this.#identifier, value);
}
}
export default Rule;
+70
View File
@@ -0,0 +1,70 @@
/* eslint-disable new-cap */
import { PROTO } from '../ipc/ipc'
const description = PROTO.Object({
text: PROTO.Optional(PROTO.String),
translate: PROTO.Optional(PROTO.String),
with: PROTO.Optional(PROTO.Array(PROTO.String))
});
export const Ready = PROTO.Void;
export const RegisterExtension = PROTO.Object({
name: PROTO.String,
version: PROTO.String,
author: PROTO.String,
description: description,
isEndstone: PROTO.Boolean
});
export const RegisterCommand = PROTO.Object({
name: PROTO.String,
description: description,
usage: PROTO.String,
callback: PROTO.Optional(PROTO.Undefined),
args: PROTO.Optional(PROTO.Array(PROTO.Object({
type: PROTO.String,
name: PROTO.String
}))),
contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
adminOnly: PROTO.Optional(PROTO.Boolean),
helpEntries: PROTO.Optional(PROTO.Array(PROTO.Object({
usage: PROTO.String,
description: description
}))),
helpHidden: PROTO.Optional(PROTO.Boolean),
extensionName: PROTO.Optional(PROTO.String)
});
export const RegisterRule = PROTO.Object({
identifier: PROTO.String,
description: description,
contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
independentRules: PROTO.Optional(PROTO.Array(PROTO.String)),
extensionName: PROTO.Optional(PROTO.String)
});
export const RuleValueRequest = PROTO.Object({
ruleID: PROTO.String
});
export const RuleValueResponse = PROTO.Object({
value: PROTO.Boolean
});
export const RuleValueSet = PROTO.Object({
ruleID: PROTO.String,
value: PROTO.Boolean
});
export const CommandCallbackRequest = PROTO.Object({
commandName: PROTO.String,
senderName: PROTO.Optional(PROTO.String),
args: PROTO.String
});
export const CommandPrefixRequest = PROTO.Void;
export const CommandPrefixResponse = PROTO.Object({
prefix: PROTO.String
});
+103
View File
@@ -0,0 +1,103 @@
/**
* @license
* MIT License
*
* Copyright (c) 2025 OmniacDev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export declare namespace PROTO {
interface Serializable<T> {
serialize(value: T, stream: ByteQueue): Generator<void, void, void>;
deserialize(stream: ByteQueue): Generator<void, T, void>;
}
class ByteQueue {
private _buffer;
private _data_view;
private _length;
private _offset;
get end(): number;
get front(): number;
get data_view(): DataView;
constructor(size?: number);
write(...values: number[]): void;
read(amount?: number): number[];
ensure_capacity(size: number): void;
static from_uint8array(array: Uint8Array): ByteQueue;
to_uint8array(): Uint8Array;
}
namespace MIPS {
function serialize(byte_queue: PROTO.ByteQueue): Generator<void, string, void>;
function deserialize(str: string): Generator<void, PROTO.ByteQueue, void>;
}
const Void: PROTO.Serializable<void>;
const Null: PROTO.Serializable<null>;
const Undefined: PROTO.Serializable<undefined>;
const Int8: PROTO.Serializable<number>;
const Int16: PROTO.Serializable<number>;
const Int32: PROTO.Serializable<number>;
const UInt8: PROTO.Serializable<number>;
const UInt16: PROTO.Serializable<number>;
const UInt32: PROTO.Serializable<number>;
const UVarInt32: PROTO.Serializable<number>;
const Float32: PROTO.Serializable<number>;
const Float64: PROTO.Serializable<number>;
const String: PROTO.Serializable<string>;
const Boolean: PROTO.Serializable<boolean>;
const UInt8Array: PROTO.Serializable<Uint8Array>;
const Date: PROTO.Serializable<Date>;
function Object<T extends object>(obj: {
[K in keyof T]: PROTO.Serializable<T[K]>;
}): PROTO.Serializable<T>;
function Array<T>(value: PROTO.Serializable<T>): PROTO.Serializable<T[]>;
function Tuple<T extends any[]>(...values: {
[K in keyof T]: PROTO.Serializable<T[K]>;
}): PROTO.Serializable<T>;
function Optional<T>(value: PROTO.Serializable<T>): PROTO.Serializable<T | undefined>;
function Map<K, V>(key: PROTO.Serializable<K>, value: PROTO.Serializable<V>): PROTO.Serializable<Map<K, V>>;
function Set<V>(value: PROTO.Serializable<V>): PROTO.Serializable<Set<V>>;
type Endpoint = string;
type Header = {
guid: string;
encoding: string;
index: number;
final: boolean;
};
const Endpoint: PROTO.Serializable<Endpoint>;
const Header: PROTO.Serializable<Header>;
}
export declare namespace NET {
function serialize(byte_queue: PROTO.ByteQueue, max_size?: number): Generator<void, string[], void>;
function deserialize(strings: string[]): Generator<void, PROTO.ByteQueue, void>;
function emit<S extends PROTO.Serializable<T>, T>(endpoint: string, serializer: S & PROTO.Serializable<T>, value: T): Generator<void, void, void>;
function listen<T, S extends PROTO.Serializable<T>>(endpoint: string, serializer: S & PROTO.Serializable<T>, callback: (value: T) => Generator<void, void, void>): () => void;
}
export declare namespace IPC {
/** Sends a message with `args` to `channel` */
function send<S extends PROTO.Serializable<T>, T>(channel: string, serializer: S & PROTO.Serializable<T>, value: T): void;
/** Sends an `invoke` message through IPC, and expects a result asynchronously. */
function invoke<TS extends PROTO.Serializable<T>, T, RS extends PROTO.Serializable<R>, R>(channel: string, serializer: TS & PROTO.Serializable<T>, value: T, deserializer: RS & PROTO.Serializable<R>): Promise<R>;
/** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */
function on<S extends PROTO.Serializable<T>, T>(channel: string, deserializer: S & PROTO.Serializable<T>, listener: (value: T) => void): () => void;
/** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */
function once<S extends PROTO.Serializable<T>, T>(channel: string, deserializer: S & PROTO.Serializable<T>, listener: (value: T) => void): () => void;
/** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */
function handle<TS extends PROTO.Serializable<T>, T, RS extends PROTO.Serializable<R>, R>(channel: string, deserializer: TS & PROTO.Serializable<T>, serializer: RS & PROTO.Serializable<R>, listener: (value: T) => R): () => void;
}
export default IPC;
+593
View File
@@ -0,0 +1,593 @@
/**
* @license
* MIT License
*
* Copyright (c) 2025 OmniacDev
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { ScriptEventSource, system, world } from '@minecraft/server';
export var PROTO;
(function (PROTO) {
class ByteQueue {
get end() {
return this._length + this._offset;
}
get front() {
return this._offset;
}
get data_view() {
return this._data_view;
}
constructor(size = 256) {
this._buffer = new Uint8Array(size);
this._data_view = new DataView(this._buffer.buffer);
this._length = 0;
this._offset = 0;
}
write(...values) {
this.ensure_capacity(values.length);
this._buffer.set(values, this.end);
this._length += values.length;
}
read(amount = 1) {
if (this._length > 0) {
const max_amount = amount > this._length ? this._length : amount;
const values = this._buffer.subarray(this._offset, this._offset + max_amount);
this._length -= max_amount;
this._offset += max_amount;
return globalThis.Array.from(values);
}
return [];
}
ensure_capacity(size) {
if (this.end + size > this._buffer.length) {
const larger_buffer = new Uint8Array((this.end + size) * 2);
larger_buffer.set(this._buffer.subarray(this._offset, this.end), 0);
this._buffer = larger_buffer;
this._offset = 0;
this._data_view = new DataView(this._buffer.buffer);
}
}
static from_uint8array(array) {
const byte_queue = new ByteQueue();
byte_queue._buffer = array;
byte_queue._length = array.length;
byte_queue._offset = 0;
byte_queue._data_view = new DataView(array.buffer);
return byte_queue;
}
to_uint8array() {
return this._buffer.subarray(this._offset, this.end);
}
}
PROTO.ByteQueue = ByteQueue;
let MIPS;
(function (MIPS) {
function* serialize(byte_queue) {
const uint8array = byte_queue.to_uint8array();
let str = '(0x';
for (let i = 0; i < uint8array.length; i++) {
const hex = uint8array[i].toString(16).padStart(2, '0').toUpperCase();
str += hex;
yield;
}
str += ')';
return str;
}
MIPS.serialize = serialize;
function* deserialize(str) {
if (str.startsWith('(0x') && str.endsWith(')')) {
const result = [];
const hex_str = str.slice(3, str.length - 1);
for (let i = 0; i < hex_str.length; i++) {
const hex = hex_str[i] + hex_str[++i];
result.push(parseInt(hex, 16));
yield;
}
return ByteQueue.from_uint8array(new Uint8Array(result));
}
return new ByteQueue();
}
MIPS.deserialize = deserialize;
})(MIPS = PROTO.MIPS || (PROTO.MIPS = {}));
PROTO.Void = {
*serialize() { },
*deserialize() { }
};
PROTO.Null = {
*serialize() { },
*deserialize() {
return null;
}
};
PROTO.Undefined = {
*serialize() { },
*deserialize() {
return undefined;
}
};
PROTO.Int8 = {
*serialize(value, stream) {
const length = 1;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setInt8(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getInt8(stream.front);
stream.read(1);
return value;
}
};
PROTO.Int16 = {
*serialize(value, stream) {
const length = 2;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setInt16(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getInt16(stream.front);
stream.read(2);
return value;
}
};
PROTO.Int32 = {
*serialize(value, stream) {
const length = 4;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setInt32(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getInt32(stream.front);
stream.read(4);
return value;
}
};
PROTO.UInt8 = {
*serialize(value, stream) {
const length = 1;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setUint8(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getUint8(stream.front);
stream.read(1);
return value;
}
};
PROTO.UInt16 = {
*serialize(value, stream) {
const length = 2;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setUint16(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getUint16(stream.front);
stream.read(2);
return value;
}
};
PROTO.UInt32 = {
*serialize(value, stream) {
const length = 4;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setUint32(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getUint32(stream.front);
stream.read(4);
return value;
}
};
PROTO.UVarInt32 = {
*serialize(value, stream) {
while (value >= 0x80) {
stream.write((value & 0x7f) | 0x80);
value >>= 7;
yield;
}
stream.write(value);
},
*deserialize(stream) {
let value = 0;
let size = 0;
let byte;
do {
byte = stream.read()[0];
value |= (byte & 0x7f) << (size * 7);
size += 1;
yield;
} while ((byte & 0x80) !== 0 && size < 10);
return value;
}
};
PROTO.Float32 = {
*serialize(value, stream) {
const length = 4;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setFloat32(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getFloat32(stream.front);
stream.read(4);
return value;
}
};
PROTO.Float64 = {
*serialize(value, stream) {
const length = 8;
stream.write(...globalThis.Array(length).fill(0));
stream.data_view.setFloat64(stream.end - length, value);
},
*deserialize(stream) {
const value = stream.data_view.getFloat64(stream.front);
stream.read(8);
return value;
}
};
PROTO.String = {
*serialize(value, stream) {
yield* PROTO.UVarInt32.serialize(value.length, stream);
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
yield* PROTO.UVarInt32.serialize(code, stream);
}
},
*deserialize(stream) {
const length = yield* PROTO.UVarInt32.deserialize(stream);
let value = '';
for (let i = 0; i < length; i++) {
const code = yield* PROTO.UVarInt32.deserialize(stream);
value += globalThis.String.fromCharCode(code);
}
return value;
}
};
PROTO.Boolean = {
*serialize(value, stream) {
stream.write(value ? 1 : 0);
},
*deserialize(stream) {
const value = stream.read()[0];
return value === 1;
}
};
PROTO.UInt8Array = {
*serialize(value, stream) {
yield* PROTO.UVarInt32.serialize(value.length, stream);
stream.write(...value);
},
*deserialize(stream) {
const length = yield* PROTO.UVarInt32.deserialize(stream);
return new Uint8Array(stream.read(length));
}
};
PROTO.Date = {
*serialize(value, stream) {
yield* PROTO.Float64.serialize(value.getTime(), stream);
},
*deserialize(stream) {
return new globalThis.Date(yield* PROTO.Float64.deserialize(stream));
}
};
function Object(obj) {
return {
*serialize(value, stream) {
for (const key in obj) {
yield* obj[key].serialize(value[key], stream);
}
},
*deserialize(stream) {
const result = {};
for (const key in obj) {
result[key] = yield* obj[key].deserialize(stream);
}
return result;
}
};
}
PROTO.Object = Object;
function Array(value) {
return {
*serialize(array, stream) {
yield* PROTO.UVarInt32.serialize(array.length, stream);
for (const item of array) {
yield* value.serialize(item, stream);
}
},
*deserialize(stream) {
const result = [];
const length = yield* PROTO.UVarInt32.deserialize(stream);
for (let i = 0; i < length; i++) {
result[i] = yield* value.deserialize(stream);
}
return result;
}
};
}
PROTO.Array = Array;
function Tuple(...values) {
return {
*serialize(tuple, stream) {
for (let i = 0; i < values.length; i++) {
yield* values[i].serialize(tuple[i], stream);
}
},
*deserialize(stream) {
const result = [];
for (let i = 0; i < values.length; i++) {
result[i] = yield* values[i].deserialize(stream);
}
return result;
}
};
}
PROTO.Tuple = Tuple;
function Optional(value) {
return {
*serialize(optional, stream) {
yield* PROTO.Boolean.serialize(optional !== undefined, stream);
if (optional !== undefined) {
yield* value.serialize(optional, stream);
}
},
*deserialize(stream) {
const defined = yield* PROTO.Boolean.deserialize(stream);
if (defined) {
return yield* value.deserialize(stream);
}
return undefined;
}
};
}
PROTO.Optional = Optional;
function Map(key, value) {
return {
*serialize(map, stream) {
yield* PROTO.UVarInt32.serialize(map.size, stream);
for (const [k, v] of map.entries()) {
yield* key.serialize(k, stream);
yield* value.serialize(v, stream);
}
},
*deserialize(stream) {
const size = yield* PROTO.UVarInt32.deserialize(stream);
const result = new globalThis.Map();
for (let i = 0; i < size; i++) {
const k = yield* key.deserialize(stream);
const v = yield* value.deserialize(stream);
result.set(k, v);
}
return result;
}
};
}
PROTO.Map = Map;
function Set(value) {
return {
*serialize(set, stream) {
yield* PROTO.UVarInt32.serialize(set.size, stream);
for (const [_, v] of set.entries()) {
yield* value.serialize(v, stream);
}
},
*deserialize(stream) {
const size = yield* PROTO.UVarInt32.deserialize(stream);
const result = new globalThis.Set();
for (let i = 0; i < size; i++) {
const v = yield* value.deserialize(stream);
result.add(v);
}
return result;
}
};
}
PROTO.Set = Set;
PROTO.Endpoint = PROTO.String;
PROTO.Header = PROTO.Object({
guid: PROTO.String,
encoding: PROTO.String,
index: PROTO.UVarInt32,
final: PROTO.Boolean
});
})(PROTO || (PROTO = {}));
export var NET;
(function (NET) {
const FRAG_MAX = 2048;
const ENCODING = 'mcbe-ipc:v3';
const ENDPOINTS = new Map();
function* serialize(byte_queue, max_size = Infinity) {
const uint8array = byte_queue.to_uint8array();
const result = [];
let acc_str = '';
let acc_size = 0;
for (let i = 0; i < uint8array.length; i++) {
const char_code = uint8array[i] | (uint8array[++i] << 8);
const utf16_size = char_code <= 0x7f ? 1 : char_code <= 0x7ff ? 2 : char_code <= 0xffff ? 3 : 4;
const char_size = char_code > 0xff ? utf16_size : 2;
if (acc_size + char_size > max_size) {
result.push(acc_str);
acc_str = '';
acc_size = 0;
}
if (char_code > 0xff) {
acc_str += String.fromCharCode(char_code);
acc_size += utf16_size;
}
else {
acc_str += char_code.toString(16).padStart(2, '0').toUpperCase();
acc_size += 2;
}
yield;
}
result.push(acc_str);
return result;
}
NET.serialize = serialize;
function* deserialize(strings) {
const result = [];
for (let i = 0; i < strings.length; i++) {
const str = strings[i];
for (let j = 0; j < str.length; j++) {
const char_code = str.charCodeAt(j);
if (char_code <= 0xff) {
const hex = str[j] + str[++j];
const hex_code = parseInt(hex, 16);
result.push(hex_code & 0xff);
result.push(hex_code >> 8);
}
else {
result.push(char_code & 0xff);
result.push(char_code >> 8);
}
yield;
}
yield;
}
return PROTO.ByteQueue.from_uint8array(new Uint8Array(result));
}
NET.deserialize = deserialize;
system.afterEvents.scriptEventReceive.subscribe(event => {
system.runJob((function* () {
const [serialized_endpoint, serialized_header] = event.id.split(':');
const endpoint_stream = yield* PROTO.MIPS.deserialize(serialized_endpoint);
const endpoint = yield* PROTO.Endpoint.deserialize(endpoint_stream);
const listeners = ENDPOINTS.get(endpoint);
if (event.sourceType === ScriptEventSource.Server && listeners) {
const header_stream = yield* PROTO.MIPS.deserialize(serialized_header);
const header = yield* PROTO.Header.deserialize(header_stream);
for (let i = 0; i < listeners.length; i++) {
yield* listeners[i](header, event.message);
}
}
})());
});
function create_listener(endpoint, listener) {
let listeners = ENDPOINTS.get(endpoint);
if (!listeners) {
listeners = new Array();
ENDPOINTS.set(endpoint, listeners);
}
listeners.push(listener);
return () => {
const idx = listeners.indexOf(listener);
if (idx !== -1)
listeners.splice(idx, 1);
if (listeners.length === 0) {
ENDPOINTS.delete(endpoint);
}
};
}
function generate_id() {
const r = (Math.random() * 0x100000000) >>> 0;
return ((r & 0xff).toString(16).padStart(2, '0') +
((r >> 8) & 0xff).toString(16).padStart(2, '0') +
((r >> 16) & 0xff).toString(16).padStart(2, '0') +
((r >> 24) & 0xff).toString(16).padStart(2, '0')).toUpperCase();
}
function* emit(endpoint, serializer, value) {
const guid = generate_id();
const endpoint_stream = new PROTO.ByteQueue();
yield* PROTO.Endpoint.serialize(endpoint, endpoint_stream);
const serialized_endpoint = yield* PROTO.MIPS.serialize(endpoint_stream);
const RUN = function* (header, serialized_packet) {
const header_stream = new PROTO.ByteQueue();
yield* PROTO.Header.serialize(header, header_stream);
const serialized_header = yield* PROTO.MIPS.serialize(header_stream);
world
.getDimension('overworld')
.runCommand(`scriptevent ${serialized_endpoint}:${serialized_header} ${serialized_packet}`);
};
const packet_stream = new PROTO.ByteQueue();
yield* serializer.serialize(value, packet_stream);
const serialized_packets = yield* serialize(packet_stream, FRAG_MAX);
for (let i = 0; i < serialized_packets.length; i++) {
const serialized_packet = serialized_packets[i];
yield* RUN({ guid, encoding: ENCODING, index: i, final: i === serialized_packets.length - 1 }, serialized_packet);
}
}
NET.emit = emit;
function listen(endpoint, serializer, callback) {
const buffer = new Map();
const listener = function* (payload, serialized_packet) {
let fragment = buffer.get(payload.guid);
if (!fragment) {
fragment = { size: -1, serialized_packets: [], data_size: 0 };
buffer.set(payload.guid, fragment);
}
if (payload.final) {
fragment.size = payload.index + 1;
}
fragment.serialized_packets[payload.index] = serialized_packet;
fragment.data_size += payload.index + 1;
if (fragment.size !== -1 && fragment.data_size === (fragment.size * (fragment.size + 1)) / 2) {
const stream = yield* deserialize(fragment.serialized_packets);
const value = yield* serializer.deserialize(stream);
yield* callback(value);
buffer.delete(payload.guid);
}
};
return create_listener(endpoint, listener);
}
NET.listen = listen;
})(NET || (NET = {}));
export var IPC;
(function (IPC) {
/** Sends a message with `args` to `channel` */
function send(channel, serializer, value) {
system.runJob(NET.emit(`ipc:${channel}:send`, serializer, value));
}
IPC.send = send;
/** Sends an `invoke` message through IPC, and expects a result asynchronously. */
function invoke(channel, serializer, value, deserializer) {
system.runJob(NET.emit(`ipc:${channel}:invoke`, serializer, value));
return new Promise(resolve => {
const terminate = NET.listen(`ipc:${channel}:handle`, deserializer, function* (value) {
resolve(value);
terminate();
});
});
}
IPC.invoke = invoke;
/** Listens to `channel`. When a new message arrives, `listener` will be called with `listener(args)`. */
function on(channel, deserializer, listener) {
return NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
listener(value);
});
}
IPC.on = on;
/** Listens to `channel` once. When a new message arrives, `listener` will be called with `listener(args)`, and then removed. */
function once(channel, deserializer, listener) {
const terminate = NET.listen(`ipc:${channel}:send`, deserializer, function* (value) {
listener(value);
terminate();
});
return terminate;
}
IPC.once = once;
/** Adds a handler for an `invoke` IPC. This handler will be called whenever `invoke(channel, ...args)` is called */
function handle(channel, deserializer, serializer, listener) {
return NET.listen(`ipc:${channel}:invoke`, deserializer, function* (value) {
const result = listener(value);
yield* NET.emit(`ipc:${channel}:handle`, serializer, result);
});
}
IPC.handle = handle;
})(IPC || (IPC = {}));
export default IPC;
+97
View File
@@ -0,0 +1,97 @@
import { CanopyExtension, Command, Rule } from './lib/canopy/CanopyExtension';
import { world } from '@minecraft/server';
/**
* Create a new CanopyExtension instance to define your extension.
*/
const extension = new CanopyExtension({
author: 'YourName',
name: 'ExampleExtension',
description: 'Example extension for §l§aCanopy§r!',
version: '1.0.0'
});
// --------------------------------------------
/**
* Adding a new Rule:
*/
const exampleRule = new Rule({
identifier: 'exampleRule', // The name of the rule
description: { text: 'An example rule that prints a message in chat when you hit a button.' }, // Shows up in the help command. Must be a RawMessage type (translatable!).
// Optional:
contingentRules: [], // Rules that will be enabled when this rule is enabled
independentRules: [], // Rules that will be disabled when this rule is enabled
onEnableCallback: () => world.afterEvents.buttonPush.subscribe(onButtonPush), // Function to run when the rule is enabled (also runs when the extension is loaded, if the rule is already enabled)
onDisableCallback: () => world.afterEvents.buttonPush.unsubscribe(onButtonPush) // Function to run when the rule is disabled
});
extension.addRule(exampleRule);
// use the rule to control your code flow
function onButtonPush(event) {
if (!exampleRule.getValue())
return;
if (event.source === undefined) // Always check for undefined entities and players. Simulated players always show up as undefined in events.
return;
event.source.sendMessage('§aYou pushed a button!');
}
// --------------------------------------------
/**
* Making a second new rule which we can use to enable the example command:
* (This is not required to make a new command, but it is helpful to allow admins to disable your commands.)
*/
const commandExampleRule = new Rule({
identifier: 'commandExample',
description: { text: 'Enables the example command.' } // Shows up in the help command. RawMessage type.
});
extension.addRule(commandExampleRule);
/**
* Adding a new Command:
*/
const exampleCommand = new Command({
name: 'example', // The name of the command
description: { text: 'An example command that prints your message in chat.' }, // Shows up in the help command. RawMessage type.
usage: 'example [message]', // The usage of the command that shows up in the help command & when used incorrectly
callback: exampleCommandCallback, // The function to run when the command is executed
// Optional:
args: [
{ type: 'string|number', name: 'message' } // The arguments that the command takes. 'string|number' means it can be either a string or a number
],
contingentRules: ['commandExample'], // Rules that must be true for the command to be enabled
adminOnly: false, // Whether the command can only be run by admins (users with the 'CanopyAdmin' tag)
helpEntries: [ // Additional help entries that show up in the help command
{ usage: `example`, description: { text: `Run the example command with the default message.` } } // Description is a RawMessage type.
],
helpHidden: false // Whether the command should be hidden from the help command.
});
extension.addCommand(exampleCommand);
/**
* Adding a command alias:
* This is essentially just adding a new command that runs the same function as the original command and hiding its help.
*/
const exampleCommandAlias = new Command({
name: 'ex',
description: { text: 'An alias for the example command.' },
usage: 'ex [message]',
callback: exampleCommandCallback,
args: [
{ type: 'string|number', name: 'message' }
],
contingentRules: ['commandExample'],
helpHidden: true
});
extension.addCommand(exampleCommandAlias);
// now you can define the function that will be called when the command is executed
function exampleCommandCallback(sender, args) {
let { message } = args;
if (message === null)
message = 'Hello, world!';
if (!isNaN(parseFloat(message)) && isFinite(message))
message = message.toString();
sender.sendMessage(`§aYou ran the example command with the message: §7${message}`);
}