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
+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
});