Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
@@ -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.
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
[](https://app.codacy.com/gh/ForestOfLight/Canopy-Extension-Example/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||||
|
[-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs)
|
||||||
|
[](LICENSE)
|
||||||
|
[](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.
|
||||||
|
|
||||||
|

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