├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── Canopy [BP] ├── .vscode │ └── launch.json ├── entities │ ├── minecart.json │ ├── probe.json │ ├── rideable.json │ └── tnt.json ├── manifest.json ├── pack_icon.png ├── scripts │ ├── constants.js │ ├── include │ │ ├── data.js │ │ └── utils.js │ ├── lib │ │ ├── MCBE-IPC │ │ │ ├── ipc.d.ts │ │ │ └── ipc.js │ │ ├── SRCItemDatabase │ │ │ ├── DBManager.js │ │ │ ├── Database.d.ts │ │ │ ├── Database.js │ │ │ ├── ItemDatabase.d.ts │ │ │ └── ItemDatabase.js │ │ ├── Vector.js │ │ ├── canopy │ │ │ ├── ArgumentParser.js │ │ │ ├── Canopy.js │ │ │ ├── Command.js │ │ │ ├── Commands.js │ │ │ ├── Extension.js │ │ │ ├── Extensions.js │ │ │ ├── GlobalRule.js │ │ │ ├── InfoDisplayRule.js │ │ │ ├── Rule.js │ │ │ ├── Rules.js │ │ │ ├── extension.ipc.js │ │ │ └── help │ │ │ │ ├── CommandHelpEntry.js │ │ │ │ ├── CommandHelpPage.js │ │ │ │ ├── HelpBook.js │ │ │ │ ├── HelpEntry.js │ │ │ │ ├── HelpPage.js │ │ │ │ ├── InfoDisplayRuleHelpEntry.js │ │ │ │ ├── InfoDisplayRuleHelpPage.js │ │ │ │ ├── RuleHelpEntry.js │ │ │ │ └── RuleHelpPage.js │ │ ├── chestui │ │ │ ├── forms.d.ts │ │ │ ├── forms.js │ │ │ └── typeIds.js │ │ └── mt.js │ ├── main.js │ └── src │ │ ├── classes │ │ ├── BlockRotator.js │ │ ├── CounterChannel.js │ │ ├── CounterChannels.js │ │ ├── DirectionState.js │ │ ├── EntityLog.js │ │ ├── EntityMovementLog.js │ │ ├── EntityTntLog.js │ │ ├── EventTracker.js │ │ ├── GeneratorChannel.js │ │ ├── GeneratorChannels.js │ │ ├── HotbarManager.js │ │ ├── Instaminable.js │ │ ├── InventoryUI.js │ │ ├── ItemCounterChannel.js │ │ ├── ItemCounterChannels.js │ │ ├── Probe.js │ │ ├── ProbeManager.js │ │ ├── Profiler.js │ │ ├── SpawnTracker.js │ │ ├── Warps.js │ │ └── WorldSpawns.js │ │ ├── commands │ │ ├── camera.js │ │ ├── canopy.js │ │ ├── changedimension.js │ │ ├── claimprojectiles.js │ │ ├── cleanup.js │ │ ├── counter.js │ │ ├── data.js │ │ ├── distance.js │ │ ├── entitydensity.js │ │ ├── gamemode.js │ │ ├── generator.js │ │ ├── health.js │ │ ├── help.js │ │ ├── info.js │ │ ├── jump.js │ │ ├── log.js │ │ ├── loop.js │ │ ├── peek.js │ │ ├── pos.js │ │ ├── removeentity.js │ │ ├── resetall.js │ │ ├── resettest.js │ │ ├── scriptevents │ │ │ ├── counter.js │ │ │ ├── generator.js │ │ │ ├── loop.js │ │ │ ├── resettest.js │ │ │ ├── spawn.js │ │ │ └── tick.js │ │ ├── simmap.js │ │ ├── sit.js │ │ ├── spawn.js │ │ ├── tick.js │ │ ├── tntfuse.js │ │ ├── trackevent.js │ │ └── warp.js │ │ ├── events │ │ └── PlayerStartSneakEvent.js │ │ ├── onReload.js │ │ ├── onStart.js │ │ └── rules │ │ ├── allowBubbleColumnPlacement.js │ │ ├── allowPeekInventory.js │ │ ├── armorStandRespawning.js │ │ ├── autoItemPickup.js │ │ ├── cauldronConcreteConversion.js │ │ ├── creativeInstantTame.js │ │ ├── creativeNoTileDrops.js │ │ ├── creativeOneHitKill.js │ │ ├── dupeTnt.js │ │ ├── durabilityNotifier.js │ │ ├── durabilitySwap.js │ │ ├── entityInstantDeath.js │ │ ├── explosionChainReactionOnly.js │ │ ├── explosionNoBlockDamage.js │ │ ├── explosionOff.js │ │ ├── flippinArrows.js │ │ ├── hotbarSwitching.js │ │ ├── infodisplay │ │ ├── Biome.js │ │ ├── ChunkCoords.js │ │ ├── Entities.js │ │ ├── EventTrackers.js │ │ ├── HopperCounterCounts.js │ │ ├── Light.js │ │ ├── LookingAt.js │ │ ├── MoonPhase.js │ │ ├── PeekInventory.js │ │ ├── SessionTime.js │ │ ├── SignalStrength.js │ │ ├── SlimeChunk.js │ │ ├── TimeOfDay.js │ │ ├── WorldDay.js │ │ ├── cardinalFacing.js │ │ ├── coords.js │ │ ├── facing.js │ │ ├── infoDisplay.js │ │ ├── infoDisplayElement.js │ │ ├── simulationMap.js │ │ └── tps.js │ │ ├── instaminableDeepslate.js │ │ ├── instaminableEndstone.js │ │ ├── noWelcomeMessage.js │ │ ├── pistonBedrockBreaking.js │ │ ├── playerSit.js │ │ ├── quickFillContainer.js │ │ ├── refillHand.js │ │ ├── renewableElytra.js │ │ ├── renewableSponge.js │ │ ├── tntPrimeMaxMomentum.js │ │ ├── tntPrimeNoMomentum.js │ │ └── universalChunkLoading.js └── structures │ └── bubble_column.mcstructure ├── Canopy [RP] ├── manifest.json ├── pack_icon.png ├── texts │ ├── de_DE.lang │ ├── en_US.lang │ ├── id_ID.lang │ ├── languages.json │ └── zh_CN.lang ├── textures │ └── ui │ │ ├── d_b.json │ │ ├── d_b.png │ │ ├── d_g.json │ │ ├── d_g.png │ │ ├── item_background.json │ │ └── item_background.png └── ui │ ├── _global_variables.json │ ├── _ui_defs.json │ ├── chest_inventory_system.json │ ├── chest_server_form.json │ ├── furnace_server_form.json │ ├── hud_screen.json │ └── server_form.json ├── LICENSE ├── README.md ├── __mocks__ └── @minecraft │ ├── server-ui.js │ └── server.js ├── __tests__ ├── BP │ └── scripts │ │ ├── include │ │ ├── data.test.js │ │ └── utils.test.js │ │ ├── lib │ │ └── canopy │ │ │ ├── ArgumentParser.test.js │ │ │ ├── Canopy.test.js │ │ │ ├── Command.test.js │ │ │ ├── Commands.test.js │ │ │ ├── Extension.test.js │ │ │ ├── Extensions.test.js │ │ │ ├── GlobalRule.test.js │ │ │ ├── InfoDisplayRule.test.js │ │ │ ├── Rule.test.js │ │ │ ├── Rules.test.js │ │ │ └── help │ │ │ ├── CommandHelpEntry.test.js │ │ │ ├── CommandHelpPage.test.js │ │ │ ├── HelpBook.test.js │ │ │ ├── HelpEntry.test.js │ │ │ ├── HelpPage.test.js │ │ │ ├── InfoDisplayRuleHelpEntry.test.js │ │ │ ├── InfoDisplayRuleHelpPage.test.js │ │ │ ├── RuleHelpEntry.test.js │ │ │ └── RuleHelpPage.test.js │ │ └── src │ │ ├── classes │ │ ├── EntityMovementLog.test.js │ │ ├── EntityTntLog.test.js │ │ ├── InventoryUI.test.js │ │ └── Profiler.test.js │ │ ├── commands │ │ ├── health.test.js │ │ └── log.test.js │ │ ├── events │ │ └── PlayerStartSneakEvent.test.js │ │ └── rules │ │ ├── allowBubbleColumnPlacement.test.js │ │ ├── allowPeekInventory.test.js │ │ ├── dupeTnt.test.js │ │ └── playerSit.test.js └── manifests.test.js ├── amelix-logo.gif ├── canopylogo_banner.jpg ├── eslint.config.js ├── package-lock.json ├── package.json └── vite.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ForestOfLight 4 | patreon: ForestOfLight 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [20.14.0] 18 | fail-fast: true 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | 30 | - name: Install dependencies 31 | run: npm install 32 | 33 | - name: Run ESLint 34 | run: npm run lint 35 | 36 | - name: Run tests with coverage 37 | run: npm test 38 | 39 | - name: Upload coverage report 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: coverage-report 43 | path: coverage 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | coverage/ -------------------------------------------------------------------------------- /Canopy [BP]/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.0", 3 | "configurations": [ 4 | { 5 | "type": "minecraft-js", 6 | "request": "attach", 7 | "name": "Debug with Minecraft", 8 | "mode": "listen", 9 | "targetModuleUuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", 10 | "localRoot": "${workspaceFolder}/scripts", 11 | "port": 19144 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Canopy [BP]/entities/minecart.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:entity": { 4 | "description": { 5 | "identifier": "minecraft:minecart", 6 | "is_spawnable": false, 7 | "is_summonable": true, 8 | "is_experimental": false 9 | }, 10 | "component_groups": { 11 | "canopy:ticking_timer": { 12 | "minecraft:timer": { 13 | "looping": false, 14 | "time": 10, 15 | "time_down_event": { 16 | "event": "canopy:disable_ticking" 17 | } 18 | } 19 | }, 20 | "canopy:ticking": { 21 | "minecraft:tick_world": { 22 | "never_despawn": true, 23 | "radius": 2 24 | } 25 | } 26 | }, 27 | 28 | "components": { 29 | "minecraft:is_stackable": { 30 | }, 31 | "minecraft:type_family": { 32 | "family": [ "minecart", "inanimate" ] 33 | }, 34 | "minecraft:collision_box": { 35 | "width": 0.98, 36 | "height": 0.7 37 | }, 38 | "minecraft:rail_movement": { 39 | }, 40 | "minecraft:rideable": { 41 | "seat_count": 1, 42 | "interact_text": "action.interact.ride.minecart", 43 | "pull_in_entities": true, 44 | 45 | "seats": { 46 | "position": [ 0.0, -0.2, 0.0 ] 47 | } 48 | }, 49 | "minecraft:rail_sensor": { 50 | "eject_on_activate": true 51 | }, 52 | "minecraft:physics": { 53 | }, 54 | "minecraft:pushable": { 55 | "is_pushable": true, 56 | "is_pushable_by_piston": true 57 | }, 58 | "minecraft:conditional_bandwidth_optimization": { 59 | "default_values": { 60 | "max_optimized_distance": 60.0, 61 | "max_dropped_ticks": 20, 62 | "use_motion_prediction_hints": true 63 | }, 64 | "conditional_values": [ 65 | { 66 | "max_optimized_distance": 0.0, 67 | "max_dropped_ticks": 0, 68 | "conditional_values": [ 69 | { "test": "is_moving", "subject": "self", "operator": "==", "value": true} 70 | ] 71 | } 72 | ] 73 | } 74 | }, 75 | "events": { 76 | "canopy:tick_tenSeconds": { 77 | "add": { 78 | "component_groups": [ "canopy:ticking", "canopy:ticking_timer"] 79 | } 80 | }, 81 | "canopy:disable_ticking": { 82 | "remove": { 83 | "component_groups": [ "canopy:ticking", "canopy:ticking_timer" ] 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Canopy [BP]/entities/rideable.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:entity": { 4 | "description": { 5 | "identifier": "canopy:rideable", 6 | "is_summonable": true, 7 | "is_spawnable": false, 8 | "is_experimental": false 9 | }, 10 | 11 | "components": { 12 | "minecraft:physics": { 13 | "has_collision": false, 14 | "has_gravity": false 15 | }, 16 | "minecraft:custom_hit_test": { 17 | "hitboxes": [ 18 | { 19 | "pivot": [0, 100, 0], 20 | "width": 0, 21 | "height": 0 22 | } 23 | ] 24 | }, 25 | "minecraft:damage_sensor": { 26 | "triggers": { 27 | "deals_damage": false 28 | } 29 | }, 30 | "minecraft:collision_box": { 31 | "width": 0.0001, 32 | "height": 0.0001 33 | }, 34 | "minecraft:health": { 35 | "value": 1, 36 | "max": 1, 37 | "min": 1 38 | }, 39 | "minecraft:pushable": { 40 | "is_pushable": false, 41 | "is_pushable_by_piston": false 42 | }, 43 | "minecraft:nameable": { 44 | "allow_name_tag_renaming": false 45 | }, 46 | "minecraft:breathable": { 47 | "breathes_air": true, 48 | "breathes_water": true 49 | }, 50 | "minecraft:rideable": { 51 | "seat_count": 1, 52 | "family_types": ["player"], 53 | "pull_in_entities": false, 54 | "seats": { 55 | "position": [ 0.0, 0.0, 0.0 ] 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Canopy [BP]/entities/tnt.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "1.12.0", 3 | "minecraft:entity": { 4 | "description": { 5 | "identifier": "minecraft:tnt", 6 | "is_spawnable": false, 7 | "is_summonable": true, 8 | "is_experimental": false 9 | }, 10 | 11 | "component_groups": { 12 | "from_explosion": { 13 | "minecraft:explode": { 14 | "fuse_length": { 15 | "range_min": 0.5, 16 | "range_max": 2.0 17 | }, 18 | "fuse_lit": true, 19 | "power": 4, 20 | "causes_fire": false 21 | } 22 | }, 23 | "canopy:fuse": { 24 | "minecraft:explode": { 25 | "fuse_length": 3600, 26 | "fuse_lit": true, 27 | "power": 4, 28 | "causes_fire": false 29 | } 30 | }, 31 | "canopy:explode": { 32 | "minecraft:explode": { 33 | "fuse_length": 0, 34 | "fuse_lit": true, 35 | "power": 4, 36 | "causes_fire": false 37 | } 38 | } 39 | }, 40 | 41 | "components": { 42 | "minecraft:type_family": { 43 | "family": [ "tnt", "inanimate" ] 44 | }, 45 | "minecraft:collision_box": { 46 | "width": 0.98, 47 | "height": 0.98 48 | }, 49 | "minecraft:physics": { 50 | }, 51 | "minecraft:pushable": { 52 | "is_pushable": false, 53 | "is_pushable_by_piston": true 54 | }, 55 | "minecraft:conditional_bandwidth_optimization": { 56 | "default_values": { 57 | "max_optimized_distance": 80.0, 58 | "max_dropped_ticks": 5, 59 | "use_motion_prediction_hints": true 60 | } 61 | } 62 | }, 63 | 64 | "events": { 65 | "from_explosion": { 66 | "add": { 67 | "component_groups": [ 68 | "from_explosion" 69 | ] 70 | } 71 | }, 72 | "canopy:fuse": { 73 | "add": { 74 | "component_groups": [ 75 | "canopy:fuse" 76 | ] 77 | } 78 | }, 79 | "canopy:explode": { 80 | "add": { 81 | "component_groups": [ 82 | "canopy:explode" 83 | ] 84 | } 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /Canopy [BP]/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 2, 3 | "header": { 4 | "name": "Canopy [BP] v1.3.9", 5 | "description": "Technical informatics & features addon by §aForestOfLight§r.", 6 | "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", 7 | "min_engine_version": [1, 21, 70], 8 | "version": [1, 3, 9] 9 | }, 10 | "modules": [ 11 | { 12 | "description": "Behavior Pack Module", 13 | "type": "data", 14 | "uuid": "47787f93-ec3c-4b0c-95fb-55a70e029041", 15 | "version": [1, 0, 0] 16 | }, 17 | { 18 | "description": "Gametest Module", 19 | "type": "script", 20 | "language": "javascript", 21 | "entry": "scripts/main.js", 22 | "uuid": "3d753132-e3c9-4305-a995-eae30b486093", 23 | "version": [1, 0, 0] 24 | } 25 | ], 26 | "dependencies": [ 27 | { 28 | "module_name": "@minecraft/server", 29 | "version": "2.0.0-beta" 30 | }, 31 | { 32 | "module_name": "@minecraft/server-ui", 33 | "version": "2.0.0-beta" 34 | }, 35 | { 36 | "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", 37 | "version": [1, 0, 3] 38 | } 39 | ], 40 | "metadata": { 41 | "authors": [ "ForestOfLight" ], 42 | "license": "MIT" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Canopy [BP]/pack_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [BP]/pack_icon.png -------------------------------------------------------------------------------- /Canopy [BP]/scripts/constants.js: -------------------------------------------------------------------------------- 1 | const PACK_VERSION = '1.3.9'; 2 | const MC_VERSION = '1.21.70.3'; 3 | 4 | export { PACK_VERSION, MC_VERSION }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/SRCItemDatabase/DBManager.js: -------------------------------------------------------------------------------- 1 | import { world } from "@minecraft/server"; 2 | import Database from "./Database"; 3 | 4 | class DatabaseManager { 5 | constructor() { 6 | /** 7 | * @returns {Database} structure ids database 8 | * @remarks Is used to store structure ids 9 | */ 10 | this.structureIds = new Database('structureIds'); 11 | } 12 | } 13 | /** 14 | * Database manager 15 | * @module DatabaseManager 16 | * @version 1.0.0 17 | * @example 18 | * Databases.config.get('key') 19 | */ 20 | export let Databases; 21 | world.afterEvents.worldLoad.subscribe(() => { 22 | Databases = new DatabaseManager(); 23 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/ArgumentParser.js: -------------------------------------------------------------------------------- 1 | import { Commands } from "./Commands"; 2 | 3 | class ArgumentParser { 4 | static regex = /(@[aepsr]\[|@"[^"]*"|"[^"]*"|\[[^\]]*\]|\S+)/g; 5 | static booleans = ['true', 'false']; 6 | static stringRegEx = /^"|"$/g; 7 | static arrayRegEx = /^\[|\]$/g; 8 | static entityRegEx = /@[aepsr]\[/g; 9 | 10 | static parseCommandString(text) { 11 | const parsedArgs = []; 12 | const rawArguments = text.match(this.regex); 13 | if (!rawArguments) 14 | throw new Error('Invalid command string'); 15 | 16 | rawArguments.forEach((arg, currIdx) => { 17 | const argData = this.#argParser(arg, currIdx, rawArguments); 18 | parsedArgs[currIdx] = argData; 19 | }); 20 | 21 | return { 22 | name: this.#extractName(parsedArgs), 23 | args: parsedArgs.filter(_ => _ !== '$nll_') 24 | }; 25 | } 26 | 27 | static #extractName(args) { 28 | let name = String(args.shift()); 29 | name = name.replace(Commands.getPrefix(), ''); 30 | return name; 31 | } 32 | 33 | static #argParser(arg, currIdx, rawArguments) { 34 | const isBoolean = this.booleans.includes(arg); 35 | const isNumber = !isNaN(Number(arg)); 36 | const isString = this.stringRegEx.test(arg); 37 | const isArray = this.arrayRegEx.test(arg); 38 | const isEntity = this.entityRegEx.test(arg); 39 | 40 | let data; 41 | if (isBoolean) { 42 | data = arg === 'true'; 43 | } else if (isNumber) { 44 | data = Number(arg); 45 | } else if (isString) { 46 | data = arg.replace(this.stringRegEx, ''); 47 | } else if (isEntity && this.arrayRegEx.test(rawArguments[currIdx+1])) { 48 | rawArguments[currIdx] += rawArguments[currIdx + 1]; 49 | rawArguments[currIdx + 1] = '$nll_'; 50 | data = rawArguments[currIdx]; 51 | } else if (isArray) { 52 | const array = []; 53 | const arrayData = arg.replace(this.arrayRegEx, ''); 54 | const arrayValues = arrayData.split(','); 55 | for (const value of arrayValues) 56 | array.push(this.#argParser(value, currIdx, rawArguments)); 57 | data = array; 58 | } else { 59 | data = arg.trim(); 60 | } 61 | return data; 62 | } 63 | } 64 | 65 | export { ArgumentParser }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/Canopy.js: -------------------------------------------------------------------------------- 1 | import { Commands } from "./Commands"; 2 | import { Command } from "./Command"; 3 | import { Rules } from "./Rules"; 4 | import { Rule } from "./Rule"; 5 | import { GlobalRule } from "./GlobalRule"; 6 | import { InfoDisplayRule } from "./InfoDisplayRule"; 7 | import { RuleHelpEntry } from "./help/RuleHelpEntry"; 8 | import { CommandHelpEntry } from "./help/CommandHelpEntry"; 9 | import { InfoDisplayRuleHelpEntry } from "./help/InfoDisplayRuleHelpEntry"; 10 | import { RuleHelpPage } from "./help/RuleHelpPage"; 11 | import { CommandHelpPage } from "./help/CommandHelpPage"; 12 | import { InfoDisplayRuleHelpPage } from "./help/InfoDisplayRuleHelpPage"; 13 | import { HelpBook } from "./help/HelpBook"; 14 | import { Extensions } from "./Extensions"; 15 | 16 | export { Commands, Command, Rules, Rule, GlobalRule, InfoDisplayRule, RuleHelpEntry, CommandHelpEntry, InfoDisplayRuleHelpEntry, 17 | RuleHelpPage, CommandHelpPage, InfoDisplayRuleHelpPage, HelpBook, Extensions }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/Command.js: -------------------------------------------------------------------------------- 1 | import { Commands } from "./Commands"; 2 | import { Extensions } from "./Extensions"; 3 | 4 | class Command { 5 | #name; 6 | #description; 7 | #usage; 8 | #callback; 9 | #args; 10 | #contingentRules; 11 | #adminOnly; 12 | #helpEntries; 13 | #helpHidden; 14 | #extension; 15 | 16 | constructor({ name, description = { text: '' }, usage, callback, args = [], contingentRules = [], adminOnly = false, helpEntries = [], helpHidden = false, extensionName = undefined }) { 17 | this.#name = name; 18 | this.#description = description; 19 | this.#usage = usage; 20 | this.#callback = callback; 21 | this.#args = args; 22 | this.#contingentRules = contingentRules; 23 | this.#adminOnly = adminOnly; 24 | this.#helpEntries = helpEntries; 25 | this.#helpHidden = helpHidden; 26 | this.#extension = Extensions.getFromName(extensionName); 27 | 28 | this.#checkMembers(extensionName); 29 | if (typeof this.#description === 'string') 30 | this.#description = { text: this.#description }; 31 | this.#helpEntries = this.#helpEntries.map(entry => { 32 | if (typeof entry.description === 'string') 33 | entry.description = { text: entry.description }; 34 | return entry; 35 | }); 36 | Commands.register(this); 37 | } 38 | 39 | #checkMembers(extensionName) { 40 | if (!this.#name) throw new Error('[Command] name is required.'); 41 | if (!this.#usage) throw new Error('[Command] usage is required.'); 42 | if (!Array.isArray(this.#args)) throw new Error('[Command] args must be an array.'); 43 | if (!Array.isArray(this.#contingentRules)) throw new Error('[Command] contingentRules must be an array.'); 44 | if (typeof this.#adminOnly !== 'boolean') throw new Error('[Command] adminOnly must be a boolean.'); 45 | if (!Array.isArray(this.#helpEntries)) throw new Error('[Command] helpEntries must be an array.'); 46 | if (extensionName && !this.#extension) throw new Error('[Command] extensionName must be a valid Extension.'); 47 | } 48 | 49 | getName() { 50 | return this.#name; 51 | } 52 | 53 | getDescription() { 54 | return this.#description; 55 | } 56 | 57 | getUsage() { 58 | return Commands.getPrefix() + this.#usage; 59 | } 60 | 61 | getArgs() { 62 | return this.#args; 63 | } 64 | 65 | getContingentRules() { 66 | return this.#contingentRules; 67 | } 68 | 69 | isAdminOnly() { 70 | return this.#adminOnly; 71 | } 72 | 73 | getHelpEntries() { 74 | return this.#helpEntries; 75 | } 76 | 77 | getExtension() { 78 | return this.#extension; 79 | } 80 | 81 | isHelpHidden() { 82 | return this.#helpHidden; 83 | } 84 | 85 | runCallback(sender, args) { 86 | if (this.#extension) 87 | this.#extension.runCommand(sender, this.#name, args); 88 | else 89 | this.#callback(sender, args); 90 | } 91 | 92 | sendUsage(sender) { 93 | sender.sendMessage({ translate: 'commands.generic.usage', with: [this.getUsage()] }); 94 | } 95 | } 96 | 97 | export { Command }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/Extensions.js: -------------------------------------------------------------------------------- 1 | import { Extension } from "./Extension"; 2 | import IPC from "../MCBE-IPC/ipc"; 3 | import { Ready, RegisterExtension } from "./extension.ipc"; 4 | 5 | class Extensions { 6 | static extensions = {}; 7 | 8 | static get(id) { 9 | return this.extensions[id]; 10 | } 11 | 12 | static getFromName(name) { 13 | return Object.values(this.extensions).find(extension => extension.getName() === name); 14 | } 15 | 16 | static remove(id) { 17 | delete this.extensions[id]; 18 | } 19 | 20 | static clear() { 21 | this.extensions = {}; 22 | } 23 | 24 | static getAll() { 25 | return Object.values(this.extensions); 26 | } 27 | 28 | static exists(id) { 29 | return this.extensions[id] !== undefined; 30 | } 31 | 32 | static getIds() { 33 | return Object.keys(this.extensions); 34 | } 35 | 36 | static getNames() { 37 | return this.getAll().map(extension => extension.getName()); 38 | } 39 | 40 | static getVersionedNames() { 41 | return this.getAll().map(extension => ({ name: extension.getName(), version: extension.getVersion() })); 42 | } 43 | 44 | static #setupExtensionRegistration() { 45 | IPC.on('canopyExtension:registerExtension', RegisterExtension, (extensionData) => { 46 | const extension = new Extension(extensionData); 47 | this.extensions[extension.getID()] = extension; 48 | console.warn(`[Canopy] Registered ${extensionData.name} v${extensionData.version}.`); 49 | }); 50 | IPC.send('canopyExtension:ready', Ready, {}); 51 | } 52 | 53 | static { 54 | this.#setupExtensionRegistration(); 55 | } 56 | } 57 | 58 | export { Extensions }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/GlobalRule.js: -------------------------------------------------------------------------------- 1 | import { Rule } from "./Rule"; 2 | 3 | class GlobalRule extends Rule { 4 | constructor(options) { 5 | options.category = "Rules"; 6 | if (!options.description) 7 | options.description = { translate: `rules.${options.identifier}` }; 8 | super({ ...options }); 9 | } 10 | } 11 | 12 | export { GlobalRule }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/InfoDisplayRule.js: -------------------------------------------------------------------------------- 1 | import { Rule } from './Rule'; 2 | import { Rules } from './Rules'; 3 | 4 | class InfoDisplayRule extends Rule { 5 | globalContingentRules; 6 | 7 | constructor(options) { 8 | options.category = "InfoDisplay"; 9 | super({ ...options }); 10 | this.globalContingentRules = options.globalContingentRules || []; 11 | } 12 | 13 | getGlobalContingentRuleIDs() { 14 | return this.globalContingentRules; 15 | } 16 | 17 | getValue(player) { 18 | return player.getDynamicProperty(super.getID()); 19 | } 20 | 21 | setValue(player, value) { 22 | player.setDynamicProperty(super.getID(), value); 23 | if (value === true) 24 | this.onEnable(player); 25 | else if (value === false) 26 | this.onDisable(player); 27 | } 28 | 29 | static get(identifier) { 30 | const rule = Rules.get(identifier); 31 | if (rule?.getCategory() === "InfoDisplay") 32 | return rule; 33 | return undefined; 34 | } 35 | 36 | static exists(identifier) { 37 | return Rules.exists(identifier) && Rules.get(identifier).getCategory() === "InfoDisplay"; 38 | } 39 | 40 | static getValue(player, identifier) { 41 | return this.get(identifier).getValue(player); 42 | } 43 | 44 | static setValue(player, identifier, value) { 45 | this.get(identifier).setValue(player, value); 46 | } 47 | 48 | static getAll() { 49 | return Object.values(Rules.getAll()).filter(rule => rule.getCategory() === "InfoDisplay"); 50 | } 51 | } 52 | 53 | export { InfoDisplayRule }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/Rule.js: -------------------------------------------------------------------------------- 1 | import { world } from '@minecraft/server'; 2 | import { Rules } from "./Rules"; 3 | import { Extensions } from './Extensions'; 4 | 5 | class Rule { 6 | #category; 7 | #identifier; 8 | #description; 9 | #contingentRules; 10 | #independentRules; 11 | #extension; 12 | 13 | constructor({ category, identifier, description = '', contingentRules = [], independentRules = [], 14 | onEnableCallback = () => {}, onDisableCallback = () => {}, extensionName = false }) { 15 | this.#category = category; 16 | this.#identifier = identifier; 17 | if (typeof description == 'string') 18 | description = { text: description }; 19 | this.#description = description; 20 | this.#contingentRules = contingentRules; 21 | this.#independentRules = independentRules; 22 | this.onEnable = onEnableCallback; 23 | this.onDisable = onDisableCallback; 24 | this.#extension = Extensions.getFromName(extensionName); 25 | Rules.register(this); 26 | } 27 | 28 | getCategory() { 29 | return this.#category; 30 | } 31 | 32 | getID() { 33 | return this.#identifier; 34 | } 35 | 36 | getDescription() { 37 | return this.#description; 38 | } 39 | 40 | getContigentRuleIDs() { 41 | return this.#contingentRules; 42 | } 43 | 44 | getIndependentRuleIDs() { 45 | return this.#independentRules; 46 | } 47 | 48 | getDependentRuleIDs() { 49 | return Rules.getDependentRuleIDs(this.#identifier); 50 | } 51 | 52 | getExtension() { 53 | return this.#extension; 54 | } 55 | 56 | async getValue() { 57 | if (this.#extension) 58 | return await this.#extension.getRuleValue(this.#identifier); 59 | return this.#parseRuleValueString(world.getDynamicProperty(this.#identifier)); 60 | } 61 | 62 | getNativeValue() { 63 | if (this.#extension) 64 | throw new Error(`[Canopy] [Rule] Native value is not available for ${this.#identifier} from extension ${this.#extension.getName()}.`); 65 | return this.#parseRuleValueString(world.getDynamicProperty(this.#identifier)); 66 | } 67 | 68 | setValue(value) { 69 | if (this.#extension) { 70 | this.#extension.setRuleValue(this.#identifier, value); 71 | } else { 72 | if (value === true) 73 | this.onEnable(); 74 | else if (value === false) 75 | this.onDisable(); 76 | world.setDynamicProperty(this.#identifier, value); 77 | } 78 | } 79 | 80 | #parseRuleValueString(value) { 81 | if (value === 'undefined' || value === undefined) 82 | return undefined; 83 | try { 84 | return JSON.parse(value); 85 | } catch { 86 | if (value === 'NaN') 87 | return NaN; 88 | throw new Error(`Failed to parse value for DynamicProperty: ${value}.`); 89 | } 90 | } 91 | } 92 | 93 | export { Rule }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/Rules.js: -------------------------------------------------------------------------------- 1 | import { world } from "@minecraft/server"; 2 | 3 | class Rules { 4 | static #rules = {}; 5 | static rulesToRegister = []; 6 | static worldLoaded = false; 7 | 8 | static async register(rule) { 9 | if (this.worldLoaded) { 10 | if (this.exists(rule.getID())) 11 | throw new Error(`[Canopy] Rule with identifier '${rule.getID()}' already exists.`); 12 | this.#rules[rule.getID()] = rule; 13 | if (rule.getCategory() === "Rules" && await rule.getValue() === true) 14 | rule.onEnable(); 15 | } else { 16 | this.rulesToRegister.push(rule); 17 | } 18 | } 19 | 20 | static get(identifier) { 21 | return this.#rules[identifier]; 22 | } 23 | 24 | static getAll() { 25 | return Object.values(this.#rules); 26 | } 27 | 28 | static getIDs() { 29 | return Object.keys(this.#rules); 30 | } 31 | 32 | static exists(identifier) { 33 | return this.#rules[identifier] !== undefined; 34 | } 35 | 36 | static remove(identifier) { 37 | delete this.#rules[identifier]; 38 | } 39 | 40 | static clear() { 41 | this.#rules = {}; 42 | } 43 | 44 | static async getValue(identifier) { 45 | const rule = this.get(identifier); 46 | if (!rule) 47 | throw new Error(`[Canopy] Rule with identifier '${identifier}' does not exist.`); 48 | return await rule.getValue(); 49 | } 50 | 51 | static getNativeValue(identifier) { 52 | const rule = this.get(identifier); 53 | if (!rule) 54 | throw new Error(`[Canopy] Rule with identifier '${identifier}' does not exist.`); 55 | return rule.getNativeValue(); 56 | } 57 | 58 | static setValue(identifier, value) { 59 | const rule = this.get(identifier) 60 | if (!rule) 61 | throw new Error(`[Canopy] Rule with identifier '${identifier}' does not exist.`); 62 | rule.setValue(value); 63 | } 64 | 65 | static getDependentRuleIDs(identifier) { 66 | const rule = this.get(identifier); 67 | if (!rule) 68 | throw new Error(`[Canopy] Rule with identifier '${identifier}' does not exist.`); 69 | return Rules.getAll().filter(r => r.getContigentRuleIDs().includes(identifier)).map(r => r.getID()); 70 | } 71 | 72 | static getByCategory(category) { 73 | return this.getAll().filter(rule => rule.getCategory() === category); 74 | } 75 | 76 | static registerQueuedRules() { 77 | for (const rule of this.rulesToRegister) 78 | this.register(rule); 79 | this.rulesToRegister = []; 80 | } 81 | } 82 | 83 | world.afterEvents.worldLoad.subscribe(() => { 84 | Rules.worldLoaded = true; 85 | Rules.registerQueuedRules(); 86 | }); 87 | 88 | export { Rules }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/extension.ipc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable new-cap */ 2 | import { PROTO } from '../MCBE-IPC/ipc' 3 | 4 | export const Ready = PROTO.Void; 5 | 6 | const description = PROTO.Object({ 7 | text: PROTO.Optional(PROTO.String), 8 | translate: PROTO.Optional(PROTO.String), 9 | with: PROTO.Optional(PROTO.Array(PROTO.String)) 10 | }); 11 | 12 | export const RegisterExtension = PROTO.Object({ 13 | name: PROTO.String, 14 | version: PROTO.String, 15 | author: PROTO.String, 16 | description: description, 17 | isEndstone: PROTO.Boolean 18 | }); 19 | 20 | export const RegisterCommand = PROTO.Object({ 21 | name: PROTO.String, 22 | description: description, 23 | usage: PROTO.String, 24 | callback: PROTO.Optional(PROTO.Undefined), 25 | args: PROTO.Optional(PROTO.Array(PROTO.Object({ 26 | type: PROTO.String, 27 | name: PROTO.String 28 | }))), 29 | contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)), 30 | adminOnly: PROTO.Optional(PROTO.Boolean), 31 | helpEntries: PROTO.Optional(PROTO.Array(PROTO.Object({ 32 | usage: PROTO.String, 33 | description: description 34 | }))), 35 | helpHidden: PROTO.Optional(PROTO.Boolean), 36 | extensionName: PROTO.Optional(PROTO.String) 37 | }); 38 | 39 | export const RegisterRule = PROTO.Object({ 40 | identifier: PROTO.String, 41 | description: description, 42 | contingentRules: PROTO.Optional(PROTO.Array(PROTO.String)), 43 | independentRules: PROTO.Optional(PROTO.Array(PROTO.String)), 44 | extensionName: PROTO.Optional(PROTO.String) 45 | }); 46 | 47 | export const RuleValueRequest = PROTO.Object({ 48 | ruleID: PROTO.String 49 | }); 50 | 51 | export const RuleValueResponse = PROTO.Object({ 52 | value: PROTO.Boolean 53 | }); 54 | 55 | export const RuleValueSet = PROTO.Object({ 56 | ruleID: PROTO.String, 57 | value: PROTO.Boolean 58 | }); 59 | 60 | export const CommandCallbackRequest = PROTO.Object({ 61 | commandName: PROTO.String, 62 | senderName: PROTO.Optional(PROTO.String), 63 | args: PROTO.String 64 | }); 65 | 66 | export const CommandPrefixRequest = PROTO.Void; 67 | 68 | export const CommandPrefixResponse = PROTO.Object({ 69 | prefix: PROTO.String 70 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/CommandHelpEntry.js: -------------------------------------------------------------------------------- 1 | import { HelpEntry } from './HelpEntry'; 2 | import { Commands } from '../Commands'; 3 | 4 | class CommandHelpEntry extends HelpEntry { 5 | constructor(command) { 6 | super(command.getName(), command.getDescription()); 7 | this.command = command; 8 | } 9 | 10 | toRawMessage() { 11 | const message = { rawtext: [{ text: `§2${this.command.getUsage()}§8 - ` }, this.description] }; 12 | for (const helpEntry of this.command.getHelpEntries()) 13 | message.rawtext.push({ rawtext: [{ text: `\n §7> §2${Commands.getPrefix()}${helpEntry.usage}§8 - ` }, helpEntry.description] }); 14 | return message; 15 | } 16 | } 17 | 18 | export { CommandHelpEntry }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/CommandHelpPage.js: -------------------------------------------------------------------------------- 1 | import { HelpPage } from "./HelpPage"; 2 | import { CommandHelpEntry } from "./CommandHelpEntry"; 3 | import { Command } from "../Command"; 4 | 5 | class CommandHelpPage extends HelpPage { 6 | constructor({ title, description = null }, extensionName = false) { 7 | super(title, description, extensionName); 8 | } 9 | 10 | addEntry(command) { 11 | if (!(command instanceof Command)) 12 | throw new Error('[HelpPage] Entry must be an instance of Command'); 13 | 14 | if (this.hasEntry(command)) 15 | return; 16 | this.entries.push(new CommandHelpEntry(command)); 17 | } 18 | 19 | hasEntry(command) { 20 | return this.entries.some(entry => entry.command.getName() === command.getName()); 21 | } 22 | 23 | toRawMessage() { 24 | const message = this.getPrintStarter(); 25 | if (this.description !== null) 26 | message.rawtext.push({ rawtext: [ { text: `\n§2` }, this.description ] }); 27 | for (const entry of this.entries) 28 | message.rawtext.push({ rawtext: [ { text: '\n ' }, entry.toRawMessage() ] }); 29 | 30 | return message; 31 | } 32 | } 33 | 34 | export { CommandHelpPage }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/HelpEntry.js: -------------------------------------------------------------------------------- 1 | class HelpEntry { 2 | constructor(title, description) { 3 | if (this.constructor === HelpEntry) 4 | throw new TypeError('HelpEntry is an abstract class and cannot be instantiated.'); 5 | this.title = title; 6 | if (typeof description === 'string') 7 | this.description = { text: description }; 8 | else 9 | this.description = description; 10 | } 11 | 12 | toRawMessage() { 13 | throw new TypeError('Method "toRawMessage" must be implemented.'); 14 | } 15 | } 16 | 17 | export { HelpEntry }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/HelpPage.js: -------------------------------------------------------------------------------- 1 | class HelpPage { 2 | entries = []; 3 | 4 | constructor(title, description = '', extensionName = false) { 5 | if (this.constructor === HelpPage) 6 | throw new TypeError("HelpPage is an abstract class and cannot be instantiated"); 7 | if (extensionName) 8 | this.title = extensionName + ':' + title; 9 | else 10 | this.title = title; 11 | if (typeof description === 'string') 12 | this.description = { text: description }; 13 | else 14 | this.description = description; 15 | this.extensionName = extensionName; 16 | } 17 | 18 | getEntries() { 19 | return this.entries; 20 | } 21 | 22 | addEntry() { 23 | throw new Error('[HelpPage] addEntry must be implemented in subclass'); 24 | } 25 | 26 | hasEntry() { 27 | throw new Error('[HelpPage] hasEntry must be implemented in subclass'); 28 | } 29 | 30 | toRawMessage() { 31 | throw new Error('[HelpPage] toRawMessage must be implemented in subclass'); 32 | } 33 | 34 | getPrintStarter() { 35 | let extensionFormat = ''; 36 | let titleFormat = this.title; 37 | if (this.extensionName) { 38 | extensionFormat = '§a' + this.extensionName + '§f:'; 39 | titleFormat = this.title.split(':')[1]; 40 | } 41 | return { rawtext: [{ translate: 'commands.help.page.header', with: [extensionFormat+titleFormat] }] }; 42 | } 43 | } 44 | 45 | export { HelpPage }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/InfoDisplayRuleHelpEntry.js: -------------------------------------------------------------------------------- 1 | import { RuleHelpEntry } from "./RuleHelpEntry"; 2 | import { InfoDisplayRule } from "../InfoDisplayRule"; 3 | 4 | class InfoDisplayRuleHelpEntry extends RuleHelpEntry { 5 | constructor(infoDisplayRule, player) { 6 | if (infoDisplayRule instanceof InfoDisplayRule === false) 7 | throw new TypeError('infoDisplayRule must be an instance of InfoDisplayRule'); 8 | super(infoDisplayRule); 9 | this.player = player; 10 | } 11 | 12 | async fetchColoredValue() { 13 | const value = await this.rule.getValue(this.player); 14 | return value ? '§atrue§r' : '§cfalse§r'; 15 | } 16 | } 17 | 18 | export { InfoDisplayRuleHelpEntry }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/InfoDisplayRuleHelpPage.js: -------------------------------------------------------------------------------- 1 | import { RuleHelpPage } from './RuleHelpPage'; 2 | import { InfoDisplayRuleHelpEntry } from './InfoDisplayRuleHelpEntry'; 3 | import { InfoDisplayRule } from '../InfoDisplayRule'; 4 | 5 | class InfoDisplayRuleHelpPage extends RuleHelpPage { 6 | constructor({ title, description, usage }, extensionName = false) { 7 | super({ title, description, usage }, extensionName); 8 | } 9 | 10 | addEntry(rule, player) { 11 | if (!(rule instanceof InfoDisplayRule)) 12 | throw new Error('[HelpPage] Entry must be an instance of InfoDisplayRule'); 13 | 14 | if (this.hasEntry(rule)) 15 | return; 16 | this.entries.push(new InfoDisplayRuleHelpEntry(rule, player)); 17 | } 18 | } 19 | 20 | export { InfoDisplayRuleHelpPage }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/RuleHelpEntry.js: -------------------------------------------------------------------------------- 1 | import { HelpEntry } from "./HelpEntry"; 2 | 3 | class RuleHelpEntry extends HelpEntry { 4 | constructor(rule) { 5 | super(rule.getID(), rule.getDescription()); 6 | this.rule = rule; 7 | } 8 | 9 | async toRawMessage() { 10 | const coloredValue = await this.fetchColoredValue().then(value => value); 11 | return { rawtext: [ { text: `§7${this.title}: ${coloredValue}§8 - ` }, this.description ] }; 12 | } 13 | 14 | async fetchColoredValue() { 15 | const value = await this.rule.getValue(); 16 | return value ? '§atrue§r' : '§cfalse§r'; 17 | } 18 | } 19 | 20 | export { RuleHelpEntry }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/lib/canopy/help/RuleHelpPage.js: -------------------------------------------------------------------------------- 1 | import { HelpPage } from './HelpPage'; 2 | import { RuleHelpEntry } from './RuleHelpEntry'; 3 | import { Rule } from '../Rule'; 4 | 5 | class RuleHelpPage extends HelpPage { 6 | constructor({ title, description, usage }, extensionId = false) { 7 | super(title, description, extensionId); 8 | this.usage = usage; 9 | } 10 | 11 | addEntry(rule) { 12 | if (!(rule instanceof Rule)) 13 | throw new Error('[HelpPage] Entry must be an instance of Rule'); 14 | 15 | if (this.hasEntry(rule)) 16 | return; 17 | this.entries.push(new RuleHelpEntry(rule)); 18 | } 19 | 20 | hasEntry(rule) { 21 | return this.entries.some(entry => entry.rule.getID() === rule.getID()); 22 | } 23 | 24 | async toRawMessage() { 25 | const message = this.getPrintStarter(); 26 | message.rawtext.push({ rawtext: [ { text: `\n§2${this.usage}§8 - ` }, this.description ] }); 27 | for (const entry of this.entries) 28 | message.rawtext.push({ rawtext: [ { text: '\n ' }, await entry.toRawMessage() ] }); 29 | 30 | return message; 31 | } 32 | } 33 | 34 | export { RuleHelpPage }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/main.js: -------------------------------------------------------------------------------- 1 | // Commands 2 | import './src/commands/info' 3 | import './src/commands/help' 4 | import './src/commands/peek' 5 | import './src/commands/jump' 6 | import './src/commands/warp' 7 | import './src/commands/gamemode' 8 | import './src/commands/camera' 9 | import './src/commands/canopy' 10 | import './src/commands/distance' 11 | import './src/commands/log' 12 | import './src/commands/entitydensity' 13 | import './src/commands/health' 14 | import './src/commands/counter' 15 | import './src/commands/resetall' 16 | import './src/commands/data' 17 | import './src/commands/tick' 18 | import './src/commands/changedimension' 19 | import './src/commands/spawn' 20 | import './src/commands/claimprojectiles' 21 | import './src/commands/trackevent' 22 | import './src/commands/tntfuse' 23 | import './src/commands/removeentity' 24 | import './src/commands/pos' 25 | import './src/commands/cleanup' 26 | import './src/commands/sit' 27 | import './src/commands/generator' 28 | import './src/commands/resettest' 29 | import './src/commands/simmap' 30 | import './src/commands/loop' 31 | 32 | // Script Events 33 | import './src/commands/scriptevents/counter' 34 | import './src/commands/scriptevents/spawn' 35 | import './src/commands/scriptevents/tick' 36 | import './src/commands/scriptevents/generator' 37 | import './src/commands/scriptevents/resettest' 38 | import './src/commands/scriptevents/loop' 39 | 40 | // Rules 41 | import './src/rules/infodisplay/InfoDisplay' 42 | import './src/rules/explosionNoBlockDamage' 43 | import './src/rules/autoItemPickup' 44 | import './src/rules/universalChunkLoading' 45 | import './src/rules/creativeNoTileDrops' 46 | import './src/rules/flippinArrows' 47 | import './src/rules/tntPrimeNoMomentum' 48 | import './src/rules/tntPrimeMaxMomentum' 49 | import './src/rules/dupeTnt' 50 | import './src/rules/pistonBedrockBreaking' 51 | import './src/rules/hotbarSwitching' 52 | import './src/rules/renewableSponge' 53 | import './src/rules/armorStandRespawning' 54 | import './src/rules/explosionOff' 55 | import './src/rules/explosionChainReactionOnly' 56 | import './src/rules/creativeInstantTame' 57 | import './src/rules/entityInstantDeath' 58 | import './src/rules/renewableElytra' 59 | import './src/rules/instaminableDeepslate' 60 | import './src/rules/instaminableEndstone' 61 | import './src/rules/quickFillContainer' 62 | import './src/rules/durabilityNotifier' 63 | import './src/rules/allowBubbleColumnPlacement' 64 | import './src/rules/cauldronConcreteConversion' 65 | import './src/rules/creativeOneHitKill' 66 | import './src/rules/playerSit' 67 | import './src/rules/refillHand' 68 | import './src/rules/durabilitySwap' 69 | import './src/rules/allowPeekInventory' 70 | 71 | // Load Time Processes 72 | import './src/onStart' 73 | import './src/onReload' 74 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/CounterChannel.js: -------------------------------------------------------------------------------- 1 | import ItemCounterChannel from "./ItemCounterChannel"; 2 | import { world, ItemStack } from "@minecraft/server"; 3 | 4 | class CounterChannel extends ItemCounterChannel { 5 | constructor(color) { 6 | super(color, `${color}CounterChannel`); 7 | } 8 | 9 | getQueryOutput(useRealTime = false) { 10 | return super.getQueryOutput('commands.counter.query.channel', useRealTime); 11 | } 12 | 13 | onTick() { 14 | super.onTick(() => this.#getItemStacks()); 15 | } 16 | 17 | #getItemStacks() { 18 | const countedItems = []; 19 | for (const hopperCounter of this.hopperList) { 20 | const hopper = world.getDimension(hopperCounter.dimensionId).getBlock(hopperCounter.location); 21 | if (!hopper) continue; 22 | 23 | const hopperContainer = hopper.getComponent('minecraft:inventory').container; 24 | const itemStack = hopperContainer?.getItem(0); 25 | if (!itemStack) continue; 26 | countedItems.push({ typeId: itemStack.typeId, amount: itemStack.amount }); 27 | hopperContainer.setItem(0, new ItemStack('minecraft:air', 1)); 28 | } 29 | return countedItems; 30 | } 31 | 32 | getAttachedBlockFromHopper(hopper) { 33 | const facing = hopper.permutation.getState("facing_direction"); 34 | switch (facing) { 35 | case 0: 36 | return hopper.below(); 37 | case 2: 38 | return hopper.north(); 39 | case 3: 40 | return hopper.south(); 41 | case 4: 42 | return hopper.west(); 43 | case 5: 44 | return hopper.east(); 45 | default: 46 | return undefined; 47 | } 48 | } 49 | } 50 | 51 | export default CounterChannel; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/CounterChannels.js: -------------------------------------------------------------------------------- 1 | import ItemCounterChannels from "./ItemCounterChannels.js"; 2 | import CounterChannel from "./CounterChannel"; 3 | import { world } from "@minecraft/server"; 4 | 5 | class CounterChannels extends ItemCounterChannels { 6 | constructor() { 7 | super(CounterChannel, 'hopperCounters'); 8 | } 9 | 10 | tryCreateHopperBlockPair(placedBlock) { 11 | if (this.isHopper(placedBlock)) { 12 | const potentialWool = this.getAttachedBlockFromHopper(placedBlock); 13 | if (this.isWool(potentialWool)) 14 | this.addHopper(placedBlock, this.getColorFromWool(potentialWool)); 15 | } else if (this.isWool(placedBlock)) { 16 | const potentialHoppers = [placedBlock.above(), placedBlock.north(), placedBlock.south(), placedBlock.west(), placedBlock.east()]; 17 | for (const potentialHopper of potentialHoppers) { 18 | if (this.isHopper(potentialHopper) && this.getAttachedBlockFromHopper(potentialHopper)?.typeId === placedBlock.typeId) 19 | this.addHopper(potentialHopper, this.getColorFromWool(placedBlock)); 20 | } 21 | } 22 | } 23 | 24 | getAttachedBlockFromHopper(hopper) { 25 | const facing = hopper.permutation.getState("facing_direction"); 26 | switch (facing) { 27 | case 0: 28 | return hopper.below(); 29 | case 2: 30 | return hopper.north(); 31 | case 3: 32 | return hopper.south(); 33 | case 4: 34 | return hopper.west(); 35 | case 5: 36 | return hopper.east(); 37 | default: 38 | return undefined; 39 | } 40 | } 41 | 42 | getColorFromWool(wool) { 43 | return wool.typeId.replace('minecraft:', '').replace('_wool', ''); 44 | } 45 | 46 | isWool(block) { 47 | return block?.typeId?.slice(-4) === 'wool'; 48 | } 49 | 50 | getAllQueryOutput(useRealTime = false) { 51 | return super.getAllQueryOutput('commands.counter.query.empty', useRealTime); 52 | } 53 | } 54 | 55 | let counterChannels; 56 | world.afterEvents.worldLoad.subscribe(() => { 57 | counterChannels = new CounterChannels(); 58 | }); 59 | 60 | export { counterChannels }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/EntityLog.js: -------------------------------------------------------------------------------- 1 | class EntityLog { 2 | constructor(type, { main, secondary, tertiary }) { 3 | if (this.constructor === EntityLog) 4 | throw new Error("Cannot instantiate abstract class 'EntityLog'."); 5 | this.type = type; 6 | this.colors = { main, secondary, tertiary }; 7 | this.subscribedPlayers = []; 8 | } 9 | 10 | subscribe(player) { 11 | this.subscribedPlayers.push(player); 12 | } 13 | 14 | unsubscribe(player) { 15 | this.subscribedPlayers = this.subscribedPlayers.filter(subscribedPlayer => subscribedPlayer.id !== player.id); 16 | } 17 | 18 | includes(player) { 19 | return this.subscribedPlayers.some(subscribedPlayer => subscribedPlayer.id === player.id); 20 | } 21 | } 22 | 23 | export { EntityLog }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/EntityTntLog.js: -------------------------------------------------------------------------------- 1 | import { EntityLog } from "./EntityLog"; 2 | import { world, system } from "@minecraft/server"; 3 | import { stringifyLocation } from "../../include/utils"; 4 | 5 | class EntityTntLog extends EntityLog { 6 | constructor(type, { main, secondary, tertiary }) { 7 | super(type, { main, secondary, tertiary }); 8 | this.tntSpawnLocations = {}; 9 | this.removedTntThisTick = []; 10 | this.initEvents(); 11 | } 12 | 13 | initEvents() { 14 | world.afterEvents.entitySpawn.subscribe((event) => this.onSpawn(event.entity)); 15 | world.beforeEvents.entityRemove.subscribe((event) => this.onRemove(event.removedEntity)); 16 | system.runInterval(() => this.onTick()); 17 | } 18 | 19 | onSpawn(entity) { 20 | if (entity?.typeId === 'minecraft:tnt') 21 | this.tntSpawnLocations[entity.id] = entity.location; 22 | } 23 | 24 | onRemove(entity) { 25 | if (entity?.typeId === 'minecraft:tnt' && this.subscribedPlayers.length > 0) 26 | this.removedTntThisTick.push({ id: entity.id, location: entity.location }); 27 | } 28 | 29 | onTick() { 30 | for (const player of this.subscribedPlayers) { 31 | if (this.isPrintable()) { 32 | const precision = player.getDynamicProperty('logPrecision'); 33 | player.sendMessage(this.getLogHeader()); 34 | player.sendMessage(this.getLogBody(precision)); 35 | } 36 | } 37 | this.removedTntThisTick = []; 38 | } 39 | 40 | isPrintable() { 41 | return this.removedTntThisTick.length > 0; 42 | } 43 | 44 | getLogHeader() { 45 | const maxTick = 1000; 46 | const shiftedTick = String(system.currentTick % maxTick).padStart(2, '0'); 47 | const coloredTick = `${shiftedTick.slice(0, -2)}${this.colors.secondary}${shiftedTick.slice(-2)}${this.colors.main}`; 48 | return { rawtext: [ 49 | { text: `${this.colors.tertiary}----- ` }, 50 | { translate: 'generic.total' }, 51 | { text: `: ${this.removedTntThisTick.length} ${this.colors.main}(tick: ${coloredTick}${this.colors.main})${this.colors.tertiary} -----`} 52 | ]}; 53 | } 54 | 55 | getLogBody(precision) { 56 | let output = ''; 57 | for (let i = 0; i < this.removedTntThisTick.length; i++) { 58 | const tntEntity = this.removedTntThisTick[i]; 59 | const startLocation = stringifyLocation(this.tntSpawnLocations[tntEntity.id], precision); 60 | const endLocation = stringifyLocation(tntEntity.location, precision); 61 | output += `§a${startLocation}§7 --> §c${endLocation}`; 62 | if (i < this.removedTntThisTick.length - 1) 63 | output += ', '; 64 | delete this.tntSpawnLocations[tntEntity.id]; 65 | } 66 | if (output.length > 0) 67 | output += '§r\n'; 68 | this.removedTntThisTick = []; 69 | return output; 70 | } 71 | } 72 | 73 | export { EntityTntLog }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/EventTracker.js: -------------------------------------------------------------------------------- 1 | import { world } from '@minecraft/server'; 2 | 3 | class EventTracker { 4 | constructor(eventName, isAfterEvent = true) { 5 | this.beforeEvents = world.beforeEvents; 6 | this.afterEvents = world.afterEvents; 7 | this.eventName = eventName; 8 | this.isAfterEvent = isAfterEvent; 9 | this.callback = undefined; 10 | this.count = 0; 11 | this.isTracking = false; 12 | this.setCallback(eventName, isAfterEvent); 13 | } 14 | 15 | setCallback(eventName, isAfterEvent = true) { 16 | if (isAfterEvent && this.afterEvents[eventName]) 17 | this.callback = this.afterEvents[eventName]; 18 | else if (this.beforeEvents[eventName]) 19 | this.callback = this.beforeEvents[eventName]; 20 | else 21 | throw new Error(`[EventTracker] Event ${eventName} not found. Could not create new tracker.`); 22 | 23 | } 24 | 25 | updateDynamicProperty() { 26 | const trackedEventsJSON = world.getDynamicProperty('trackedEvents'); 27 | const trackedEvents = trackedEventsJSON ? JSON.parse(trackedEventsJSON) : []; 28 | 29 | let found = false; 30 | for (let i = 0; i < trackedEvents.length; i++) { 31 | if (trackedEvents[i].eventName === this.eventName && trackedEvents[i].isAfterEvent === this.isAfterEvent) { 32 | if (this.isTracking) 33 | trackedEvents[i].count = this.count; 34 | else 35 | trackedEvents.splice(i, 1); 36 | found = true; 37 | break; 38 | } 39 | } 40 | if (!found && this.isTracking) 41 | trackedEvents.push(this.getInfo()); 42 | world.setDynamicProperty('trackedEvents', JSON.stringify(trackedEvents)); 43 | } 44 | 45 | start() { 46 | this.isTracking = true; 47 | this.callback.subscribe(this.increment.bind(this)); 48 | this.updateDynamicProperty(); 49 | } 50 | 51 | stop() { 52 | this.isTracking = false; 53 | this.callback.unsubscribe(this.increment.bind(this)); 54 | this.updateDynamicProperty(); 55 | } 56 | 57 | increment() { 58 | this.count++; 59 | this.updateDynamicProperty(); 60 | } 61 | 62 | getCount() { 63 | return this.count; 64 | } 65 | 66 | setCount(count) { 67 | this.count = count; 68 | this.updateDynamicProperty(); 69 | } 70 | 71 | reset() { 72 | this.count = 0; 73 | this.updateDynamicProperty(); 74 | } 75 | 76 | getInfo() { 77 | return { eventName: this.eventName, isAfterEvent: this.isAfterEvent, count: this.count }; 78 | } 79 | 80 | getInfoString() { 81 | return `§7${this.eventName}${this.isAfterEvent ? 'After' : 'Before'}Event:§f ${this.count}`; 82 | } 83 | } 84 | 85 | export default EventTracker; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/GeneratorChannel.js: -------------------------------------------------------------------------------- 1 | import ItemCounterChannel from "./ItemCounterChannel"; 2 | import { world, ItemStack } from "@minecraft/server"; 3 | 4 | class GeneratorChannel extends ItemCounterChannel { 5 | constructor(color) { 6 | super(color, `${color}GeneratorChannel`); 7 | } 8 | 9 | getQueryOutput(useRealTime = false) { 10 | return super.getQueryOutput('commands.generator.query.channel', useRealTime); 11 | } 12 | 13 | onTick() { 14 | super.onTick(() => this.#generateItems()); 15 | } 16 | 17 | getAttachedBlockFromHopper(hopper) { 18 | return hopper.above(); 19 | } 20 | 21 | #generateItems() { 22 | const generatedItems = []; 23 | for (const hopperGenerator of this.hopperList) { 24 | const hopper = world.getDimension(hopperGenerator.dimensionId).getBlock(hopperGenerator.location); 25 | if (!hopper) continue; 26 | const hopperContainer = hopper.getComponent('minecraft:inventory').container; 27 | const itemStack = hopperContainer?.getItem(0); 28 | if (itemStack) { 29 | hopperGenerator.outputItemType = itemStack.typeId; 30 | hopperGenerator.outputItemAmount = itemStack.amount; 31 | } else { 32 | if (hopperGenerator.outputItemType === null) 33 | continue; 34 | hopperContainer.setItem(0, new ItemStack(hopperGenerator.outputItemType)); 35 | generatedItems.push({ typeId: hopperGenerator.outputItemType, amount: 1 }); 36 | } 37 | } 38 | return generatedItems; 39 | } 40 | } 41 | 42 | export default GeneratorChannel; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/GeneratorChannels.js: -------------------------------------------------------------------------------- 1 | import ItemCounterChannels from "./ItemCounterChannels.js"; 2 | import GeneratorChannel from "./GeneratorChannel"; 3 | import { world } from "@minecraft/server"; 4 | 5 | class GeneratorChannels extends ItemCounterChannels { 6 | constructor() { 7 | super(GeneratorChannel, 'hopperGenerators'); 8 | } 9 | 10 | tryCreateHopperBlockPair(placedBlock) { 11 | if (this.isHopper(placedBlock)) { 12 | const potentialWool = placedBlock.above(); 13 | if (this.#isWool(potentialWool)) { 14 | const color = this.#getColorFromWool(potentialWool); 15 | this.addHopper(placedBlock, color); 16 | } 17 | } else if (this.#isWool(placedBlock)) { 18 | const potentialHopper = placedBlock.below(); 19 | if (this.isHopper(potentialHopper)) { 20 | const color = this.#getColorFromWool(placedBlock); 21 | this.addHopper(potentialHopper, color); 22 | } 23 | } 24 | } 25 | 26 | #getColorFromWool(wool) { 27 | return wool.typeId.replace('minecraft:', '').replace('_wool', ''); 28 | } 29 | 30 | #isWool(block) { 31 | return block?.typeId?.slice(-4) === 'wool'; 32 | } 33 | 34 | getAllQueryOutput(useRealTime = false) { 35 | return super.getAllQueryOutput('commands.generator.query.empty', useRealTime); 36 | } 37 | } 38 | 39 | let generatorChannels; 40 | world.afterEvents.worldLoad.subscribe(() => { 41 | generatorChannels = new GeneratorChannels(); 42 | }); 43 | 44 | export { generatorChannels }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/HotbarManager.js: -------------------------------------------------------------------------------- 1 | import SRCItemDatabase from "../../lib/SRCItemDatabase/ItemDatabase.js"; 2 | 3 | class HotbarManager { 4 | constructor(player) { 5 | this.player = player; 6 | const tableName = 'bar' + player.id.toString().substr(0, 9); 7 | this.itemDatabase = new SRCItemDatabase(tableName); 8 | this.lastLoadedHotbar = this.getLastLoadedHotbar(); 9 | } 10 | 11 | getActiveHotbarItems() { 12 | const container = this.player.getComponent('minecraft:inventory')?.container; 13 | const hotbarItems = []; 14 | for (let slotIndex = 0; slotIndex < 9; slotIndex++) { 15 | const itemStack = container.getItem(slotIndex); 16 | if (itemStack) 17 | hotbarItems.push({ key: slotIndex, item: itemStack }); 18 | 19 | } 20 | return hotbarItems; 21 | } 22 | 23 | saveHotbar() { 24 | const index = this.getLastLoadedHotbar(); 25 | const items = this.getActiveHotbarItems().map(item => ({ ...item, key: `${index}-${item.key}` })); 26 | this.itemDatabase.setMany(items); 27 | for (let slotIndex = 0; slotIndex < 9; slotIndex++) { 28 | if (!items.some(item => item.key === `${index}-${slotIndex}`)) 29 | this.itemDatabase.delete(`${index}-${slotIndex}`); 30 | } 31 | } 32 | 33 | loadHotbar(index) { 34 | const playerInventory = this.player.getComponent('inventory').container; 35 | for (let slotIndex = 0; slotIndex < 9; slotIndex++) { 36 | const item = this.itemDatabase.get(`${index}-${slotIndex}`); 37 | if (item) 38 | playerInventory.setItem(slotIndex, item); 39 | else 40 | playerInventory.setItem(slotIndex, null); 41 | } 42 | this.player.setDynamicProperty('lastLoadedHotbar', index); 43 | } 44 | 45 | getItemsString(items) { 46 | let output = 'Items:'; 47 | for (const slotIndex in items) { 48 | const itemStruct = items[slotIndex]; 49 | output += `\n${itemStruct.key}: ${itemStruct.item.typeId} x${itemStruct.item.count}`; 50 | } 51 | return output; 52 | } 53 | 54 | getLastLoadedHotbar() { 55 | const lastLoadedHotbar = this.player.getDynamicProperty('lastLoadedHotbar'); 56 | if (lastLoadedHotbar === undefined) 57 | return 0; 58 | return parseInt(lastLoadedHotbar, 10); 59 | } 60 | } 61 | 62 | export default HotbarManager; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/Instaminable.js: -------------------------------------------------------------------------------- 1 | import { Rules} from "../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | 4 | const beaconRefreshOffset = {}; 5 | const BEACON_REFRESH_RATE = 80; 6 | 7 | world.afterEvents.effectAdd.subscribe(event => { 8 | if (event.effect?.typeId !== 'haste' || event.entity?.typeId !== 'minecraft:player') return; 9 | beaconRefreshOffset[event.entity.id] = system.currentTick % BEACON_REFRESH_RATE; 10 | }); 11 | 12 | class Instaminable { 13 | constructor(litmusCallback, ruleId) { 14 | this.litmusCallback = litmusCallback; 15 | this.ruleId = ruleId; 16 | this.init(); 17 | } 18 | 19 | init() { 20 | system.runInterval(() => { 21 | for (const player of world.getPlayers()) { 22 | if (!player) 23 | continue; 24 | if (player.getEffect('haste')?.amplifier === 2 && this.isTickBeforeRefresh(player)) 25 | player.removeEffect('haste'); 26 | 27 | } 28 | }); 29 | 30 | world.beforeEvents.playerBreakBlock.subscribe((event) => { 31 | const blockId = event.block.typeId; 32 | if (Rules.getNativeValue(this.ruleId) !== true) return; 33 | if (!this.litmusCallback(blockId)) return; 34 | const player = event.player; 35 | if (this.isEfficiencyFiveNetheritePick(event.itemStack) && this.hasHasteTwo(player)) { 36 | const duration = player.getEffect('haste')?.duration; 37 | if (duration > 0) 38 | system.run(() => player.addEffect('haste', duration, { amplifier: 2 })); 39 | } 40 | }); 41 | } 42 | 43 | isTickBeforeRefresh(player) { 44 | return beaconRefreshOffset[player.id] === (system.currentTick + 1) % BEACON_REFRESH_RATE; 45 | } 46 | 47 | isEfficiencyFiveNetheritePick(itemStack) { 48 | if (itemStack && itemStack.typeId === 'minecraft:netherite_pickaxe') { 49 | const enchants = itemStack.getComponent('minecraft:enchantable').getEnchantments(); 50 | return enchants.some(enchant => enchant.type.id === 'efficiency' && enchant.level === 5); 51 | } 52 | return false; 53 | } 54 | 55 | hasHasteTwo(player) { 56 | const haste = player.getEffect('haste'); 57 | return haste?.amplifier === 1; 58 | } 59 | } 60 | 61 | export default Instaminable; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/InventoryUI.js: -------------------------------------------------------------------------------- 1 | import { EntityComponentTypes, ItemComponentTypes, system } from "@minecraft/server"; 2 | import { forceShow, titleCase, parseName } from "../../include/utils"; 3 | import { ChestFormData } from "../../lib/chestui/forms"; 4 | 5 | class InventoryUI { 6 | target; 7 | 8 | constructor(target) { 9 | this.target = target; 10 | } 11 | 12 | show(player) { 13 | system.run(() => { 14 | forceShow(player, this.buildInventoryUI()); 15 | }); 16 | } 17 | 18 | buildInventoryUI() { 19 | const numSlots = this.getInventorySize(); 20 | const form = new ChestFormData(numSlots); 21 | form.title(this.formatContainerName()); 22 | for (let slotNum = 0; slotNum < numSlots; slotNum++) 23 | this.buildSlotButton(form, slotNum); 24 | return form; 25 | } 26 | 27 | buildSlotButton(form, slotNum) { 28 | const itemStack = this.getItemStackFromSlot(slotNum); 29 | if (!itemStack) return; 30 | form.button( 31 | slotNum, 32 | this.formatItemName(itemStack), 33 | this.formatItemDescription(itemStack), 34 | itemStack.typeId, 35 | itemStack.amount, 36 | this.getDurabilityPercentage(itemStack), 37 | this.isEnchanted(itemStack) 38 | ); 39 | } 40 | 41 | getItemStackFromSlot(slot) { 42 | return this.getInventory().container.getItem(slot); 43 | } 44 | 45 | getInventorySize() { 46 | return this.getInventory().container.size; 47 | } 48 | 49 | getInventory() { 50 | let inventory; 51 | try { 52 | inventory = this.target.getComponent(EntityComponentTypes.Inventory); 53 | } catch { 54 | throw new Error('[Canopy] Failed to get inventory component. The entity may be unloaded or removed.'); 55 | } 56 | if (!inventory) 57 | throw new Error('[Canopy] No inventory component found for the target entity.'); 58 | return inventory; 59 | } 60 | 61 | formatContainerName() { 62 | return titleCase(parseName(this.target).replace('minecraft:', '')); 63 | } 64 | 65 | formatItemName(itemStack) { 66 | if (itemStack.nameTag) 67 | return `§o${itemStack.nameTag}`; 68 | return titleCase(itemStack.typeId.replace('minecraft:', '')); 69 | } 70 | 71 | formatItemDescription(itemStack) { 72 | const itemDesc = this.formatEnchantments(itemStack) 73 | return itemDesc; 74 | } 75 | 76 | formatEnchantments(itemStack) { 77 | const enchantments = itemStack.getComponent(ItemComponentTypes.Enchantable)?.getEnchantments(); 78 | if (!enchantments) 79 | return []; 80 | return enchantments.map(enchantment => { 81 | const enchantmentName = titleCase(enchantment.type.id.replace('minecraft:', '')); 82 | return `${enchantmentName} ${enchantment.level}`; 83 | }); 84 | } 85 | 86 | getDurabilityPercentage(itemStack) { 87 | const durabilityComponent = itemStack.getComponent(ItemComponentTypes.Durability); 88 | if (!durabilityComponent) 89 | return void 0; 90 | return (durabilityComponent.maxDurability - durabilityComponent.damage) / durabilityComponent.maxDurability * 100; 91 | } 92 | 93 | isEnchanted(itemStack) { 94 | const enchantableComponent = itemStack.getComponent(ItemComponentTypes.Enchantable); 95 | return enchantableComponent && enchantableComponent.getEnchantments().length > 0; 96 | } 97 | } 98 | 99 | export { InventoryUI }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/Probe.js: -------------------------------------------------------------------------------- 1 | import { system } from '@minecraft/server'; 2 | 3 | class Probe { 4 | constructor(entity, player) { 5 | this.assignedPlayer = player; 6 | this.entity = entity; 7 | this.attachRunner = null; 8 | this.entityInvalid = false; 9 | } 10 | 11 | attachToPlayer() { 12 | this.attachRunner = system.runInterval(() => { 13 | if (this.entity.isValid) { 14 | try { 15 | this.entity.teleport(this.getTeleportLocation(), { dimension: this.assignedPlayer.dimension }); 16 | } catch (error) { 17 | if (error.message.includes('property \'location\'')) 18 | return; 19 | throw error; 20 | } 21 | } else { 22 | this.entityInvalid = true; 23 | } 24 | }); 25 | } 26 | 27 | getTeleportLocation() { 28 | const location = this.assignedPlayer.location; 29 | const yaw = this.assignedPlayer.getRotation().y; 30 | 31 | const x = location.x + Math.sin(yaw * Math.PI / 180) * .15; 32 | const z = location.z - Math.cos(yaw * Math.PI / 180) * .15; 33 | return { x: x, y: location.y, z: z }; 34 | } 35 | 36 | detachFromPlayer() { 37 | system.clearRun(this.attachRunner); 38 | } 39 | 40 | getProperty(property) { 41 | try { 42 | return this.entity.getProperty('canopy:' + property); 43 | } catch { 44 | return -1; 45 | } 46 | } 47 | } 48 | 49 | export default Probe; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/Profiler.js: -------------------------------------------------------------------------------- 1 | import { system, TicksPerSecond } from "@minecraft/server"; 2 | import { wait } from "../../include/utils"; 3 | 4 | const MS_PER_SECOND = 1000; 5 | const SAMPLE_INTERVAL = 2; 6 | const NUM_SAMPLES = 50; 7 | 8 | class Profiler { 9 | static lastTickDate = Date.now(); 10 | static tickMs = 0; 11 | static tickTps = 0; 12 | static tps = 0; 13 | static profileTime = SAMPLE_INTERVAL * NUM_SAMPLES; 14 | static #tpsValues = []; 15 | static #isRunning = false; 16 | 17 | get lastTickDate() { return this.lastTickDate; } 18 | get tickMs() { return this.tickMs; } 19 | get tickTps() { return this.tickTps; } 20 | get tps() { return this.tps; } 21 | 22 | static start() { 23 | if (this.#isRunning) return; 24 | this.lastTickDate = Date.now(); 25 | system.runInterval(() => this.#updateTPS(), 1); 26 | this.#isRunning = true; 27 | } 28 | 29 | static async profile() { 30 | const mspt = await this.#profileMSPT(); 31 | const tps = await this.#profileTPS(); 32 | return { tps, mspt }; 33 | } 34 | 35 | static async #profileTPS() { 36 | const tpsValues = []; 37 | const runner = system.runInterval(() => { 38 | tpsValues.push(this.tickTps); 39 | }, SAMPLE_INTERVAL); 40 | await new Promise(resolve => system.runTimeout(() => { 41 | system.clearRun(runner); 42 | resolve(); 43 | }, SAMPLE_INTERVAL * NUM_SAMPLES)); 44 | const result = { 45 | result: this.tps, 46 | min: tpsValues.reduce((a, b) => Math.min(a, b)), 47 | max: tpsValues.reduce((a, b) => Math.max(a, b)), 48 | values: tpsValues 49 | } 50 | return result; 51 | } 52 | 53 | static async #profileMSPT() { 54 | const msptValues = []; 55 | const runner = system.runInterval( async () => { 56 | const mspt = await this.#getRealMspt(); 57 | if (mspt !== null) 58 | msptValues.push(mspt); 59 | }, SAMPLE_INTERVAL); 60 | await new Promise(resolve => system.runTimeout(() => { 61 | system.clearRun(runner); 62 | resolve(); 63 | }, SAMPLE_INTERVAL * NUM_SAMPLES)); 64 | const result = { 65 | result: msptValues.reduce((a, b) => a + b, 0) / msptValues.length, 66 | min: msptValues.reduce((a, b) => Math.min(a, b)), 67 | max: msptValues.reduce((a, b) => Math.max(a, b)), 68 | values: msptValues 69 | } 70 | return result; 71 | } 72 | 73 | static #updateTPS() { 74 | this.tickMS = Date.now() - this.lastTickDate; 75 | this.tickTps = MS_PER_SECOND / this.tickMS; 76 | this.#tpsValues.push(this.tickTps); 77 | if (this.#tpsValues.length > TicksPerSecond) 78 | this.#tpsValues.shift(); 79 | this.lastTickDate = Date.now(); 80 | this.tps = this.#tpsValues.reduce((a, b) => a + b, 0) / this.#tpsValues.length; 81 | } 82 | 83 | static #getRealMspt() { 84 | const lastTick = system.currentTick; 85 | const { startTime, endTime } = wait(50); 86 | return new Promise(resolve => { 87 | system.runTimeout(() => { 88 | if (system.currentTick - lastTick === 1) 89 | resolve(Date.now() - startTime - (endTime - startTime)); 90 | else 91 | resolve(null); 92 | }, 1); 93 | }); 94 | } 95 | } 96 | 97 | Profiler.start(); 98 | 99 | export { Profiler }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/classes/Warps.js: -------------------------------------------------------------------------------- 1 | import { world } from "@minecraft/server"; 2 | 3 | class Warps { 4 | static warps; 5 | 6 | static init() { 7 | const warps = world.getDynamicProperty('warps'); 8 | try { 9 | this.warps = JSON.parse(warps); 10 | } catch { 11 | this.warps = {}; 12 | } 13 | } 14 | 15 | static updateDP() { 16 | world.setDynamicProperty(`warps`, JSON.stringify(this.warps)); 17 | } 18 | 19 | static add(name, location, dimensionId) { 20 | if (this.has(name)) 21 | throw new Error('Warp already exists'); 22 | this.warps[name] = { 23 | name, 24 | location, 25 | dimensionId 26 | }; 27 | this.updateDP(); 28 | } 29 | 30 | static remove(name) { 31 | if (!this.has(name)) 32 | throw new Error(`Failed to remove warp ${name}`); 33 | delete this.warps[name]; 34 | this.updateDP(); 35 | } 36 | 37 | static teleport(player, name) { 38 | const warp = this.warps[name]; 39 | if (!warp) 40 | throw new Error('Warp does not exist'); 41 | player.teleport({ x: warp.location.x, y: warp.location.y, z: warp.location.z }, { dimension: world.getDimension(warp.dimensionId) }); 42 | } 43 | 44 | static has(name) { 45 | return this.warps[name] !== undefined; 46 | } 47 | 48 | static isEmpty() { 49 | return Object.keys(this.warps).length === 0; 50 | } 51 | 52 | static getNames() { 53 | return Object.keys(this.warps); 54 | } 55 | } 56 | 57 | world.afterEvents.worldLoad.subscribe(() => { Warps.init(); }) 58 | 59 | export default Warps; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/changedimension.js: -------------------------------------------------------------------------------- 1 | import { Rule, Command } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | import { isNumeric, stringifyLocation, getColoredDimensionName } from "../../include/utils"; 4 | 5 | const validDimensions = { 6 | 'o': 'overworld', 7 | 'overworld': 'overworld', 8 | 'minecraft:overworld': 'overworld', 9 | 'n': 'nether', 10 | 'nether': 'nether', 11 | 'minecraft:the_nether': 'nether', 12 | 'e': 'the_end', 13 | 'end': 'the_end', 14 | 'the_end': 'the_end', 15 | 'minecraft:the_end': 'the_end' 16 | }; 17 | 18 | new Rule({ 19 | category: 'Rules', 20 | identifier: 'commandChangeDimension', 21 | description: { translate: 'rules.commandChangeDimension' } 22 | }); 23 | 24 | const cmd = new Command({ 25 | name: 'dtp', 26 | description: { translate: 'commands.changedimension' }, 27 | usage: 'dtp [x y z]', 28 | args: [ 29 | { type: 'string', name: 'dimension' }, 30 | { type: 'number', name: 'x' }, 31 | { type: 'number', name: 'y' }, 32 | { type: 'number', name: 'z' } 33 | ], 34 | callback: changedimensionCommand, 35 | contingentRules: ['commandChangeDimension'] 36 | }); 37 | 38 | function changedimensionCommand(player, args) { 39 | const { dimension, x, y, z } = args; 40 | if (!dimension) 41 | return cmd.sendUsage(player); 42 | const toDimensionId = validDimensions[dimension.toLowerCase()]; 43 | if (!toDimensionId) 44 | return player.sendMessage({ translate: 'commands.changedimension.notfound', with: [Object.keys(validDimensions).join(', ')] }); 45 | 46 | const fromDimensionId = player.dimension.id.replace('minecraft:', ''); 47 | const toDimension = world.getDimension(toDimensionId); 48 | if ((x !== null && y !== null && z !== null) && (isNumeric(x) && isNumeric(y) && isNumeric(z))) { 49 | const location = { x, y, z }; 50 | player.teleport(location, { dimension: toDimension } ); 51 | player.sendMessage({ translate: 'commands.changedimension.success.coords', with: [stringifyLocation(location), toDimensionId] }); 52 | } else if (x === null && y === null && z === null) { 53 | player.teleport(convertCoords(fromDimensionId, toDimensionId, player.location), { dimension: toDimension }); 54 | player.sendMessage({ translate: 'commands.changedimension.success', with: [getColoredDimensionName(toDimensionId)] }); 55 | } else { 56 | player.sendMessage({ translate: 'commands.changedimension.fail.coords' }); 57 | } 58 | } 59 | 60 | function convertCoords(fromDimension, toDimension, location) { 61 | if (fromDimension === 'overworld' && toDimension === 'nether') 62 | return { x: location.x / 8, y: location.y, z: location.z / 8 }; 63 | else if (fromDimension === 'nether' && toDimension === 'overworld') 64 | return { x: location.x * 8, y: location.y, z: location.z * 8 }; 65 | return location; 66 | } 67 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/claimprojectiles.js: -------------------------------------------------------------------------------- 1 | import { world } from "@minecraft/server"; 2 | import { Rule, Command } from "../../lib/canopy/Canopy"; 3 | import { isNumeric } from "../../include/utils"; 4 | 5 | const CLAIM_RADIUS = 25; 6 | 7 | new Rule({ 8 | category: 'Rules', 9 | identifier: 'commandClaimProjectiles', 10 | description: { translate: 'rules.commandClaimProjectiles' } 11 | }); 12 | 13 | new Command({ 14 | name: 'claimprojectiles', 15 | description: { translate: 'commands.claimprojectiles' }, 16 | usage: 'claimprojectiles [playerName/radius] [radius]', 17 | args: [ 18 | { type: 'string|number', name: 'playerName' }, 19 | { type: 'number', name: 'radius' } 20 | ], 21 | callback: claimProjectilesCommand, 22 | contingentRules: ['commandClaimProjectiles'] 23 | }); 24 | 25 | function claimProjectilesCommand(sender, args) { 26 | const playerName = args.playerName; 27 | let radius = args.radius; 28 | let targetPlayer; 29 | if (playerName === null) 30 | targetPlayer = sender; 31 | else 32 | targetPlayer = getTargetPlayer(sender, String(playerName)); 33 | if (!targetPlayer) 34 | return sender.sendMessage({ translate: 'generic.player.notfound', with: [String(playerName)] }); 35 | if (isNumeric(playerName)) { 36 | radius = playerName; 37 | targetPlayer = sender; 38 | } 39 | if (radius === null) 40 | radius = CLAIM_RADIUS; 41 | 42 | const projectiles = getProjectilesInRange(targetPlayer, radius); 43 | if (projectiles.length === 0) 44 | return sender.sendMessage({ translate: 'commands.claimprojectiles.fail.nonefound', with: [String(radius)] }); 45 | 46 | const numChanged = changeOwner(projectiles, targetPlayer); 47 | targetPlayer.sendMessage({ translate: 'commands.claimprojectiles.success.self', with: [String(numChanged), String(radius)] }); 48 | if (sender !== targetPlayer) 49 | sender.sendMessage({ translate: 'commands.claimprojectiles.success.other', with: [String(numChanged), String(radius), targetPlayer.name] }); 50 | } 51 | 52 | function getTargetPlayer(sender, playerName) { 53 | if (playerName === null) 54 | return sender; 55 | const players = world.getPlayers({ name: playerName }); 56 | if (players.length === 1) 57 | return players[0]; 58 | else if (isNumeric(playerName)) 59 | return playerName; 60 | return undefined; 61 | } 62 | 63 | function getProjectilesInRange(sender, radius) { 64 | const radiusProjectiles = []; 65 | const radiusEntities = sender.dimension.getEntities({ location: sender.location, maxDistance: radius }); 66 | for (const entity of radiusEntities) { 67 | if (entity?.hasComponent('minecraft:projectile')) 68 | radiusProjectiles.push(entity); 69 | } 70 | return radiusProjectiles; 71 | } 72 | 73 | function changeOwner(projectiles, targetPlayer) { 74 | for (const projectile of projectiles) { 75 | if (!projectile) 76 | continue; 77 | projectile.getComponent('minecraft:projectile').owner = targetPlayer; 78 | } 79 | return projectiles.length; 80 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/cleanup.js: -------------------------------------------------------------------------------- 1 | import { Rule, Command } from '../../lib/canopy/Canopy'; 2 | 3 | new Rule({ 4 | category: 'Rules', 5 | identifier: 'commandCleanup', 6 | description: { translate: 'rules.commandCleanup' } 7 | }); 8 | 9 | new Command({ 10 | name: 'cleanup', 11 | description: { translate: 'commands.cleanup' }, 12 | usage: 'cleanup [distance]', 13 | args: [ 14 | { type: 'number', name: 'distance' } 15 | ], 16 | callback: cleanupCommand, 17 | contingentRules: ['commandCleanup'] 18 | }); 19 | 20 | new Command({ 21 | name: 'k', 22 | description: { translate: 'commands.cleanup' }, 23 | usage: 'k [distance]', 24 | args: [ 25 | { type: 'number', name: 'distance' } 26 | ], 27 | callback: cleanupCommand, 28 | contingentRules: ['commandCleanup'], 29 | helpHidden: true 30 | }); 31 | 32 | const TRASH_ENTITY_TYPES = ['minecraft:item', 'minecraft:xp_orb']; 33 | 34 | function cleanupCommand(sender, args) { 35 | const { distance } = args; 36 | const removedCount = removeTrashEntities(sender, distance); 37 | sender.sendMessage({ translate: 'commands.cleanup.success', with: [removedCount.toString()] }); 38 | } 39 | 40 | function removeTrashEntities(player, distance) { 41 | let removedCount = 0; 42 | for (const type of TRASH_ENTITY_TYPES) { 43 | let entities; 44 | if (distance === null) 45 | entities = player.dimension.getEntities({ type: type }); 46 | else 47 | entities = player.dimension.getEntities({ type: type, location: player.location, maxDistance: distance }); 48 | for (const entity of entities) { 49 | entity.remove(); 50 | removedCount++; 51 | } 52 | } 53 | return removedCount; 54 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/gamemode.js: -------------------------------------------------------------------------------- 1 | import { Rule, Command } from '../../lib/canopy/Canopy'; 2 | 3 | new Rule({ 4 | category: 'Rules', 5 | identifier: 'commandGamemode', 6 | description: { translate: 'rules.commandGamemode' } 7 | }); 8 | 9 | const gamemodeMap = { 10 | 's': 'survival', 11 | 'a': 'adventure', 12 | 'c': 'creative', 13 | 'sp': 'spectator' 14 | }; 15 | 16 | for (const key in gamemodeMap) { 17 | new Command({ 18 | name: key, 19 | description: { translate: `commands.gamemode.${key}` }, 20 | usage: key, 21 | callback: (sender) => sender.runCommand(`gamemode ${gamemodeMap[key]}`), 22 | contingentRules: ['commandGamemode'] 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/generator.js: -------------------------------------------------------------------------------- 1 | import { Rule, Command } from "../../lib/canopy/Canopy"; 2 | import { generatorChannels } from "../classes/GeneratorChannels"; 3 | import { formatColorStr, broadcastActionBar } from "../../include/utils"; 4 | 5 | new Rule({ 6 | category: 'Rules', 7 | identifier: 'hopperGenerators', 8 | description: { translate: 'rules.hopperGenerators' }, 9 | onEnableCallback: () => generatorChannels.enable(), 10 | onDisableCallback: () => generatorChannels.disable() 11 | }); 12 | 13 | const cmd = new Command({ 14 | name: 'generator', 15 | description: { translate: 'commands.generator' }, 16 | usage: 'generator [reset/realtime]', 17 | args: [ 18 | { type: 'string', name: 'argOne' }, 19 | { type: 'string', name: 'argTwo' } 20 | ], 21 | callback: generatorCommand, 22 | contingentRules: ['hopperGenerators'], 23 | helpEntries: [ 24 | { usage: 'generator ', description: { translate: 'commands.generator.query' } }, 25 | { usage: 'generator [color] realtime', description: { translate: 'commands.generator.realtime' } }, 26 | { usage: 'generator [color] reset', description: { translate: 'commands.generator.reset' } } 27 | ] 28 | }); 29 | 30 | new Command({ 31 | name: 'gt', 32 | description: { translate: 'commands.generator' }, 33 | usage: 'gt [reset/realtime]', 34 | args: [ 35 | { type: 'string', name: 'argOne' }, 36 | { type: 'string', name: 'argTwo' } 37 | ], 38 | callback: generatorCommand, 39 | contingentRules: ['hopperGenerators'], 40 | helpHidden: true 41 | }); 42 | 43 | function generatorCommand(sender, args) { 44 | const { argOne, argTwo } = args; 45 | 46 | if (argOne === 'reset') 47 | resetAll(sender); 48 | else if (argOne === 'realtime') 49 | queryAll(sender, { useRealTime: true }); 50 | else if (generatorChannels.isValidColor(argOne) && !argTwo) 51 | query(sender, argOne); 52 | else if (!argOne && !argTwo || argOne === 'all' && !argTwo) 53 | queryAll(sender); 54 | else if (argOne && argTwo === 'realtime') 55 | query(sender, argOne, { useRealTime: true }); 56 | else if (argOne && argTwo === 'reset') 57 | reset(sender, argOne); 58 | else if (argOne && !generatorChannels.isValidColor(argOne)) 59 | sender.sendMessage({ translate: 'commands.generator.channel.notfound', with: [argOne] }); 60 | else 61 | cmd.sendUsage(sender); 62 | } 63 | 64 | function reset(sender, color) { 65 | generatorChannels.resetCounts(color); 66 | sender.sendMessage({ translate: 'commands.generator.reset.single', with: [formatColorStr(color)] }); 67 | broadcastActionBar({ translate: 'commands.generator.reset.single.actionbar', with: [sender.name, formatColorStr(color)]}, sender); 68 | } 69 | 70 | function resetAll(sender) { 71 | generatorChannels.resetAllCounts(); 72 | sender.sendMessage({ translate: 'commands.generator.reset.all' }); 73 | broadcastActionBar({ translate: 'commands.generator.reset.all.actionbar', with: [sender.name] }, sender); 74 | } 75 | 76 | function query(sender, color, { useRealTime = false } = {}) { 77 | sender.sendMessage(generatorChannels.getQueryOutput(color, useRealTime)); 78 | } 79 | 80 | function queryAll(sender, { useRealTime = false } = {}) { 81 | sender?.sendMessage(generatorChannels.getAllQueryOutput(useRealTime)); 82 | } 83 | 84 | export { query, queryAll }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/health.js: -------------------------------------------------------------------------------- 1 | import { Command } from "../../lib/canopy/Canopy"; 2 | import { printDimensionEntities } from "../commands/entitydensity"; 3 | import { Profiler } from '../classes/Profiler'; 4 | 5 | new Command({ 6 | name: 'health', 7 | description: { translate: 'commands.health' }, 8 | usage: 'health', 9 | callback: healthCommand 10 | }) 11 | 12 | export async function healthCommand(sender) { 13 | sender.sendMessage(`§7Profiling tick time...`); 14 | const profile = await Profiler.profile(); 15 | printDimensionEntities(sender); 16 | sender.sendMessage(formatProfileMessage(profile)); 17 | } 18 | 19 | function formatProfileMessage(profile) { 20 | let message = `§7Tps:§r ${profile.tps.result >= 20.0 ? `§a20.0` : `§c${profile.tps.result.toFixed(1)}`}`; 21 | message += ` §7Range: ${profile.tps.min.toFixed(1)}-${profile.tps.max.toFixed(1)}\n`; 22 | message += `§7Mspt:§r ${profile.mspt.result < 50 ? '§a' : '§c'}${profile.mspt.result.toFixed(1)}`; 23 | message += ` §7Range: ${profile.mspt.min.toFixed(1)}-${profile.mspt.max.toFixed(1)}`; 24 | return message; 25 | } 26 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/jump.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules, Command } from "../../lib/canopy/Canopy"; 2 | 3 | new Rule({ 4 | category: 'Rules', 5 | identifier: 'commandJumpSurvival', 6 | description: { translate: 'rules.commandJumpSurvival' } 7 | }); 8 | 9 | new Command({ 10 | name: 'jump', 11 | description: { translate: 'commands.jump' }, 12 | usage: 'jump', 13 | callback: jumpCommand 14 | }); 15 | 16 | new Command({ 17 | name: 'j', 18 | description: { translate: 'commands.jump' }, 19 | usage: 'j', 20 | callback: jumpCommand, 21 | helpHidden: true 22 | }) 23 | 24 | function jumpCommand(sender) { 25 | if (!Rules.getNativeValue('commandJumpSurvival') && sender.getGameMode() === 'survival') 26 | return sender.sendMessage({ translate: 'rules.generic.blocked', with: ['commandJumpSurvival'] }); 27 | 28 | const blockRayResult = sender.getBlockFromViewDirection({ includeLiquidBlocks: false, includePassableBlocks: true, maxDistance: 64*16 }); 29 | if (!blockRayResult?.block) 30 | return sender.sendMessage({ translate: 'commands.jump.fail.noblock' }); 31 | const jumpLocation = getBlockLocationFromFace(blockRayResult.block, blockRayResult.face); 32 | sender.teleport(jumpLocation); 33 | } 34 | 35 | function getBlockLocationFromFace(block, face) { 36 | switch(face) { 37 | case 'Up': 38 | return { x: block.x, y: block.y + 1, z: block.z}; 39 | case 'Down': 40 | return { x: block.x, y: block.y - 2, z: block.z}; 41 | case 'North': 42 | return { x: block.x, y: block.y, z: block.z - 1}; 43 | case 'South': 44 | return { x: block.x, y: block.y, z: block.z + 1}; 45 | case 'East': 46 | return { x: block.x + 1, y: block.y, z: block.z}; 47 | case 'West': 48 | return { x: block.x - 1, y: block.y, z: block.z}; 49 | default: 50 | throw new Error('Invalid face'); 51 | } 52 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/log.js: -------------------------------------------------------------------------------- 1 | import { Command } from "../../lib/canopy/Canopy"; 2 | import { EntityMovementLog } from "../classes/EntityMovementLog"; 3 | import { EntityTntLog } from "../classes/EntityTntLog"; 4 | 5 | const MAIN_COLOR = '§7'; 6 | const SECONDARY_COLOR = '§c'; 7 | const TERTIARY_COLOR = '§a'; 8 | 9 | const entityLogs = { 10 | "projectiles": new EntityMovementLog('projectiles', { main: MAIN_COLOR, secondary: SECONDARY_COLOR, tertiary: TERTIARY_COLOR }), 11 | "falling_blocks": new EntityMovementLog('falling_blocks', { main: MAIN_COLOR, secondary: SECONDARY_COLOR, tertiary: TERTIARY_COLOR }), 12 | "tnt": new EntityTntLog('tnt', { main: MAIN_COLOR, secondary: SECONDARY_COLOR, tertiary: TERTIARY_COLOR }) 13 | }; 14 | 15 | const cmd = new Command({ 16 | name: 'log', 17 | description: { translate: 'commands.log' }, 18 | usage: 'log [precision]', 19 | args: [ 20 | { type: 'string', name: 'type' }, 21 | { type: 'number', name: 'precision' } 22 | ], 23 | callback: logCommand 24 | }); 25 | 26 | export function logCommand(sender, args) { 27 | const { type, precision } = args; 28 | if (sender.getDynamicProperty('logPrecision') === undefined) 29 | sender.setDynamicProperty('logPrecision', 3); 30 | if (precision !== null) 31 | setLogPrecsion(sender, precision); 32 | if (Object.keys(entityLogs).includes(type)) 33 | toggleLogging(sender, type); 34 | else 35 | cmd.sendUsage(sender); 36 | } 37 | 38 | function setLogPrecsion(sender, value) { 39 | const precision = Math.max(0, Math.min(parseInt(value, 10), 15)); 40 | sender.setDynamicProperty('logPrecision', precision); 41 | sender.sendMessage({ translate: 'commands.log.precision', with: [String(precision)] }); 42 | } 43 | 44 | function toggleLogging(sender, type) { 45 | let message; 46 | if (entityLogs[type].includes(sender)) { 47 | entityLogs[type].unsubscribe(sender); 48 | message = { translate: 'commands.log.stopped', with: [type] }; 49 | } else { 50 | entityLogs[type].subscribe(sender); 51 | message = { translate: 'commands.log.started', with: [type] }; 52 | } 53 | sender.sendMessage(message); 54 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/loop.js: -------------------------------------------------------------------------------- 1 | import { Command } from 'lib/canopy/Canopy'; 2 | import { CommandError } from '@minecraft/server'; 3 | 4 | const cmd = new Command({ 5 | name: 'loop', 6 | description: { translate: 'commands.loop' }, 7 | usage: 'loop <"command to run">', 8 | args: [ 9 | { type: 'number', name: 'times' }, 10 | { type: 'string', name: 'command' } 11 | ], 12 | callback: loopCommand 13 | }) 14 | 15 | function loopCommand(sender, args) { 16 | const { times, command } = args; 17 | if (times === null || command === null) 18 | return cmd.sendUsage(sender); 19 | loop(times, command, sender); 20 | } 21 | 22 | function loop(times, command, runLocation) { 23 | for (let i = 0; i < times; i++) { 24 | try { 25 | runLocation.runCommand(command); 26 | } catch (error) { 27 | if (error instanceof CommandError) 28 | return runLocation.sendMessage(`§cLoop error (Iteration ${i+1}): ${error.message}`); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/peek.js: -------------------------------------------------------------------------------- 1 | import { Command } from "../../lib/canopy/Canopy"; 2 | import { getRaycastResults, getClosestTarget, stringifyLocation } from "../../include/utils"; 3 | import { InventoryUI } from "../classes/InventoryUI"; 4 | 5 | const MAX_DISTANCE = 6*16; 6 | const currentQuery = {}; 7 | 8 | new Command({ 9 | name: 'peek', 10 | description: { translate: 'commands.peek' }, 11 | usage: 'peek [query]', 12 | args: [ 13 | { type: 'string', name: 'itemQuery' } 14 | ], 15 | contingentRules: ['allowPeekInventory'], 16 | callback: peekCommand 17 | }); 18 | 19 | function peekCommand(sender, args) { 20 | const { itemQuery } = args; 21 | 22 | updateQueryMap(sender, itemQuery); 23 | const target = getTarget(sender); 24 | if (!target) return; 25 | 26 | const invUI = new InventoryUI(target); 27 | showUI(sender, target, invUI); 28 | } 29 | 30 | function updateQueryMap(sender, itemQuery) { 31 | const oldQuery = currentQuery[sender.name]; 32 | if ([null, undefined].includes(oldQuery) && itemQuery === null) 33 | return; 34 | if (itemQuery === null && ![null, undefined].includes(oldQuery)) { 35 | currentQuery[sender.name] = null; 36 | sender.sendMessage({ translate: 'commands.peek.query.cleared' }); 37 | return; 38 | } 39 | currentQuery[sender.name] = itemQuery; 40 | sender.sendMessage({ translate: 'commands.peek.query.set', with: [itemQuery] }); 41 | } 42 | 43 | function getTarget(sender) { 44 | const {blockRayResult, entityRayResult} = getRaycastResults(sender, MAX_DISTANCE); 45 | if (!blockRayResult && !entityRayResult[0]) 46 | return sender.sendMessage({ translate: 'generic.target.notfound' }); 47 | return getClosestTarget(sender, blockRayResult, entityRayResult); 48 | } 49 | 50 | function showUI(sender, target, invUI) { 51 | try { 52 | invUI.show(sender); 53 | } catch (error) { 54 | if (error.message.includes('entity may be unloaded or removed')) 55 | sender.sendMessage({ translate: 'commands.peek.fail.unloaded', with: [stringifyLocation(target.entity.location, 0)] }); 56 | else if (error.message.includes('no inventory component found')) 57 | sender.sendMessage({ translate: 'commands.peek.fail.noinventory', with: [target.name, stringifyLocation(target.entity.location, 0)] }); 58 | else 59 | new Error(`[Canopy] Error showing inventory UI:`, { cause: error }); 60 | } 61 | } 62 | 63 | export { currentQuery }; 64 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/pos.js: -------------------------------------------------------------------------------- 1 | import { Rule, Command, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | import { stringifyLocation, getColoredDimensionName } from "../../include/utils"; 4 | 5 | const NETHER_SCALE_FACTOR = 8; 6 | 7 | new Rule({ 8 | category: 'Rules', 9 | identifier: 'commandPosOthers', 10 | description: { translate: 'rules.commandPosOthers' } 11 | }); 12 | 13 | new Command({ 14 | name: 'pos', 15 | description: { translate: 'commands.pos' }, 16 | usage: 'pos [player]', 17 | args: [ 18 | { type: 'string|number', name: 'player' } 19 | ], 20 | callback: posCommand 21 | }); 22 | 23 | function posCommand(sender, args) { 24 | const { player } = args; 25 | if (!Rules.getNativeValue('commandPosOthers') && player !== null) 26 | return sender.sendMessage({ translate: 'rules.generic.blocked', with: ['commandPosOthers'] }); 27 | const target = player === null ? sender : world.getPlayers({ name: String(player) })[0]; 28 | if (!target) 29 | return sender.sendMessage({ translate: 'generic.player.notfound', with: [player] }); 30 | 31 | const message = { 32 | rawtext: [ 33 | getPositionText(player, target), { text: '\n' }, 34 | getDimensionText(target), { text: '\n' }, 35 | getRelativeDimensionPositionText(target) 36 | ] 37 | }; 38 | sender.sendMessage(message); 39 | } 40 | 41 | function getPositionText(player, target) { 42 | if (player === null) 43 | return { translate: 'commands.pos.self', with: [stringifyLocation(target.location, 2)] }; 44 | return { translate: 'commands.pos.other', with: [target.name, stringifyLocation(target.location, 2)] }; 45 | } 46 | 47 | function getDimensionText(target) { 48 | return { translate: 'commands.pos.dimension', with: [getColoredDimensionName(target.dimension.id)] }; 49 | } 50 | 51 | function getRelativeDimensionPositionText(target) { 52 | if (target.dimension.id === 'minecraft:nether') 53 | return { translate: 'commands.pos.relative.overworld', with: [stringifyLocation(netherPosToOverworld(target.location), 2)] }; 54 | else if (target.dimension.id === 'minecraft:overworld') 55 | return { translate: 'commands.pos.relative.nether', with: [stringifyLocation(overworldPosToNether(target.location), 2)] }; 56 | } 57 | 58 | function netherPosToOverworld(pos) { 59 | return { x: pos.x * NETHER_SCALE_FACTOR, y: pos.y, z: pos.z * NETHER_SCALE_FACTOR }; 60 | } 61 | 62 | function overworldPosToNether(pos) { 63 | return { x: pos.x / NETHER_SCALE_FACTOR, y: pos.y, z: pos.z / NETHER_SCALE_FACTOR }; 64 | } 65 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/removeentity.js: -------------------------------------------------------------------------------- 1 | import { Player, world } from '@minecraft/server'; 2 | import { Rule, Command } from 'lib/canopy/Canopy'; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'commandRemoveEntity', 7 | description: { translate: 'rules.commandRemoveEntity' } 8 | }); 9 | 10 | new Command({ 11 | name: 'removeentity', 12 | description: { translate: 'commands.removeentity' }, 13 | usage: 'removeentity [id]', 14 | args: [ 15 | { type: 'number', name: 'id' } 16 | ], 17 | callback: removeEntityCommand, 18 | contingentRules: ['commandRemoveEntity'] 19 | }); 20 | 21 | function removeEntityCommand(sender, args) { 22 | const { id } = args; 23 | const target = getTargetEntity(sender, id); 24 | if (target instanceof Player) { 25 | sender.sendMessage({ translate: 'commands.removeentity.fail.player' }); 26 | } else if (target) { 27 | target.remove(); 28 | sender.sendMessage({ translate: 'commands.removeentity.success', with: [target.typeId.replace('minecraft:', ''), target.id] }); 29 | } else if (id === null) { 30 | sender.sendMessage({ translate: 'generic.entity.notfound' }); 31 | } else { 32 | sender.sendMessage({ translate: 'commands.removeentity.fail.noid', with: [String(id)] }); 33 | } 34 | } 35 | 36 | function getTargetEntity(sender, id) { 37 | if (id === null) 38 | return sender.getEntitiesFromViewDirection({ ignoreBlockCollision: false, includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 16 })[0]?.entity; 39 | return world.getEntity(String(id)); 40 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/resetall.js: -------------------------------------------------------------------------------- 1 | import { world } from '@minecraft/server' 2 | import { Command } from 'lib/canopy/Canopy' 3 | 4 | new Command({ 5 | name: 'resetall', 6 | description: { translate: 'commands.resetall' }, 7 | usage: 'resetall', 8 | callback: resetallCommand, 9 | adminOnly: true 10 | }); 11 | 12 | function resetallCommand() { 13 | world.clearDynamicProperties(); 14 | const players = world.getAllPlayers(); 15 | players.forEach(player => { 16 | player?.clearDynamicProperties(); 17 | }); 18 | world.sendMessage({ translate: 'commands.resetall.success' }); 19 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/resettest.js: -------------------------------------------------------------------------------- 1 | import { Command } from 'lib/canopy/Canopy' 2 | import { counterChannels } from '../classes/CounterChannels'; 3 | import { generatorChannels } from "../classes/GeneratorChannels"; 4 | import { worldSpawns } from 'src/commands/spawn'; 5 | 6 | new Command({ 7 | name: 'resettest', 8 | description: { translate: 'commands.resettest' }, 9 | usage: 'resettest', 10 | callback: resettestCommand 11 | }) 12 | 13 | function resettestCommand(sender) { 14 | if (worldSpawns !== null) 15 | worldSpawns.reset(); 16 | counterChannels.resetAllCounts(); 17 | generatorChannels.resetAllCounts(); 18 | sender.sendMessage({ translate: 'commands.resettest.success' }); 19 | } 20 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/counter.js: -------------------------------------------------------------------------------- 1 | import { Rule } from "../../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | import { counterChannels } from "../../classes/CounterChannels"; 4 | import { broadcastActionBar, getScriptEventSourceName, formatColorStr } from "../../../include/utils"; 5 | 6 | system.afterEvents.scriptEventReceive.subscribe(async (event) => { 7 | if (event.id !== 'canopy:counter') return; 8 | if (!await Rule.getValue('hopperCounters')) 9 | return broadcastActionBar({ translate: 'rules.generic.blocked', with: ['hopperCounters'] }); 10 | const sourceName = getScriptEventSourceName(event); 11 | const message = event.message; 12 | 13 | if (message === '') { 14 | world.getAllPlayers().forEach(player => { counterChannels.getAllQueryOutput(player); }); 15 | } else if (counterChannels.isValidColor(message)) { 16 | world.getAllPlayers().forEach(player => { counterChannels.getQueryOutput(player, message); }); 17 | } else if (message === 'reset') { 18 | counterChannels.resetAllCounts(); 19 | broadcastActionBar({ translate: 'commands.counter.reset.all.actionbar', with: [sourceName] }); 20 | } 21 | const args = message.split(' '); 22 | if (counterChannels.isValidColor(args[0]) && args[1] === 'reset') { 23 | counterChannels.resetCounts(args[0]); 24 | broadcastActionBar({ translate: 'commands.counter.reset.single.actionbar', with: [sourceName, formatColorStr(args[0])] }); 25 | } 26 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/generator.js: -------------------------------------------------------------------------------- 1 | import { Rule } from "../../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | import { generatorChannels } from "../../classes/GeneratorChannels"; 4 | import { broadcastActionBar, getScriptEventSourceName, formatColorStr } from "../../../include/utils"; 5 | 6 | system.afterEvents.scriptEventReceive.subscribe(async (event) => { 7 | if (event.id !== 'canopy:generator') return; 8 | if (!await Rule.getValue('hopperGenerators')) 9 | return broadcastActionBar({ translate: 'rules.generic.blocked', with: ['hopperGenerators'] }); 10 | const sourceName = getScriptEventSourceName(event); 11 | const message = event.message; 12 | 13 | if (message === '') { 14 | world.getAllPlayers().forEach(player => { generatorChannels.getAllQueryOutput(player); }); 15 | } else if (generatorChannels.isValidColor(message)) { 16 | world.getAllPlayers().forEach(player => { generatorChannels.getQueryOutput(player, message); }); 17 | } else if (message === 'reset') { 18 | generatorChannels.resetAllCounts(); 19 | broadcastActionBar({ translate: 'commands.generator.reset.all.actionbar', with: [sourceName] }); 20 | } 21 | const args = message.split(' '); 22 | if (generatorChannels.isValidColor(args[0]) && args[1] === 'reset') { 23 | generatorChannels.resetCounts(args[0]); 24 | broadcastActionBar({ translate: 'commands.generator.reset.single.actionbar', with: [sourceName, formatColorStr(args[0])] }); 25 | } 26 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/loop.js: -------------------------------------------------------------------------------- 1 | import { system, world, DimensionTypes, Block } from "@minecraft/server"; 2 | import { getScriptEventSourceObject, isNumeric } from "../../../include/utils"; 3 | 4 | system.afterEvents.scriptEventReceive.subscribe((event) => { 5 | if (event.id !== "canopy:loop") return; 6 | const message = event.message; 7 | const args = message.match(/(\d+)\s+"([^"]+)"/)?.slice(1); 8 | if (!args) return; 9 | 10 | const source = getScriptEventSourceObject(event); 11 | let runLocation = source; 12 | if (source === 'Server') 13 | runLocation = world.getDimension(DimensionTypes.get('overworld')); 14 | else if (source === 'Unknown') 15 | new Error('Unknown source. Try running the command from somewhere else.'); 16 | else if (typeof source === Block) 17 | runLocation = source.dimension; 18 | loopCommand(args[0], args[1], runLocation); 19 | }); 20 | 21 | function loopCommand(times, command, runLocation) { 22 | if (!isNumeric(times)) 23 | new Error('Invalid arguments. Usage: loop '); 24 | for (let i = 0; i < times; i++) 25 | runLocation.runCommand(command); 26 | } 27 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/resettest.js: -------------------------------------------------------------------------------- 1 | import { system } from "@minecraft/server"; 2 | import { getScriptEventSourceName, broadcastActionBar } from "../../../include/utils"; 3 | import { counterChannels } from "../../classes/CounterChannels"; 4 | import { generatorChannels } from "../../classes/GeneratorChannels"; 5 | import { worldSpawns } from "../../commands/spawn"; 6 | 7 | system.afterEvents.scriptEventReceive.subscribe((event) => { 8 | if (event.id !== 'canopy:resettest') return; 9 | const sourceName = getScriptEventSourceName(event); 10 | if (worldSpawns !== null) 11 | worldSpawns.reset(); 12 | counterChannels.resetAllCounts(); 13 | generatorChannels.resetAllCounts(); 14 | broadcastActionBar({ translate: 'commands.resettest.success.actionbar', with: [sourceName] }); 15 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/spawn.js: -------------------------------------------------------------------------------- 1 | import { system, world } from "@minecraft/server"; 2 | import { worldSpawns } from "../../commands/spawn"; 3 | import { getScriptEventSourceName, broadcastActionBar } from "../../../include/utils"; 4 | 5 | system.afterEvents.scriptEventReceive.subscribe((event) => { 6 | if (event.id !== 'canopy:spawn') return; 7 | const sourceName = getScriptEventSourceName(event); 8 | const message = event.message; 9 | 10 | if (message === 'test') resetSpawnCounters(sourceName); 11 | if (message === 'tracking') printTrackingStatus(); 12 | }); 13 | 14 | function resetSpawnCounters(sourceName) { 15 | if (worldSpawns === null) 16 | return broadcastActionBar({ translate: 'commands.spawn.tracking.no' }); 17 | worldSpawns.reset(); 18 | broadcastActionBar({ translate: 'commands.spawn.tracking.reset.success.actionbar', with: [sourceName] }); 19 | } 20 | 21 | function printTrackingStatus() { 22 | if (worldSpawns === null) 23 | return broadcastActionBar({ translate: 'commands.spawn.tracking.no' }); 24 | world.sendMessage(worldSpawns.getOutput()); 25 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/scriptevents/tick.js: -------------------------------------------------------------------------------- 1 | import { Rules } from "../../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | import { broadcastActionBar, getScriptEventSourceName, isNumeric } from "../../../include/utils"; 4 | 5 | system.afterEvents.scriptEventReceive.subscribe((event) => { 6 | if (event.id !== "canopy:tick") return; 7 | if (!Rules.getNativeValue('commandTick')) { 8 | broadcastActionBar({ translate: 'rules.generic.blocked', with: ['commandTick'] }); 9 | return; 10 | } 11 | const message = event.message; 12 | const args = message.split(' '); 13 | const sourceName = getScriptEventSourceName(event); 14 | if (args[0] === "sleep" && isNumeric(args[1])) 15 | tickSleep(sourceName, args[1]); 16 | }); 17 | 18 | function tickSleep(sourceName, milliseconds) { 19 | if (milliseconds === null || milliseconds < 1) return; 20 | world.sendMessage({ translate: 'command.tick.sleep.success', with: [sourceName, String(milliseconds)] }); 21 | const startTime = Date.now(); 22 | let waitTime = 0; 23 | while (waitTime < milliseconds) 24 | waitTime = Date.now() - startTime; 25 | 26 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/sit.js: -------------------------------------------------------------------------------- 1 | import { Command } from "../../lib/canopy/Canopy"; 2 | import { playerSit } from "../rules/playerSit"; 3 | 4 | new Command({ 5 | name: 'sit', 6 | description: { translate: 'commands.sit' }, 7 | usage: 'sit', 8 | args: [ 9 | { type: 'number', name: 'distance' } 10 | ], 11 | callback: sitCommand, 12 | contingentRules: ['playerSit'] 13 | }); 14 | 15 | function sitCommand(sender, args) { 16 | if (sender?.getComponent('riding')?.entityRidingOn) 17 | return sender.sendMessage({ translate: 'commands.sit.busy' }); 18 | playerSit.sit(sender, args); 19 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/commands/tntfuse.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules, Command } from "../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | import { isNumeric } from "../../include/utils"; 4 | 5 | const MIN_FUSE_TICKS = 1; 6 | const MAX_FUSE_TICKS = 72000; 7 | 8 | new Rule({ 9 | category: 'Rules', 10 | identifier: 'commandTntFuse', 11 | description: { translate: 'rules.commandTntFuse' } 12 | }); 13 | 14 | const cmd = new Command({ 15 | name: 'tntfuse', 16 | description: { translate: 'commands.tntfuse' }, 17 | usage: 'tntfuse ', 18 | args: [ 19 | { type: 'number|string', name: 'ticks' } 20 | ], 21 | callback: tntfuseCommand, 22 | contingentRules: ['commandTntFuse'] 23 | }); 24 | 25 | world.afterEvents.entitySpawn.subscribe((event) => { 26 | if (event.entity?.typeId !== 'minecraft:tnt' || event.cause === 'Event') return; 27 | const fuseTimeProperty = world.getDynamicProperty('tntFuseTime'); 28 | let fuseTime = 80; 29 | if (fuseTimeProperty !== undefined && Rules.getNativeValue('commandTntFuse')) 30 | fuseTime = fuseTimeProperty; 31 | if (fuseTime === 1) { 32 | event.entity.triggerEvent('canopy:explode'); 33 | } else { 34 | event.entity.triggerEvent('canopy:fuse'); 35 | system.runTimeout(() => { 36 | if (event.entity.isValid) 37 | event.entity.triggerEvent('canopy:explode'); 38 | }, fuseTime - 1); 39 | } 40 | }); 41 | 42 | function tntfuseCommand(sender, args) { 43 | let { ticks } = args; 44 | if (ticks === 'reset') { 45 | ticks = 80; 46 | sender.sendMessage({ translate: 'commands.tntfuse.reset.success' }); 47 | setFuseTime(ticks); 48 | } else if (isNumeric(ticks) && ticks >= MIN_FUSE_TICKS && ticks <= MAX_FUSE_TICKS) { 49 | sender.sendMessage({ translate: 'commands.tntfuse.set.success', with: [String(ticks)] }); 50 | setFuseTime(ticks); 51 | } else if (!isNumeric(ticks) || ticks < MIN_FUSE_TICKS || ticks > MAX_FUSE_TICKS) { 52 | sender.sendMessage({ translate: 'commands.tntfuse.set.fail', with: [String(ticks), String(MIN_FUSE_TICKS), String(MAX_FUSE_TICKS)] }); 53 | } else { 54 | cmd.sendUsage(sender); 55 | } 56 | } 57 | 58 | function setFuseTime(ticks) { 59 | world.setDynamicProperty('tntFuseTime', Number(ticks)); 60 | } 61 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/events/PlayerStartSneakEvent.js: -------------------------------------------------------------------------------- 1 | import { system, world, InputButton, ButtonState } from "@minecraft/server"; 2 | 3 | class PlayerStartSneakEvent { 4 | runner; 5 | playersSneakingThisTick = []; 6 | playersSneakingLastTick = []; 7 | callbacks = []; 8 | 9 | constructor() { 10 | this.playersSneakingThisTick = []; 11 | this.playersSneakingLastTick = []; 12 | this.callbacks = []; 13 | } 14 | 15 | subscribe(callback) { 16 | if (!this.isTracking()) 17 | this.startSneakTracking(callback); 18 | this.callbacks.push(callback); 19 | } 20 | 21 | unsubscribe(callback) { 22 | this.removeCallback(callback); 23 | if (this.callbacks.length === 0) 24 | this.endSneakTracking(); 25 | } 26 | 27 | startSneakTracking() { 28 | this.playerSneakingLastTick = system.currentTick; 29 | this.runner = system.runInterval(this.onTick.bind(this)); 30 | } 31 | 32 | onTick() { 33 | this.updateSneaks(); 34 | this.sendEvents(); 35 | } 36 | 37 | updateSneaks() { 38 | this.playersSneakingLastTick = [...this.playersSneakingThisTick]; 39 | this.playersSneakingThisTick = []; 40 | world.getAllPlayers().forEach(player => { 41 | if (this.isPlayerSneaking(player)) 42 | this.playersSneakingThisTick.push(player); 43 | }); 44 | } 45 | 46 | sendEvents() { 47 | const playersStartedSneak = this.getPlayersWhoStartedSneaking(); 48 | if (playersStartedSneak.length === 0) return; 49 | this.callbacks.forEach(callback => { 50 | const event = { 51 | players: playersStartedSneak 52 | } 53 | callback(event); 54 | }); 55 | } 56 | 57 | getPlayersWhoStartedSneaking() { 58 | return this.playersSneakingThisTick.filter(player => 59 | !this.playersSneakingLastTick.includes(player) 60 | ); 61 | } 62 | 63 | removeCallback(callback) { 64 | this.callbacks = this.callbacks.filter(cb => cb !== callback); 65 | } 66 | 67 | isTracking() { 68 | return this.callbacks.length > 0; 69 | } 70 | 71 | isPlayerSneaking(player) { 72 | return player && player.inputInfo.getButtonState(InputButton.Sneak) === ButtonState.Pressed; 73 | } 74 | 75 | endSneakTracking() { 76 | system.clearRun(this.runner); 77 | } 78 | } 79 | 80 | const playerStartSneakEvent = new PlayerStartSneakEvent(); 81 | 82 | export { PlayerStartSneakEvent, playerStartSneakEvent }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/onReload.js: -------------------------------------------------------------------------------- 1 | import { world } from '@minecraft/server'; 2 | import { broadcastActionBar } from "../include/utils"; 3 | import ProbeManager from "./classes/ProbeManager"; 4 | 5 | world.afterEvents.worldLoad.subscribe(() => { 6 | const players = world.getAllPlayers(); 7 | if (players[0]?.isValid) { 8 | broadcastActionBar('§aBehavior packs have been reloaded.'); 9 | ProbeManager.startCleanupCycle(); 10 | } 11 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/onStart.js: -------------------------------------------------------------------------------- 1 | import { world, system } from "@minecraft/server"; 2 | import ProbeManager from "./classes/ProbeManager"; 3 | import { displayWelcome } from "./rules/noWelcomeMessage"; 4 | 5 | let worldIsValid = false; 6 | 7 | world.afterEvents.playerJoin.subscribe((event) => { 8 | const runner = system.runInterval(() => { 9 | const players = world.getPlayers({ name: event.playerName }); 10 | players.forEach(player => { 11 | if (!player) return; 12 | if (player?.isValid) { 13 | system.clearRun(runner); 14 | onValidPlayer(player); 15 | if (!worldIsValid) 16 | onValidWorld(); 17 | worldIsValid = true; 18 | } 19 | }); 20 | }); 21 | }); 22 | 23 | function onValidPlayer(player) { 24 | displayWelcome(player); 25 | } 26 | 27 | function onValidWorld() { 28 | ProbeManager.startCleanupCycle(); 29 | } 30 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/allowBubbleColumnPlacement.js: -------------------------------------------------------------------------------- 1 | import { GlobalRule } from "../../lib/canopy/Canopy"; 2 | import { world, system } from '@minecraft/server'; 3 | 4 | class AllowBubbleColumnPlacement extends GlobalRule { 5 | constructor() { 6 | super({ 7 | identifier: 'allowBubbleColumnPlacement', 8 | onEnableCallback: () => this.subscribeToEvent(), 9 | onDisableCallback: () => this.unsubscribeFromEvent() 10 | }); 11 | this.onPlayerPlaceBlockBound = this.onPlayerPlaceBlock.bind(this); 12 | } 13 | 14 | subscribeToEvent() { 15 | world.beforeEvents.playerPlaceBlock.subscribe(this.onPlayerPlaceBlockBound); 16 | } 17 | 18 | unsubscribeFromEvent() { 19 | world.beforeEvents.playerPlaceBlock.unsubscribe(this.onPlayerPlaceBlockBound); 20 | } 21 | 22 | onPlayerPlaceBlock(event) { 23 | if (!event.player) return; 24 | system.run(() => { 25 | if (this.hasBubbleColumnInMainhand(event)) 26 | this.placeBubbleColumn(event.dimension, event.block.location); 27 | }); 28 | } 29 | 30 | hasBubbleColumnInMainhand(event) { 31 | return event.player.getComponent('equippable').getEquipment('Mainhand')?.typeId === 'minecraft:bubble_column'; 32 | } 33 | 34 | placeBubbleColumn(dimension, location) { 35 | world.structureManager.place('mystructure:bubble_column', dimension, location); 36 | } 37 | } 38 | 39 | export const allowBubbleColumnPlacement = new AllowBubbleColumnPlacement(); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/allowPeekInventory.js: -------------------------------------------------------------------------------- 1 | import { GlobalRule } from '../../lib/canopy/Canopy'; 2 | import { world, EntityComponentTypes } from '@minecraft/server'; 3 | import { InventoryUI } from '../classes/InventoryUI'; 4 | 5 | class AllowPeekInventory extends GlobalRule { 6 | peekItemId = 'minecraft:spyglass'; 7 | 8 | constructor() { 9 | super({ 10 | identifier: 'allowPeekInventory', 11 | onEnableCallback: () => this.subscribeToEvents(), 12 | onDisableCallback: () => this.unsubscribeFromEvents() 13 | }); 14 | this.onPlayerInteractionBound = this.onPlayerInteraction.bind(this); 15 | } 16 | 17 | subscribeToEvents() { 18 | world.beforeEvents.playerInteractWithBlock.subscribe(this.onPlayerInteractionBound); 19 | world.beforeEvents.playerInteractWithEntity.subscribe(this.onPlayerInteractionBound); 20 | } 21 | 22 | unsubscribeFromEvents() { 23 | world.beforeEvents.playerInteractWithBlock.unsubscribe(this.onPlayerInteractionBound); 24 | world.beforeEvents.playerInteractWithEntity.unsubscribe(this.onPlayerInteractionBound); 25 | } 26 | 27 | onPlayerInteraction(event) { 28 | if (!event.player || event.itemStack?.typeId !== this.peekItemId) return; 29 | const target = event.block || event.target; 30 | if (!this.hasInventory(target)) return; 31 | event.cancel = true; 32 | const invUI = new InventoryUI(target); 33 | invUI.show(event.player); 34 | } 35 | 36 | hasInventory(target) { 37 | return target?.getComponent(EntityComponentTypes.Inventory) !== undefined; 38 | } 39 | } 40 | 41 | export const allowPeekInventory = new AllowPeekInventory(); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/armorStandRespawning.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'armorStandRespawning', 7 | description: { translate: 'rules.armorStandRespawning' } 8 | }); 9 | 10 | world.afterEvents.projectileHitEntity.subscribe((event) => { 11 | if (!Rules.getNativeValue('armorStandRespawning') || event.projectile.typeId === "minecraft:fishing_hook") return; 12 | const entity = event.getEntityHit().entity; 13 | if (entity?.typeId === "minecraft:armor_stand") { 14 | const hasCleanedItem = cleanDroppedItem(event); 15 | if (hasCleanedItem) 16 | event.dimension.spawnEntity(entity.typeId, event.location); 17 | } 18 | }); 19 | 20 | function cleanDroppedItem(event) { 21 | const nearbyItems = event.dimension.getEntities({ location: event.location, maxDistance: 2, type: "minecraft:item" }); 22 | 23 | for (const itemEntity of nearbyItems) { 24 | if (itemEntity?.typeId !== "minecraft:item") continue; 25 | const itemStack = itemEntity.getComponent("minecraft:item")?.itemStack; 26 | if (itemStack.typeId === "minecraft:armor_stand" && itemStack.amount === 1) { 27 | itemEntity.remove(); 28 | return true; 29 | } 30 | } 31 | return false; 32 | } 33 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/creativeInstantTame.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'creativeInstantTame', 7 | description: { translate: 'rules.creativeInstantTame' } 8 | }); 9 | 10 | 11 | world.beforeEvents.playerInteractWithEntity.subscribe((event) => { 12 | if (!Rules.getNativeValue('creativeInstantTame') || event.player?.getGameMode() !== 'creative') return; 13 | const tameable = event.target?.getComponent('tameable'); 14 | if (tameable !== undefined && isUsingTameItem(tameable.getTameItems, event.itemStack)) { 15 | system.run(() => { 16 | try { 17 | tameable.tame(event.player); 18 | } catch { 19 | // was already tamed 20 | } 21 | }); 22 | } 23 | }); 24 | 25 | function isUsingTameItem(tameItemStacks, playeritemStack) { 26 | return tameItemStacks.map(stack => stack.typeId).includes(playeritemStack?.typeId); 27 | } 28 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/creativeNoTileDrops.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { system, world } from "@minecraft/server"; 3 | import { calcDistance } from "../../include/utils"; 4 | 5 | const REMOVAL_DISTANCE = 2.5; 6 | 7 | new Rule({ 8 | category: 'Rules', 9 | identifier: 'creativeNoTileDrops', 10 | description: { translate: 'rules.creativeNoTileDrops' } 11 | }); 12 | 13 | let brokenBlockEventsThisTick = []; 14 | let brokenBlockEventsLastTick = []; 15 | 16 | system.runInterval(() => { 17 | brokenBlockEventsLastTick = brokenBlockEventsThisTick; 18 | brokenBlockEventsThisTick = []; 19 | }); 20 | 21 | world.afterEvents.playerBreakBlock.subscribe((blockEvent) => { 22 | if (blockEvent.player?.getGameMode() !== 'creative' 23 | || !Rules.getNativeValue('creativeNoTileDrops')) 24 | return; 25 | brokenBlockEventsThisTick.push(blockEvent); 26 | }); 27 | 28 | world.afterEvents.entitySpawn.subscribe((entityEvent) => { 29 | if (entityEvent.cause !== 'Spawned' || entityEvent.entity.typeId !== 'minecraft:item') return; 30 | if (!Rules.getNativeValue('creativeNoTileDrops')) return; 31 | 32 | const item = entityEvent.entity; 33 | const brokenBlockEvents = brokenBlockEventsThisTick.concat(brokenBlockEventsLastTick); 34 | const brokenBlockEvent = brokenBlockEvents.find(blockEvent => isItemWithinRemovalDistance(blockEvent.block.location, item)); 35 | if (!brokenBlockEvent) return; 36 | 37 | item.remove(); 38 | }); 39 | 40 | function isItemWithinRemovalDistance(location, item) { 41 | return calcDistance(location, item.location) < REMOVAL_DISTANCE; 42 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/creativeOneHitKill.js: -------------------------------------------------------------------------------- 1 | import { world, InputButton, ButtonState } from "@minecraft/server"; 2 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'creativeOneHitKill', 7 | description: { translate: 'rules.creativeOneHitKill' } 8 | }); 9 | 10 | world.afterEvents.entityHitEntity.subscribe((event) => { 11 | if (!Rules.getNativeValue('creativeOneHitKill') || event.damagingEntity?.typeId !== 'minecraft:player') return; 12 | const player = event.damagingEntity; 13 | if (player.getGameMode() === 'creative') { 14 | if (player.inputInfo.getButtonState(InputButton.Sneak) === ButtonState.Pressed) { 15 | player.dimension.getEntities({ location: event.hitEntity.location, maxDistance: 3 }).forEach(entity => { 16 | if (['item', 'player', 'experience_orb'].includes(entity.typeId.replace('minecraft:', ''))) return; 17 | entity?.kill(); 18 | }); 19 | } else { 20 | if (event.hitEntity?.typeId === 'minecraft:player') return; 21 | event.hitEntity?.kill(); 22 | } 23 | } 24 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/durabilityNotifier.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules} from "../../lib/canopy/Canopy"; 2 | import { world, GameMode } from '@minecraft/server'; 3 | 4 | const ACTIVE_DURABILITY = 2; 5 | const ADDITIONAL_DURABILITIES = [20]; 6 | 7 | const rule = new Rule({ 8 | category: 'Rules', 9 | identifier: 'durabilityNotifier', 10 | description: { translate: 'rules.durabilityNotifier', with: [ACTIVE_DURABILITY.toString()] } 11 | }); 12 | 13 | world.afterEvents.playerBreakBlock.subscribe((event) => durabilityClink(event.player, event.itemStackBeforeBreak, event.itemStackAfterBreak)); 14 | world.afterEvents.playerInteractWithBlock.subscribe((event) => durabilityClink(event.player, event.beforeItemStack, event.itemStack)); 15 | world.afterEvents.playerInteractWithEntity.subscribe((event) => durabilityClink(event.player, event.beforeItemStack, event.itemStack)); 16 | 17 | function durabilityClink(player, beforeItemStack, itemStack) { 18 | if (!Rules.getNativeValue(rule.getID()) || !player || !itemStack || !beforeItemStack 19 | || player.getGameMode() === GameMode.creative 20 | || !usedDurability(beforeItemStack, itemStack) 21 | ) return; 22 | const durability = getRemainingDurability(itemStack); 23 | if (ADDITIONAL_DURABILITIES.includes(durability)) 24 | showNotification(player, durability); 25 | 26 | if (durability <= ACTIVE_DURABILITY) { 27 | const pitch = 1 - (durability/5); 28 | showNotification(player, durability, pitch); 29 | } 30 | } 31 | 32 | function usedDurability(beforeItemStack, afterItemStack) { 33 | return afterItemStack.hasComponent('durability') && beforeItemStack.hasComponent('durability') 34 | && (beforeItemStack.getComponent('durability').damage < afterItemStack.getComponent('durability').damage); 35 | } 36 | 37 | function getRemainingDurability(itemStack) { 38 | const durabilityComponent = itemStack.getComponent('durability'); 39 | if (!durabilityComponent) return undefined; 40 | return durabilityComponent.maxDurability - durabilityComponent.damage; 41 | } 42 | 43 | function showNotification(player, durability, pitch = undefined) { 44 | if (pitch !== undefined) 45 | player.playSound('note.xylophone', { pitch }); 46 | player.onScreenDisplay.setActionBar({ translate: 'rules.durabilityNotifier.alert', with: [String(durability)] }); 47 | } 48 | 49 | export { usedDurability, getRemainingDurability }; 50 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/durabilitySwap.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules} from "../../lib/canopy/Canopy"; 2 | import { world, GameMode } from '@minecraft/server'; 3 | import { usedDurability, getRemainingDurability } from 'src/rules/durabilityNotifier'; 4 | 5 | const rule = new Rule({ 6 | category: 'Rules', 7 | identifier: 'durabilitySwap', 8 | description: { translate: 'rules.durabilitySwap' } 9 | }); 10 | 11 | world.afterEvents.playerBreakBlock.subscribe((event) => durabilitySwap(event.player, event.itemStackBeforeBreak, event.itemStackAfterBreak)); 12 | world.afterEvents.playerInteractWithBlock.subscribe((event) => durabilitySwap(event.player, event.beforeItemStack, event.itemStack)); 13 | world.afterEvents.playerInteractWithEntity.subscribe((event) => durabilitySwap(event.player, event.beforeItemStack, event.itemStack)); 14 | 15 | function durabilitySwap(player, beforeItemStack, itemStack) { 16 | if (!Rules.getNativeValue(rule.getID()) || !player || !itemStack || !beforeItemStack 17 | || player.getGameMode() === GameMode.creative 18 | || !usedDurability(beforeItemStack, itemStack) 19 | ) return; 20 | const durability = getRemainingDurability(itemStack); 21 | if (durability === 0) 22 | swapOutItem(player); 23 | } 24 | 25 | function swapOutItem(player) { 26 | const playerInventory = player.getComponent('inventory')?.container; 27 | if (!playerInventory) return; 28 | let swapSlot = findEmptySlot(playerInventory); 29 | if (swapSlot === -1) 30 | swapSlot = findSlotWithoutDurabilityComponent(playerInventory); 31 | if (swapSlot === -1) 32 | swapSlot = findSlotWithSomeDurability(playerInventory); 33 | if (swapSlot === -1) return; 34 | playerInventory.swapItems(player.selectedSlotIndex, swapSlot, playerInventory); 35 | } 36 | 37 | function findEmptySlot(playerInventory) { 38 | for (let slotIndex = 9; slotIndex < playerInventory.size; slotIndex++) { 39 | const slot = playerInventory.getSlot(slotIndex); 40 | if (!slot.hasItem()) 41 | return slotIndex; 42 | 43 | } 44 | return -1; 45 | } 46 | 47 | function findSlotWithoutDurabilityComponent(playerInventory) { 48 | for (let slotIndex = 0; slotIndex < playerInventory.size; slotIndex++) { 49 | const slot = playerInventory.getSlot(slotIndex); 50 | if (slot.hasItem() && !slot.getItem()?.hasComponent('durability')) 51 | return slotIndex; 52 | 53 | } 54 | return -1; 55 | } 56 | 57 | function findSlotWithSomeDurability(playerInventory) { 58 | for (let slotIndex = 0; slotIndex < playerInventory.size; slotIndex++) { 59 | const slot = playerInventory.getSlot(slotIndex); 60 | if (slot.hasItem() && getRemainingDurability(slot.getItem()) > 0) 61 | return slotIndex; 62 | 63 | } 64 | return -1; 65 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/entityInstantDeath.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'entityInstantDeath', 7 | description: { translate: 'rules.entityInstantDeath' } 8 | }); 9 | 10 | world.afterEvents.entityDie.subscribe(async (event) => { 11 | if (!await Rules.getNativeValue('entityInstantDeath')) return; 12 | try { 13 | event.deadEntity.remove(); 14 | } catch { 15 | // already dead 16 | } 17 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/explosionChainReactionOnly.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'explosionChainReactionOnly', 7 | description: { translate: 'rules.explosionChainReactionOnly' }, 8 | independentRules: ['explosionNoBlockDamage', 'explosionOff'] 9 | }); 10 | 11 | world.beforeEvents.explosion.subscribe((event) => { 12 | if (!Rules.getNativeValue('explosionChainReactionOnly')) return; 13 | const explodedTntBlocks = event.getImpactedBlocks().filter(block => block.typeId === 'minecraft:tnt'); 14 | event.setImpactedBlocks(explodedTntBlocks); 15 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/explosionNoBlockDamage.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'explosionNoBlockDamage', 7 | description: { translate: 'rules.explosionNoBlockDamage' }, 8 | independentRules: ['explosionChainReactionOnly', 'explosionOff'] 9 | }); 10 | 11 | world.beforeEvents.explosion.subscribe((event) => { 12 | if (!Rules.getNativeValue('explosionNoBlockDamage')) return; 13 | event.setImpactedBlocks([]); 14 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/explosionOff.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'explosionOff', 7 | description: { translate: 'rules.explosionOff' }, 8 | independentRules: ['explosionChainReactionOnly', 'explosionNoBlockDamage'] 9 | }); 10 | 11 | world.beforeEvents.explosion.subscribe((event) => { 12 | if (!Rules.getNativeValue('explosionOff')) return; 13 | event.cancel = true; 14 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/hotbarSwitching.js: -------------------------------------------------------------------------------- 1 | import { Rule } from "lib/canopy/Canopy"; 2 | import { system, world, InputButton, ButtonState, EntityComponentTypes } from '@minecraft/server'; 3 | import HotbarManager from 'src/classes/HotbarManager'; 4 | 5 | let runner; 6 | new Rule({ 7 | category: 'Rules', 8 | identifier: 'hotbarSwitching', 9 | description: { translate: 'rules.hotbarSwitching' }, 10 | onEnableCallback: () => { 11 | runner = system.runInterval(onTick.bind(this)); 12 | }, 13 | onDisableCallback: () => { 14 | system.clearRun(runner); 15 | } 16 | }); 17 | 18 | const ARROW_SLOT = 17; 19 | const lastSelectedSlots = {}; 20 | const hotbarManagers = {}; 21 | 22 | function onTick() { 23 | const players = world.getAllPlayers(); 24 | for (const player of players) { 25 | if (!player) continue; 26 | if (!hasAppropriateGameMode(player)) continue; 27 | if (hotbarManagers[player.id] === undefined) 28 | hotbarManagers[player.id] = new HotbarManager(player); 29 | processHotbarSwitching(player); 30 | } 31 | } 32 | 33 | function hasAppropriateGameMode(player) { 34 | return player.getGameMode() === 'creative'; 35 | } 36 | 37 | function processHotbarSwitching(player) { 38 | if (lastSelectedSlots[player.id] !== undefined && (!hasArrowInCorrectSlot(player) || !hasAppropriateGameMode(player))) { 39 | delete lastSelectedSlots[player.id]; 40 | return; 41 | } 42 | if (lastSelectedSlots[player.id] === undefined && (!hasArrowInCorrectSlot(player) || !hasAppropriateGameMode(player))) 43 | return; 44 | if (hasScrolled(player) && player.inputInfo.getButtonState(InputButton.Sneak) === ButtonState.Pressed) 45 | switchToHotbar(player, player.selectedSlotIndex); 46 | lastSelectedSlots[player.id] = player.selectedSlotIndex; 47 | } 48 | 49 | function switchToHotbar(player, index) { 50 | const hotbarMgr = hotbarManagers[player.id]; 51 | hotbarMgr.saveHotbar(); 52 | hotbarMgr.loadHotbar(index); 53 | player.onScreenDisplay.setActionBar(`§a${index + 1}`); 54 | } 55 | 56 | function hasArrowInCorrectSlot(player) { 57 | const container = player.getComponent(EntityComponentTypes.Inventory)?.container; 58 | return container?.getItem(ARROW_SLOT)?.typeId === 'minecraft:arrow'; 59 | } 60 | 61 | function hasScrolled(player) { 62 | return player.selectedSlotIndex !== lastSelectedSlots[player.id]; 63 | } 64 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/Biome.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import ProbeManager from '../../classes/ProbeManager'; 3 | 4 | class Biome extends InfoDisplayElement { 5 | player; 6 | 7 | constructor(player, displayLine) { 8 | const ruleData = { 9 | identifier: 'biome', 10 | description: { translate: 'rules.infoDisplay.biome' }, 11 | onDisableCallback: () => ProbeManager.removeProbe(player) 12 | }; 13 | super(ruleData, displayLine); 14 | this.player = player; 15 | } 16 | 17 | getFormattedDataOwnLine() { 18 | return { translate: 'rules.infoDisplay.biome.display', with: [ProbeManager.getBiome(this.player)] }; 19 | } 20 | 21 | getFormattedDataSharedLine() { 22 | return this.getFormattedDataOwnLine(); 23 | } 24 | } 25 | 26 | export default Biome; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/ChunkCoords.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | 3 | class ChunkCoords extends InfoDisplayElement { 4 | constructor(player, displayLine) { 5 | const ruleData = { identifier: 'chunkCoords', description: { translate: 'rules.infoDisplay.chunkCoords' } }; 6 | super(ruleData, displayLine); 7 | this.player = player; 8 | } 9 | 10 | getFormattedDataOwnLine() { 11 | const chunkLocation = this.getChunkCoords(this.player); 12 | return { translate: 'rules.infoDisplay.chunkCoords.display', with: [`${chunkLocation.x} ${chunkLocation.y} ${chunkLocation.z}`] }; 13 | } 14 | 15 | getFormattedDataSharedLine() { 16 | return this.getFormattedDataOwnLine(); 17 | } 18 | 19 | getChunkCoords() { 20 | const chunkX = Math.floor(this.player.location.x / 16); 21 | const chunkY = Math.floor(this.player.location.y / 16); 22 | const chunkZ = Math.floor(this.player.location.z / 16); 23 | return { x: chunkX, y: chunkY, z: chunkZ }; 24 | } 25 | } 26 | 27 | export default ChunkCoords; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/Entities.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from "./InfoDisplayElement.js"; 2 | import { Vector } from "../../../lib/Vector.js"; 3 | 4 | class Entities extends InfoDisplayElement { 5 | player; 6 | 7 | constructor(player, displayLine) { 8 | const ruleData = { identifier: 'entities', description: { translate: 'rules.infoDisplay.entities' } }; 9 | super(ruleData, displayLine); 10 | this.player = player; 11 | } 12 | 13 | getFormattedDataOwnLine() { 14 | return { translate: 'rules.infoDisplay.entities.display', with: [this.getEntitiesOnScreenCount()] }; 15 | } 16 | 17 | getFormattedDataSharedLine() { 18 | return this.getFormattedDataOwnLine(); 19 | } 20 | 21 | getEntitiesOnScreenCount() { // author: jeanmajid 22 | const viewDirection = Vector.normalize(this.player.getViewDirection()); 23 | const entities = this.player.dimension.getEntities({ location: this.player.location, maxDistance: 96 }); 24 | 25 | let count = 0; 26 | for (const entity of entities) { 27 | if (!entity) continue; 28 | try{ 29 | const toEntity = Vector.normalize(Vector.subtract(entity.location, this.player.location)); 30 | const dotProduct = Vector.dot(viewDirection, toEntity); 31 | if (dotProduct > 0.4) count++; 32 | } catch (error) { 33 | if (error.message.includes('property')) continue; // Entity has despawned 34 | throw error; 35 | } 36 | } 37 | return String(count); 38 | } 39 | } 40 | 41 | export default Entities; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/EventTrackers.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import { getAllTrackerInfoString } from 'src/commands/trackevent'; 3 | 4 | class EventTrackers extends InfoDisplayElement { 5 | constructor(displayLine) { 6 | const ruleData = { identifier: 'eventTrackers', description: { translate: 'rules.infoDisplay.eventTrackers' } }; 7 | super(ruleData, displayLine, true); 8 | } 9 | 10 | getFormattedDataOwnLine() { 11 | return { text: getAllTrackerInfoString().join('\n') }; 12 | } 13 | 14 | getFormattedDataSharedLine() { 15 | return this.getFormattedDataOwnLine(); 16 | } 17 | } 18 | 19 | export default EventTrackers; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/HopperCounterCounts.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from "./InfoDisplayElement"; 2 | import { counterChannels } from "../../classes/CounterChannels"; 3 | import { getColorCode } from "../../../include/utils"; 4 | 5 | class HopperCounterCounts extends InfoDisplayElement { 6 | constructor(displayLine) { 7 | const ruleData = { identifier: 'hopperCounterCounts', description: { translate: 'rules.infoDisplay.hopperCounterCounts' } }; 8 | super(ruleData, displayLine, true); 9 | } 10 | 11 | getFormattedDataOwnLine() { 12 | const activeChannels = counterChannels.getActiveChannels(); 13 | let output = ''; 14 | if (activeChannels.length > 0 && activeChannels.length <= 4) 15 | output += 'Counters: '; 16 | for (let i = 0; i < activeChannels.length; i++) { 17 | if (i !== 0 && (i % 4) === 0) 18 | output += '\n'; 19 | const channel = activeChannels[i]; 20 | output += getColorCode(channel.color); 21 | if (channel.isEmpty()) 22 | output += 'N/A'; 23 | else 24 | output += channel.getModedTotalCount(); 25 | output += '§r '; 26 | } 27 | output += '\n'; 28 | return { text: output.trim() }; 29 | } 30 | 31 | getFormattedDataSharedLine() { 32 | return this.getFormattedDataOwnLine(); 33 | } 34 | } 35 | 36 | export default HopperCounterCounts; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/Light.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import ProbeManager from '../../classes/ProbeManager'; 3 | 4 | class Light extends InfoDisplayElement { 5 | player; 6 | 7 | constructor(player, displayLine) { 8 | const ruleData = { 9 | identifier: 'light', 10 | description: { translate: 'rules.infoDisplay.light' }, 11 | onDisableCallback: () => ProbeManager.removeProbe(player) 12 | }; 13 | super(ruleData, displayLine); 14 | this.player = player; 15 | } 16 | 17 | getFormattedDataOwnLine() { 18 | return { translate: 'rules.infoDisplay.light.display', with: [String(ProbeManager.getLightLevel(this.player))] }; 19 | } 20 | 21 | getFormattedDataSharedLine() { 22 | return this.getFormattedDataOwnLine(); 23 | } 24 | } 25 | 26 | export default Light; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/LookingAt.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from "./InfoDisplayElement"; 2 | import { getRaycastResults, parseName, stringifyLocation } from "../../../include/utils"; 3 | 4 | class LookingAt extends InfoDisplayElement { 5 | constructor(player, displayLine) { 6 | const ruleData = { identifier: 'lookingAt', description: { translate: 'rules.infoDisplay.lookingAt' } }; 7 | super(ruleData, displayLine); 8 | this.player = player; 9 | } 10 | 11 | getFormattedDataOwnLine() { 12 | return { text: String(this.getLookingAtName()) }; 13 | } 14 | 15 | getFormattedDataSharedLine() { 16 | return this.getFormattedDataOwnLine(); 17 | } 18 | 19 | getLookingAtName() { 20 | const { blockRayResult, entityRayResult } = getRaycastResults(this.player, 7); 21 | return this.#parseLookingAtEntity(entityRayResult).LookingAtName || this.#parseLookingAtBlock(blockRayResult).LookingAtName; 22 | } 23 | 24 | #parseLookingAtBlock(lookingAtBlock) { 25 | let blockName = ''; 26 | let raycastHitFace; 27 | const block = lookingAtBlock?.block ?? undefined; 28 | if (block) { 29 | raycastHitFace = lookingAtBlock.face; 30 | try { 31 | blockName = `§a${parseName(block)}`; 32 | } catch (error) { 33 | if (error.message.includes('loaded')) 34 | blockName = `§c${stringifyLocation(block.location, 0)} Unloaded`; 35 | else if (error.message.includes('undefined')) 36 | blockName = '§7Undefined'; 37 | } 38 | } 39 | return { LookingAtName: blockName, LookingAtFace: raycastHitFace, LookingAtLocation: block?.location, LookingAtBlock: block }; 40 | } 41 | 42 | #parseLookingAtEntity(lookingAtEntities) { 43 | let entityName; 44 | const entity = lookingAtEntities[0]?.entity ?? undefined; 45 | if (entity) { 46 | try { 47 | entityName = `§a${parseName(entity)}`; 48 | if (entity.typeId === 'minecraft:player') 49 | entityName = `§a§o${entity.name}§r`; 50 | } catch (error) { 51 | if (error.message.includes('loaded')) 52 | entityName = `§c${stringifyLocation(entity.location, 0)} Unloaded`; 53 | else if (error.message.includes('undefined')) 54 | entityName = '§7Undefined'; 55 | } 56 | } 57 | return { LookingAtName: entityName, LookingAtEntity: entity } 58 | } 59 | } 60 | 61 | export default LookingAt; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/MoonPhase.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import { world } from '@minecraft/server'; 3 | 4 | class MoonPhase extends InfoDisplayElement { 5 | constructor(displayLine) { 6 | const ruleData = { identifier: 'moonPhase', description: { translate: 'rules.infoDisplay.moonPhase' } }; 7 | super(ruleData, displayLine, true); 8 | } 9 | 10 | getFormattedDataOwnLine() { 11 | return { translate: 'rules.infoDisplay.moonPhase.display', with: [this.getParsedMoonPhase()] }; 12 | } 13 | 14 | getFormattedDataSharedLine() { 15 | return this.getFormattedDataOwnLine(); 16 | } 17 | 18 | getParsedMoonPhase() { 19 | switch (world.getMoonPhase()) { 20 | case 0: 21 | return 'Full Moon'; 22 | case 1: 23 | return 'Waning Gibbous'; 24 | case 2: 25 | return 'First Quarter'; 26 | case 3: 27 | return 'Waning Crescent'; 28 | case 4: 29 | return 'New Moon'; 30 | case 5: 31 | return 'Waxing Crescent'; 32 | case 6: 33 | return 'Last Quarter'; 34 | case 7: 35 | return 'Waxing Gibbous'; 36 | default: 37 | return 'Unknown'; 38 | } 39 | } 40 | } 41 | 42 | export default MoonPhase; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/PeekInventory.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from "./InfoDisplayElement"; 2 | import { getRaycastResults, getClosestTarget, populateItems } from "../../../include/utils"; 3 | import { currentQuery } from "../../commands/peek"; 4 | 5 | class PeekInventory extends InfoDisplayElement { 6 | player; 7 | 8 | constructor(player, displayLine) { 9 | const ruleData = { identifier: 'peekInventory', 10 | description: { translate: 'rules.infoDisplay.peekInventory' }, 11 | contingentRules: ['lookingAt'], 12 | globalContingentRules: ['allowPeekInventory'] 13 | }; 14 | super(ruleData, displayLine, false); 15 | this.player = player; 16 | } 17 | 18 | getFormattedDataOwnLine() { 19 | return { text: `${this.parsePeekInventory()}` }; 20 | } 21 | 22 | getFormattedDataSharedLine() { 23 | return this.getFormattedDataOwnLine(); 24 | } 25 | 26 | parsePeekInventory() { 27 | const { blockRayResult, entityRayResult } = getRaycastResults(this.player, 7); 28 | if (!blockRayResult && !entityRayResult) return ''; 29 | const target = getClosestTarget(this.player, blockRayResult, entityRayResult); 30 | if (!target) return ''; 31 | 32 | let inventory; 33 | try { 34 | inventory = target.getComponent('inventory'); 35 | } catch { 36 | return ''; 37 | } 38 | if (!inventory) return ''; 39 | 40 | let output = ''; 41 | const items = populateItems(inventory); 42 | if (Object.keys(items).length > 0) { 43 | for (const itemName in items) { 44 | if (itemName.includes(currentQuery[this.player.name])) 45 | output += `§c${itemName}: ${items[itemName]}\n`; 46 | else 47 | output += `§r${itemName}: ${items[itemName]}\n`; 48 | } 49 | } else {output = '§rEmpty';} 50 | 51 | return output; 52 | } 53 | } 54 | 55 | export default PeekInventory; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/SessionTime.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | 3 | class SessionTime extends InfoDisplayElement { 4 | player; 5 | 6 | constructor(player, displayLine) { 7 | const ruleData = { identifier: 'sessionTime', description: { translate: 'rules.infoDisplay.sessionTime' } }; 8 | super(ruleData, displayLine); 9 | this.player = player; 10 | player.setDynamicProperty('joinDate', Date.now()); 11 | } 12 | 13 | getFormattedDataOwnLine() { 14 | return { translate: 'rules.infoDisplay.sessionTime.display', with: [this.getSessionTime()] }; 15 | } 16 | 17 | getFormattedDataSharedLine() { 18 | return this.getFormattedDataOwnLine(); 19 | } 20 | 21 | getSessionTime() { 22 | const joinDate = this.player.getDynamicProperty('joinDate'); 23 | if (!joinDate) return '?:?'; 24 | const sessionTime = (Date.now() - joinDate) / 1000; 25 | const hours = Math.floor(sessionTime / 3600); 26 | const minutes = Math.floor((sessionTime % 3600) / 60); 27 | const seconds = Math.floor(sessionTime % 60); 28 | let output = ''; 29 | if (hours > 0) output += `${hours}:`; 30 | output += `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; 31 | return output; 32 | } 33 | } 34 | 35 | export default SessionTime; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/SignalStrength.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from "./InfoDisplayElement"; 2 | import { getRaycastResults } from "../../../include/utils"; 3 | 4 | class SignalStrength extends InfoDisplayElement { 5 | player; 6 | 7 | constructor(player, displayLine) { 8 | const ruleData = { identifier: 'signalStrength', description: { translate: 'rules.infoDisplay.signalStrength' }, contingentRules: ['lookingAt'] }; 9 | super(ruleData, displayLine, false); 10 | this.player = player; 11 | } 12 | 13 | getFormattedDataOwnLine() { 14 | const signalStrength = this.getSignalStrength(); 15 | return signalStrength ? { text: `§7(§c${signalStrength}§7)§r` } : { text: '' }; 16 | } 17 | 18 | getFormattedDataSharedLine() { 19 | return this.getFormattedDataOwnLine(); 20 | } 21 | 22 | getSignalStrength() { 23 | const { blockRayResult, entityRayResult } = getRaycastResults(this.player, 7); 24 | if (entityRayResult[0]?.entity) 25 | return 0; 26 | try { 27 | return blockRayResult?.block?.getRedstonePower(); 28 | } catch (error) { 29 | if (error.name === 'LocationInUnloadedChunkError') 30 | return 0; 31 | throw error; 32 | } 33 | } 34 | } 35 | 36 | export default SignalStrength; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/SlimeChunk.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import MT from 'lib/mt.js'; 3 | 4 | class SlimeChunk extends InfoDisplayElement { 5 | player; 6 | 7 | constructor(player, displayLine) { 8 | const ruleData = { identifier: 'slimeChunk', description: { translate: 'rules.infoDisplay.slimeChunk' } }; 9 | super(ruleData, displayLine); 10 | this.player = player; 11 | } 12 | 13 | getFormattedDataOwnLine() { 14 | return this.isSlime() ? { translate: 'rules.infoDisplay.slimeChunk.display' } : { text: '' }; 15 | } 16 | 17 | getFormattedDataSharedLine() { 18 | return this.getFormattedDataOwnLine(); 19 | } 20 | 21 | 22 | isSlime() { 23 | if (this.player.dimension.id !== "minecraft:overworld") 24 | return false; 25 | 26 | const chunkX = Math.floor(this.player.location.x / 16) >>> 0; 27 | const chunkZ = Math.floor(this.player.location.z / 16) >>> 0; 28 | const seed = ((a, b) => { 29 | const a00 = a & 0xffff; 30 | const a16 = a >>> 16; 31 | const b00 = b & 0xffff; 32 | const b16 = b >>> 16; 33 | const c00 = a00 * b00; 34 | let c16 = c00 >>> 16; 35 | 36 | c16 += a16 * b00; 37 | c16 &= 0xffff; 38 | c16 += a00 * b16; 39 | 40 | const lo = c00 & 0xffff; 41 | const hi = c16 & 0xffff; 42 | 43 | return((hi << 16) | lo) >>> 0; 44 | })(chunkX, 0x1f1f1f1f) ^ chunkZ; 45 | 46 | const mt = new MT(seed); 47 | const n = mt.nextInt(); 48 | const isSlime = (n % 10 === 0); 49 | 50 | return(isSlime); 51 | } 52 | } 53 | 54 | export default SlimeChunk; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/TimeOfDay.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import { world } from '@minecraft/server'; 3 | 4 | class TimeOfDay extends InfoDisplayElement { 5 | constructor(displayLine) { 6 | const ruleData = { identifier: 'timeOfDay', description: { translate: 'rules.infoDisplay.timeOfDay' } }; 7 | super(ruleData, displayLine, true); 8 | } 9 | 10 | getFormattedDataOwnLine() { 11 | return { translate: 'rules.infoDisplay.timeOfDay.display', with: [this.ticksToTime(world.getTimeOfDay())] }; 12 | } 13 | 14 | getFormattedDataSharedLine() { 15 | return { text: `§7${this.ticksToTime(world.getTimeOfDay())}§r` }; 16 | } 17 | 18 | ticksToTime(ticks) { 19 | const ticksPerDay = 24000; 20 | const hoursPerDay = 24; 21 | const ticksPerHour = ticksPerDay / hoursPerDay; 22 | const hoursOffset = 6; // 0 ticks is 6:00 AM ingame 23 | ticks = (ticks + hoursOffset * ticksPerHour) % ticksPerDay; 24 | 25 | const ticksPerMinute = 60; 26 | let hours = Math.floor(ticks / ticksPerHour); 27 | const minutes = Math.floor((ticks % ticksPerHour) * ticksPerMinute / ticksPerHour); 28 | 29 | const noon = 12; 30 | const halfADay = 12; 31 | let period = 'AM'; 32 | if (hours >= noon) 33 | period = 'PM'; 34 | if (hours > noon) 35 | hours -= halfADay; 36 | else if (hours === 0) 37 | hours = noon; 38 | 39 | const padding = 2; 40 | const formattedHours = hours.toString().padStart(padding, '0'); 41 | const formattedMinutes = minutes.toString().padStart(padding, '0'); 42 | 43 | return `${formattedHours}:${formattedMinutes} ${period}`; 44 | } 45 | } 46 | 47 | export default TimeOfDay; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/WorldDay.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import { world } from '@minecraft/server'; 3 | 4 | class WorldDay extends InfoDisplayElement { 5 | constructor(displayLine) { 6 | const ruleData = { identifier: 'worldDay', description: { translate: 'rules.infoDisplay.worldDay' } }; 7 | super(ruleData, displayLine, true); 8 | } 9 | 10 | getFormattedDataOwnLine() { 11 | return { translate: 'rules.infoDisplay.worldDay.display', with: [String(world.getDay())] }; 12 | } 13 | 14 | getFormattedDataSharedLine() { 15 | return this.getFormattedDataOwnLine(); 16 | } 17 | } 18 | 19 | export default WorldDay; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/cardinalFacing.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | 3 | class CardinalFacing extends InfoDisplayElement { 4 | player; 5 | 6 | constructor(player, displayLine) { 7 | const ruleData = { identifier: 'cardinalFacing', description: { translate: 'rules.infoDisplay.cardinalFacing' } }; 8 | super(ruleData, displayLine); 9 | this.player = player; 10 | } 11 | 12 | getFormattedDataOwnLine() { 13 | return { translate: 'rules.infoDisplay.cardinalFacing.display', with: [this.getPlayerDirection()] }; 14 | } 15 | 16 | getFormattedDataSharedLine() { 17 | return { text: `§7${this.getPlayerDirection()}§r` }; 18 | } 19 | 20 | getPlayerDirection() { 21 | const { x, z } = this.player.getViewDirection(); 22 | const angle = Math.atan2(z, x) * (180 / Math.PI); 23 | 24 | if (angle >= -45 && angle < 45) return 'E (+x)' 25 | else if (angle >= 45 && angle < 135) return 'S (+z)'; 26 | else if (angle >= 135 || angle < -135) return 'W (-x)'; 27 | return 'N (-z)'; 28 | } 29 | } 30 | 31 | export default CardinalFacing; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/coords.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | 3 | class Coords extends InfoDisplayElement { 4 | player; 5 | 6 | constructor(player, displayLine) { 7 | const ruleData = { identifier: 'coords', description: { translate: 'rules.infoDisplay.coords' } }; 8 | super(ruleData, displayLine); 9 | this.player = player; 10 | } 11 | 12 | getFormattedDataOwnLine() { 13 | const coords = this.player.location; 14 | [coords.x, coords.y, coords.z] = [coords.x.toFixed(2), coords.y.toFixed(2), coords.z.toFixed(2)]; 15 | return { text: `§r${coords.x} ${coords.y} ${coords.z}§r` }; 16 | } 17 | 18 | getFormattedDataSharedLine() { 19 | return this.getFormattedDataOwnLine(); 20 | } 21 | } 22 | 23 | export default Coords; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/facing.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | 3 | class Facing extends InfoDisplayElement { 4 | player; 5 | 6 | constructor(player, displayLine) { 7 | const ruleData = { identifier: 'facing', description: { translate: 'rules.infoDisplay.facing' } }; 8 | super(ruleData, displayLine); 9 | this.player = player; 10 | } 11 | 12 | getFormattedDataOwnLine() { 13 | const rotation = this.player.getRotation(); 14 | [ rotation.x, rotation.y ] = [ rotation.x.toFixed(2), rotation.y.toFixed(2) ]; 15 | return { translate: 'rules.infoDisplay.facing.display', with: [rotation.x, rotation.y] }; 16 | } 17 | 18 | getFormattedDataSharedLine() { 19 | return this.getFormattedDataOwnLine(); 20 | } 21 | } 22 | 23 | export default Facing; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/infoDisplayElement.js: -------------------------------------------------------------------------------- 1 | import { InfoDisplayRule } from '../../../lib/canopy/Canopy'; 2 | import { Rules } from '../../../lib/canopy/Rules'; 3 | 4 | class InfoDisplayElement { 5 | identifier; 6 | rule; 7 | lineNumber; 8 | isWorldwide; 9 | 10 | constructor(ruleData, lineNumber, isWorldwide = false) { 11 | if (this.constructor === InfoDisplayElement) 12 | throw new TypeError("Abstract class 'InfoDisplayElement' cannot be instantiated directly."); 13 | if (!ruleData.identifier || !ruleData.description) 14 | throw new Error("ruleData must have 'identifier' and 'description' properties."); 15 | this.identifier = ruleData.identifier; 16 | this.rule = Rules.get(this.identifier) || new InfoDisplayRule({ identifier: this.identifier, ...ruleData }); 17 | this.isWorldwide = isWorldwide; 18 | this.lineNumber = lineNumber; 19 | } 20 | 21 | getFormattedDataOwnLine() { 22 | throw new Error("Method 'getFormattedDataOwnLine()' must be implemented."); 23 | } 24 | 25 | getFormattedDataSharedLine() { 26 | throw new Error("Method 'getFormattedDataSharedLine()' must be implemented."); 27 | } 28 | } 29 | 30 | export default InfoDisplayElement; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/simulationMap.js: -------------------------------------------------------------------------------- 1 | import { world } from '@minecraft/server'; 2 | import InfoDisplayElement from './InfoDisplayElement.js'; 3 | import { getConfig, getLoadedChunksMessage } from '../../commands/simmap.js'; 4 | 5 | class SimulationMap extends InfoDisplayElement { 6 | player; 7 | 8 | constructor(player, displayLine) { 9 | const ruleData = { identifier: 'simulationMap', description: { translate: 'rules.infoDisplay.simulationMap' } }; 10 | super(ruleData, displayLine); 11 | this.player = player; 12 | } 13 | 14 | getFormattedDataOwnLine() { 15 | const config = getConfig(this.player); 16 | if (config.isLocked) { 17 | const dimension = world.getDimension(config.dimension); 18 | return getLoadedChunksMessage(dimension, config.location, config.distance); 19 | } 20 | return getLoadedChunksMessage(this.player.dimension, this.player.location, config.distance); 21 | 22 | } 23 | 24 | getFormattedDataSharedLine() { 25 | return { text: `§cSimulationMap should always be on its own InfoDisplay line.§r` }; 26 | } 27 | } 28 | 29 | export default SimulationMap; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/infodisplay/tps.js: -------------------------------------------------------------------------------- 1 | import InfoDisplayElement from './InfoDisplayElement.js'; 2 | import { Profiler } from '../../classes/Profiler.js'; 3 | import { TicksPerSecond } from '@minecraft/server'; 4 | 5 | class TPS extends InfoDisplayElement { 6 | constructor(displayLine) { 7 | const ruleData = { identifier: 'tps', description: { translate: 'rules.infoDisplay.tps' } }; 8 | super(ruleData, displayLine, true); 9 | } 10 | 11 | getFormattedDataOwnLine() { 12 | return { translate: 'rules.infoDisplay.tps.display', with: [this.getTPS()] }; 13 | } 14 | 15 | getFormattedDataSharedLine() { 16 | return this.getFormattedDataOwnLine(); 17 | } 18 | 19 | getTPS() { 20 | const tps = Profiler.tps; 21 | const nearbyRange = 0.19; 22 | if (tps >= TicksPerSecond - nearbyRange && tps <= TicksPerSecond + nearbyRange) 23 | return `§a${TicksPerSecond}.0`; 24 | return tps >= TicksPerSecond ? `§a${tps.toFixed(1)}` : `§c${tps.toFixed(1)}`; 25 | } 26 | } 27 | 28 | export default TPS; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/instaminableDeepslate.js: -------------------------------------------------------------------------------- 1 | import { Rule } from 'lib/canopy/Canopy'; 2 | import Instaminable from 'src/classes/Instaminable'; 3 | 4 | const instamineableDeepslateRule = new Rule({ 5 | category: 'Rules', 6 | identifier: 'instaminableDeepslate', 7 | description: { translate: 'rules.instaminableDeepslate' } 8 | }); 9 | 10 | function isDeepslate(value) { 11 | return value.includes('deepslate') 12 | } 13 | 14 | new Instaminable(isDeepslate, instamineableDeepslateRule.getID()); 15 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/instaminableEndstone.js: -------------------------------------------------------------------------------- 1 | import { Rule } from 'lib/canopy/Canopy'; 2 | import Instaminable from 'src/classes/Instaminable'; 3 | 4 | const instamineableEndstoneRule = new Rule({ 5 | category: 'Rules', 6 | identifier: 'instaminableEndstone', 7 | description: { translate: 'rules.instaminableEndstone' } 8 | }); 9 | 10 | function isEndStone(value) { 11 | return value.includes('end_stone'); 12 | } 13 | 14 | new Instaminable(isEndStone, instamineableEndstoneRule.getID()); 15 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/noWelcomeMessage.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules, Extensions } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | import { PACK_VERSION } from "../../constants"; 4 | 5 | new Rule({ 6 | category: 'Rules', 7 | identifier: 'noWelcomeMessage', 8 | description: { translate: 'rules.noWelcomeMessage' } 9 | }); 10 | 11 | const hasShownWelcome = {}; 12 | 13 | world.afterEvents.playerLeave.subscribe((event) => { 14 | hasShownWelcome[event.playerId] = false; 15 | }); 16 | 17 | function displayWelcome(player) { 18 | if (Rules.getNativeValue('noWelcomeMessage') || hasShownWelcome[player?.id]) return; 19 | hasShownWelcome[player.id] = true; 20 | const graphic = [ 21 | '§a + ----- +\n', 22 | '§a / / |\n', 23 | '§a+ ----- + |\n', 24 | '§a | | +\n', 25 | '§a | | /\n', 26 | '§a+ ----- +\n' 27 | ].join(''); 28 | player.sendMessage({ rawtext: [{ text: graphic }, { translate: 'generic.welcome.start', with: [PACK_VERSION] }] }); 29 | 30 | const extensions = Extensions.getVersionedNames(); 31 | if (extensions.length === 0) return; 32 | const extensionsMessage = { rawtext: [{ translate: 'generic.welcome.extensions' }] }; 33 | for (let i = 0; i < extensions.length; i++) { 34 | const extensionName = extensions[i]; 35 | if (i > 0) 36 | extensionsMessage.rawtext.push({ text: '§r§7,' }); 37 | extensionsMessage.rawtext.push({ text: ` §2§o${extensionName.name} v${extensionName.version}` }); 38 | } 39 | player.sendMessage(extensionsMessage); 40 | } 41 | 42 | export { displayWelcome }; -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/pistonBedrockBreaking.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { BlockPermutation, ItemStack, world } from '@minecraft/server'; 3 | import DirectionStateFinder from "../classes/DirectionState"; 4 | 5 | new Rule({ 6 | category: 'Rules', 7 | identifier: 'pistonBedrockBreaking', 8 | description: { translate: 'rules.pistonBedrockBreaking' } 9 | }); 10 | 11 | const insideBedrockPistonList = []; 12 | 13 | world.afterEvents.pistonActivate.subscribe((event) => { 14 | if (!Rules.getNativeValue('pistonBedrockBreaking') || !['Expanding', 'Retracting'].includes(event.piston.state)) 15 | return; 16 | const piston = event.piston; 17 | const block = event.block; 18 | const directionState = DirectionStateFinder.getDirectionState(block.permutation); 19 | if (directionState === undefined) return; 20 | directionState.value = DirectionStateFinder.getRawMirroredDirection(block); 21 | if (piston.state === 'Expanding') { 22 | const behindBlock = DirectionStateFinder.getRelativeBlock(block, directionState); 23 | if (behindBlock.typeId === 'minecraft:bedrock') { 24 | block.setPermutation(BlockPermutation.resolve(block.typeId, { [directionState.name]: directionState.value })); 25 | insideBedrockPistonList.push({ dimensionId: block.dimension.id, location: block.location }); 26 | } 27 | } else if (piston.state === 'Retracting') { 28 | const oldPiston = getBlockFromPistonList(block); 29 | if (oldPiston !== undefined) { 30 | const blockType = block.typeId; 31 | const dropLocation = block.center(); 32 | block.setType('minecraft:air'); 33 | event.dimension.spawnItem(new ItemStack(blockType, 1), dropLocation); 34 | insideBedrockPistonList.splice(insideBedrockPistonList.indexOf(oldPiston), 1); 35 | } 36 | } 37 | }); 38 | 39 | function getBlockFromPistonList(block) { 40 | for (const pistonBlock of insideBedrockPistonList) { 41 | if (pistonBlock.dimensionId === block.dimension.id 42 | && pistonBlock.location.x === block.location.x 43 | && pistonBlock.location.y === block.location.y 44 | && pistonBlock.location.z === block.location.z) 45 | return pistonBlock; 46 | 47 | } 48 | return undefined; 49 | } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/playerSit.js: -------------------------------------------------------------------------------- 1 | import { DimensionTypes, EntityComponentTypes, system, world } from "@minecraft/server"; 2 | import { GlobalRule } from "../../lib/canopy/Canopy"; 3 | import { playerStartSneakEvent } from "../events/PlayerStartSneakEvent"; 4 | 5 | const ruleID = 'playerSit'; 6 | const SNEAK_COUNT = 3; 7 | 8 | class PlayerSit extends GlobalRule { 9 | sneakCount = SNEAK_COUNT; 10 | sneakSpeedMs = 4*50; 11 | playerSneaks = {}; 12 | onPlayerStartSneakBound; 13 | 14 | constructor() { 15 | super({ 16 | identifier: ruleID, 17 | description: { translate: `rules.${ruleID}`, with: [SNEAK_COUNT.toString()] }, 18 | onEnableCallback: () => playerStartSneakEvent.subscribe(this.onPlayerStartSneakBound), 19 | onDisableCallback: () => playerStartSneakEvent.unsubscribe(this.onPlayerStartSneakBound) 20 | }); 21 | this.onPlayerStartSneakBound = this.onPlayerStartSneak.bind(this); 22 | this.startEntityCleanup(); 23 | } 24 | 25 | onPlayerStartSneak(event) { 26 | event.players.forEach(player => { 27 | this.handlePlayerSneak(player); 28 | }); 29 | } 30 | 31 | handlePlayerSneak(player) { 32 | const currentTimeMs = Date.now(); 33 | const sneakTracker = this.playerSneaks[player.id] || { count: 0, lastTimeMs: currentTimeMs, lastTick: system.currentTick }; 34 | if (player.isOnGround && currentTimeMs - sneakTracker.lastTimeMs < this.sneakSpeedMs) { 35 | sneakTracker.count++; 36 | if (sneakTracker.count >= this.sneakCount) { 37 | this.sit(player); 38 | sneakTracker.count = 0; 39 | } 40 | } else { 41 | sneakTracker.count = 1; 42 | } 43 | sneakTracker.lastTimeMs = currentTimeMs; 44 | sneakTracker.lastTick = system.currentTick; 45 | this.playerSneaks[player.id] = sneakTracker; 46 | } 47 | 48 | sit(player) { 49 | const heightAdjustment = -0.12; 50 | const entityLocation = { x: player.location.x, y: player.location.y + heightAdjustment, z: player.location.z }; 51 | const rideableEntity = player.dimension.spawnEntity('canopy:rideable', entityLocation); 52 | rideableEntity.setRotation(player.getRotation()); 53 | rideableEntity.getComponent('rideable').addRider(player); 54 | } 55 | 56 | startEntityCleanup() { 57 | system.runInterval(this.cleanupEntities.bind(this), 10); 58 | } 59 | 60 | cleanupEntities() { 61 | DimensionTypes.getAll().forEach((dimensionType) => { 62 | this.removeEntitiesWithNoRider(world.getDimension(dimensionType.typeId).getEntities({ type: 'canopy:rideable' })); 63 | }); 64 | } 65 | 66 | removeEntitiesWithNoRider(entities) { 67 | entities.forEach(entity => { 68 | if (entity.getComponent(EntityComponentTypes.Rideable).getRiders().length === 0) 69 | entity.remove(); 70 | }); 71 | } 72 | } 73 | 74 | export const playerSit = new PlayerSit(); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/quickFillContainer.js: -------------------------------------------------------------------------------- 1 | import { system, world } from "@minecraft/server"; 2 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'quickFillContainer', 7 | description: { translate: 'rules.quickFillContainer' } 8 | }); 9 | 10 | const ARROW_SLOT = 9; 11 | const BANNED_CONTAINERS = ['minecraft:beacon', 'minecraft:jukebox', 'minecraft:lectern']; 12 | 13 | world.beforeEvents.playerInteractWithBlock.subscribe((event) => { 14 | if (!Rules.getNativeValue('quickFillContainer')) return; 15 | const player = event.player; 16 | if (!player) return; 17 | const block = event.block; 18 | if (BANNED_CONTAINERS.includes(block?.typeId)) return; 19 | const blockInv = block.getComponent('inventory')?.container; 20 | if (!blockInv) return; 21 | const playerInv = player.getComponent('inventory')?.container; 22 | if (!playerInv || playerInv.getItem(ARROW_SLOT)?.typeId !== 'minecraft:arrow') return; 23 | const handItemStack = event.itemStack; 24 | if (!handItemStack) return; 25 | if (block.typeId.includes('shulker_box') && handItemStack.typeId.includes('shulker_box')) return; 26 | event.cancel = true; 27 | 28 | system.run(() => { 29 | const successfulTransfers = transferAllItemType(playerInv, blockInv, handItemStack.typeId); 30 | if (successfulTransfers === 0) { // true either no items were transferred OR the stacks in the container were only topped off 31 | return; 32 | } 33 | const feedback = { rawtext: [{ translate: 'rules.quickFillContainer.filled', with: [block.typeId.replace('minecraft:', ''), handItemStack.typeId.replace('minecraft:', '')] }] }; 34 | feedback.rawtext.push({ text: ` (§a${blockInv.size - blockInv.emptySlotsCount}§7/§a${blockInv.size}§7)` }); 35 | player.onScreenDisplay.setActionBar(feedback); 36 | }); 37 | }); 38 | 39 | function transferAllItemType(fromContainer, toContainer, itemTypeId) { 40 | let successfulTransfers = 0; 41 | for (let slotIndex = 0; slotIndex < fromContainer.size; slotIndex++) { 42 | const currFromItem = fromContainer.getItem(slotIndex); 43 | if (currFromItem?.typeId === itemTypeId) { 44 | const untransferred = toContainer.addItem(currFromItem); 45 | if (untransferred) { 46 | fromContainer.setItem(slotIndex, untransferred); 47 | } else { 48 | fromContainer.setItem(slotIndex, null); 49 | successfulTransfers++; 50 | } 51 | } 52 | } 53 | return successfulTransfers; 54 | } 55 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/refillHand.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules} from "../../lib/canopy/Canopy"; 2 | import { GameMode, world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'refillHand', 7 | description: { translate: 'rules.refillHand' } 8 | }); 9 | 10 | const ARROW_SLOT = 10; 11 | 12 | world.afterEvents.playerInteractWithBlock.subscribe((event) => captureEvent(event)); 13 | world.afterEvents.playerInteractWithEntity.subscribe((event) => captureEvent(event)); 14 | 15 | function captureEvent(event) { 16 | if (!Rules.getNativeValue('refillHand')) return; 17 | const player = event.player; 18 | if (!player || player.getGameMode() !== GameMode.survival) return; 19 | processRefillHand(player, event.beforeItemStack, event.itemStack); 20 | } 21 | 22 | function processRefillHand(player, beforeItemStack, afterItemStack) { 23 | const playerInventory = player.getComponent('inventory')?.container; 24 | if (beforeItemStack === undefined || !hasArrowInCorrectSlot(playerInventory)) return; 25 | if (hasRunOutOfItems(beforeItemStack, afterItemStack)) 26 | refillHand(player, playerInventory, beforeItemStack); 27 | 28 | } 29 | 30 | function hasArrowInCorrectSlot(playerInventory) { 31 | return playerInventory?.getItem(ARROW_SLOT)?.typeId === 'minecraft:arrow'; 32 | } 33 | 34 | function hasRunOutOfItems(beforeItemStack, afterItemStack) { 35 | return beforeItemStack?.typeId !== afterItemStack?.typeId; 36 | } 37 | 38 | function refillHand(player, playerInventory, beforeItemStack) { 39 | for (let slotIndex = 0; slotIndex < playerInventory.size; slotIndex++) { 40 | const slot = playerInventory.getSlot(slotIndex); 41 | if (slot.hasItem() && slot.isStackableWith(beforeItemStack)) { 42 | playerInventory.swapItems(slotIndex, player.selectedSlotIndex, playerInventory); 43 | return; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/renewableElytra.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "lib/canopy/Canopy"; 2 | import { ItemStack, world } from "@minecraft/server"; 3 | 4 | const DROP_CHANCE = 0.01; 5 | 6 | new Rule({ 7 | category: 'Rules', 8 | identifier: 'renewableElytra', 9 | description: { translate: 'rules.renewableElytra' } 10 | }); 11 | 12 | world.afterEvents.entityDie.subscribe((event) => { 13 | if (!Rules.getNativeValue('renewableElytra')) return; 14 | const entity = event.deadEntity; 15 | if (entity?.typeId === 'minecraft:phantom' && event.damageSource.damagingProjectile?.typeId === 'minecraft:shulker_bullet') { 16 | if (Math.random() > DROP_CHANCE) return; 17 | entity.dimension.spawnItem(new ItemStack('minecraft:elytra', 1), entity.location); 18 | } 19 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/renewableSponge.js: -------------------------------------------------------------------------------- 1 | import { world } from "@minecraft/server"; 2 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'renewableSponge', 7 | description: { translate: 'rules.renewableSponge' } 8 | }); 9 | 10 | world.afterEvents.entityHurt.subscribe((event) => { 11 | if (event.hurtEntity?.typeId !== 'minecraft:guardian' || !Rules.getNativeValue('renewableSponge') || event.damageSource.cause !== 'lightning') 12 | return; 13 | const guardian = event.hurtEntity; 14 | guardian.dimension.spawnEntity('minecraft:elder_guardian', guardian.location); 15 | guardian.remove(); 16 | }); -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/tntPrimeMaxMomentum.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world, system } from '@minecraft/server'; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'tntPrimeMaxMomentum', 7 | description: { translate: 'rules.tntPrimeMaxMomentum' }, 8 | independentRules: ['tntPrimeNoMomentum'] 9 | }); 10 | 11 | const MAX_VELOCITY = 0.019600000232548116; // From vanilla TNT: 49/2500 with some floating point error 12 | 13 | world.afterEvents.entitySpawn.subscribe((event) => { 14 | if (event.entity.typeId !== 'minecraft:tnt' || !Rules.getNativeValue('tntPrimeMaxMomentum')) return; 15 | const entity = event.entity; 16 | if (Rules.getNativeValue('dupeTnt')) { 17 | system.runTimeout(() => { 18 | if (!entity.isValid) return; 19 | haltHorizontalVelocity(entity); 20 | applyHardcodedImpulse(entity); 21 | }, 1); 22 | } else { 23 | negateXZVelocity(entity); 24 | applyHardcodedImpulse(entity); 25 | } 26 | }); 27 | 28 | function haltHorizontalVelocity(entity) { 29 | const velocity = entity.getVelocity(); 30 | centerEntityPosition(entity); // Entity could be off-center, resulting in a non-straight drop 31 | entity.applyImpulse({ x: 0, y: velocity.y, z: 0 }); 32 | } 33 | 34 | function centerEntityPosition(entity) { 35 | const blockCenter = getHorizontalCenter(entity.location); 36 | entity.teleport(blockCenter); 37 | } 38 | 39 | function getHorizontalCenter(location) { 40 | const halfABlock = 0.5; 41 | return { x: Math.floor(location.x) + halfABlock, y: location.y, z: Math.floor(location.z) + halfABlock }; 42 | } 43 | 44 | function negateXZVelocity(entity) { 45 | const velocity = entity.getVelocity(); 46 | entity.applyImpulse({ x: -velocity.x, y: 0, z: -velocity.z }); 47 | } 48 | 49 | function applyHardcodedImpulse(entity) { 50 | const randX = getRandomMaxMomentumValue(); 51 | const randZ = getRandomMaxMomentumValue(); 52 | entity.applyImpulse({ x: randX, y: 0, z: randZ }); 53 | } 54 | 55 | function getRandomMaxMomentumValue() { 56 | const randValues = [-MAX_VELOCITY, 0, MAX_VELOCITY]; 57 | const randIndex = Math.floor(Math.random() * randValues.length); 58 | const randValue = randValues[randIndex]; 59 | return randValue; 60 | } 61 | 62 | export { negateXZVelocity, haltHorizontalVelocity } -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/tntPrimeNoMomentum.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world, system } from '@minecraft/server'; 3 | import { negateXZVelocity, haltHorizontalVelocity } from './tntPrimeMaxMomentum.js'; 4 | 5 | new Rule({ 6 | category: 'Rules', 7 | identifier: 'tntPrimeNoMomentum', 8 | description: { translate: 'rules.tntPrimeNoMomentum' }, 9 | independentRules: ['tntPrimeMaxMomentum'] 10 | }); 11 | 12 | world.afterEvents.entitySpawn.subscribe((event) => { 13 | if (event.entity.typeId !== 'minecraft:tnt' || !Rules.getNativeValue('tntPrimeNoMomentum')) return; 14 | const entity = event.entity; 15 | if (Rules.getNativeValue('dupeTnt')) { 16 | system.runTimeout(() => { 17 | if (!entity.isValid) return; 18 | haltHorizontalVelocity(entity); 19 | }, 1); 20 | } else { 21 | negateXZVelocity(entity); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /Canopy [BP]/scripts/src/rules/universalChunkLoading.js: -------------------------------------------------------------------------------- 1 | import { Rule, Rules } from "../../lib/canopy/Canopy"; 2 | import { world } from "@minecraft/server"; 3 | 4 | new Rule({ 5 | category: 'Rules', 6 | identifier: 'universalChunkLoading', 7 | description: { translate: 'rules.universalChunkLoading' } 8 | }); 9 | 10 | world.afterEvents.entitySpawn.subscribe((event) => { 11 | if (event.entity.typeId !== 'minecraft:minecart' || !Rules.getNativeValue('universalChunkLoading')) return; 12 | event.entity.triggerEvent('canopy:tick_tenSeconds'); 13 | }); 14 | -------------------------------------------------------------------------------- /Canopy [BP]/structures/bubble_column.mcstructure: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [BP]/structures/bubble_column.mcstructure -------------------------------------------------------------------------------- /Canopy [RP]/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 2, 3 | "header": { 4 | "name": "Canopy [RP] v1.3.9", 5 | "description": "Technical informatics & features addon by §aForestOfLight§r.", 6 | "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", 7 | "version": [1, 0, 3], 8 | "min_engine_version": [1,17,0] 9 | }, 10 | "modules": [ 11 | { 12 | "type": "resources", 13 | "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", 14 | "version": [1, 0, 0] 15 | } 16 | ], 17 | "dependencies": [ 18 | { 19 | "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", 20 | "version": [1, 3, 9] 21 | } 22 | ], 23 | "metadata": { 24 | "authors": [ "ForestOfLight" ], 25 | "license": "MIT" 26 | } 27 | } -------------------------------------------------------------------------------- /Canopy [RP]/pack_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [RP]/pack_icon.png -------------------------------------------------------------------------------- /Canopy [RP]/texts/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "en_US", 3 | "de_DE", 4 | "id_ID", 5 | "zh_CN" 6 | ] -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/d_b.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_size": [3, 3], 3 | 4 | "nineslice_size": [ 5 | 1, 1, 1, 1 6 | ] 7 | } -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/d_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [RP]/textures/ui/d_b.png -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/d_g.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_size": [3, 3], 3 | 4 | "nineslice_size": [ 5 | 1, 1, 1, 1 6 | ] 7 | } -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/d_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [RP]/textures/ui/d_g.png -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/item_background.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_size": [18, 18], 3 | "nineslice_size": 1 4 | } -------------------------------------------------------------------------------- /Canopy [RP]/textures/ui/item_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/Canopy [RP]/textures/ui/item_background.png -------------------------------------------------------------------------------- /Canopy [RP]/ui/_global_variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "$show_inventory": false, // Set to false if don't want to show inventory. Not recommended for low end devices. 3 | "$border_and_background_texture": "textures/ui/dialog_background_opaque", // Add your own texture for background and border here. Edit `textures/ui/item_background` to change slot texture. 4 | 5 | /* 6 | *It is important to only enable the layouts that you're going to use and disable others to decrease lag* 7 | */ 8 | 9 | "$disable_furnace_ui": true, 10 | 11 | "$disable_1_slots_layout": true, 12 | 13 | "$disable_5_slots_layout": false, 14 | 15 | "$disable_9_slots_layout": false, 16 | 17 | "$disable_18_slots_layout": true, 18 | 19 | "$disable_27_slots_layout": false, 20 | 21 | "$disable_36_slots_layout": false, 22 | 23 | "$disable_45_slots_layout": true, 24 | 25 | "$disable_54_slots_layout": false 26 | } -------------------------------------------------------------------------------- /Canopy [RP]/ui/_ui_defs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui_defs": [ 3 | "ui/chest_server_form.json", 4 | "ui/chest_inventory_system.json", 5 | "ui/furnace_server_form.json", 6 | "ui/canopy/infodisplay.json" 7 | ] 8 | } -------------------------------------------------------------------------------- /Canopy [RP]/ui/hud_screen.json: -------------------------------------------------------------------------------- 1 | { 2 | "hud_title_text": { 3 | "type": "image", 4 | "size": [ "100%c + 10px", "100%c + 8px"], 5 | "alpha": 0.8, 6 | "anchor_from": "top_right", 7 | "anchor_to": "top_right", 8 | "offset": [ 0, 0 ], 9 | "texture": "textures/ui/Gray", 10 | "layer": 30, 11 | "controls": [ 12 | { 13 | "title": { 14 | "type": "label", 15 | "text": "#text", 16 | "layer": 31, 17 | "localize": false, 18 | "font_size": "small", 19 | "font_scale_factor": 1.6, 20 | "bindings": [ 21 | { 22 | "binding_name": "#hud_title_text_string", 23 | "binding_name_override": "#text", 24 | "binding_type": "global" 25 | } 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /Canopy [RP]/ui/server_form.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "server_form", 3 | "long_form": { 4 | "type": "panel", 5 | "controls": [ 6 | { 7 | "long_form@common_dialogs.main_panel_no_buttons": { 8 | "$title_panel": "common_dialogs.standard_title_label", 9 | "$title_size": [ 10 | "100% - 14px", 11 | 10 12 | ], 13 | "size": [ 14 | 225, 15 | 200 16 | ], 17 | "$text_name": "#title_text", 18 | "$title_text_binding_type": "none", 19 | "$child_control": "server_form.long_form_panel", 20 | "layer": 2, 21 | "bindings": [ 22 | { 23 | "binding_name": "#title_text" 24 | }, 25 | { 26 | "binding_type": "view", 27 | "source_property_name": "(((#title_text - '§c§h§e§s§t') = #title_text) and ((#title_text - '§f§u§r§n§a§c§e') = #title_text))", 28 | "target_property_name": "#visible" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "chest_ui@chest_ui.chest_panel": { 35 | "bindings": [ 36 | { 37 | "binding_name": "#title_text" 38 | }, 39 | { 40 | "binding_type": "view", 41 | "source_property_name": "(not ((#title_text - '§c§h§e§s§t') = #title_text))", 42 | "target_property_name": "#visible" 43 | } 44 | ] 45 | } 46 | }, 47 | { 48 | "furnace_ui@furnace_ui.furnace_panel": { 49 | "ignored": "$disable_furnace_ui", 50 | "bindings": [ 51 | { 52 | "binding_name": "#title_text" 53 | }, 54 | { 55 | "binding_type": "view", 56 | "source_property_name": "(not ((#title_text - '§f§u§r§n§a§c§e') = #title_text))", 57 | "target_property_name": "#visible" 58 | } 59 | ] 60 | } 61 | } 62 | ] 63 | } 64 | //"third_party_server_screen@common.base_screen": { 65 | // "type": "screen", 66 | // "size": [ 67 | // "100%", 68 | // "100%" 69 | // ], 70 | // "anchor_from": "top_left", 71 | // "anchor_to": "top_left", 72 | // "$screen_content": "server_form.main_screen_content", 73 | // "$screen_animations": [ 74 | // "@server_form.exit_wait" 75 | // ], 76 | // "$background_animations": [ 77 | // "@server_form.exit_wait" 78 | // ], 79 | // "force_render_below": true, 80 | // "low_frequency_rendering": true, 81 | // "load_screen_immediately": true, 82 | // "render_only_when_topmost": false, 83 | // "render_game_behind": true, 84 | // "button_mappings": [ 85 | // { 86 | // "from_button_id": "button.menu_cancel", 87 | // "to_button_id": "button.menu_exit", 88 | // "mapping_type": "global" 89 | // } 90 | // ] 91 | //}, 92 | //"exit_wait": { 93 | // "anim_type": "offset", 94 | // "easing": "linear", 95 | // "duration": 0.08, 96 | // "from": [ 97 | // 0, 98 | // 0 99 | // ], 100 | // "to": [ 101 | // 0, 102 | // 0 103 | // ], 104 | // "play_event": "screen.exit_pop", 105 | // "end_event": "screen.exit_end" 106 | //} 107 | } 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ForestOfLight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__mocks__/@minecraft/server-ui.js: -------------------------------------------------------------------------------- 1 | export const FormCancelationReason = { 2 | UserBusy: "UserBusy", 3 | UserClosed: "UserClosed", 4 | } 5 | export const uiManager = { 6 | closeAllForms: () => {} 7 | } 8 | export const ActionFormData = {}; -------------------------------------------------------------------------------- /__mocks__/@minecraft/server.js: -------------------------------------------------------------------------------- 1 | export const world = {}; 2 | export const system = {}; 3 | export const ItemStack = {}; 4 | export const DimensionTypes = {}; 5 | export const ScriptEventSource = {}; 6 | export const InputButton = {}; 7 | export const ButtonState = {}; 8 | export const EntityComponentTypes = {}; 9 | export const ItemComponentTypes = {}; -------------------------------------------------------------------------------- /__tests__/BP/scripts/include/data.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import axios from 'axios'; 3 | import { MC_VERSION } from '../../../../Canopy [BP]/scripts/constants.js'; 4 | import { categoryToMobMap, intToBiomeMap } from 'Canopy [BP]/scripts/include/data.js'; 5 | import stripJsonComments from 'strip-json-comments'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | import { titleCase } from '../../../../Canopy [BP]/scripts/include/utils.js'; 9 | 10 | vi.mock('@minecraft/server', { 11 | world: {}, 12 | ItemStack: {}, 13 | DimensionTypes: {} 14 | }); 15 | 16 | vi.mock("@minecraft/server-ui", () => ({ 17 | ModalFormData: vi.fn() 18 | })); 19 | 20 | const bedrockSamplesRawUrl = `https://raw.githubusercontent.com/Mojang/bedrock-samples/refs/tags/v${MC_VERSION}/behavior_pack/spawn_rules/`; 21 | 22 | async function fetchBedrockSamplesData(entityType) { 23 | let response; 24 | try { 25 | response = await axios.get(bedrockSamplesRawUrl + entityType + '.json'); 26 | } catch (error) { 27 | if (error.response.status === 404) { 28 | return { 29 | "minecraft:spawn_rules": { 30 | "description": { 31 | "population_control": "none" 32 | } 33 | } 34 | }; 35 | } 36 | } 37 | if (typeof response.data == 'string') { 38 | const stringData = stripJsonComments(response.data); 39 | return JSON.parse(stringData); 40 | } 41 | return response.data; 42 | } 43 | 44 | describe.concurrent('categoryToMobMap', () => { 45 | for (const category in categoryToMobMap) { 46 | const categoryMobs = categoryToMobMap[category]; 47 | for (const mob of categoryMobs) { 48 | 49 | it(`${mob} population should match bedrock-samples`, async () => { 50 | const mobData = await fetchBedrockSamplesData(mob); 51 | let mobCategory = mobData["minecraft:spawn_rules"]["description"]["population_control"]; 52 | if (!mobCategory) 53 | mobCategory = 'none'; 54 | expect(mobCategory).toBe(category); 55 | }); 56 | } 57 | } 58 | }); 59 | 60 | const probeEntityPath = path.resolve('Canopy [BP]/entities/probe.json'); 61 | 62 | describe('intToBiomeMap', () => { 63 | const probeData = JSON.parse(stripJsonComments(fs.readFileSync(probeEntityPath, 'utf-8'))); 64 | for (const biomeId in probeData['minecraft:entity']['events']) { 65 | if (!biomeId.startsWith('canopy:') || biomeId.includes('reset_biome_property')) 66 | continue; 67 | 68 | it(`${biomeId} should be valid in the intToBiomeMap`, () => { 69 | const biomeName = titleCase(biomeId.replace('canopy:', '')); 70 | expect(Object.values(intToBiomeMap)).toContain(biomeName); 71 | const biomeInt = probeData['minecraft:entity']['events'][biomeId]['set_property']['canopy:biome']; 72 | expect(intToBiomeMap[biomeInt]).toBe(biomeName); 73 | }); 74 | } 75 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/Canopy.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import * as Canopy from "../../../../../Canopy [BP]/scripts/lib/canopy/Canopy"; 3 | 4 | vi.mock('@minecraft/server', () => ({ 5 | world: { 6 | beforeEvents: { 7 | chatSend: { 8 | subscribe: vi.fn() 9 | } 10 | }, 11 | afterEvents: { 12 | worldLoad: { 13 | subscribe: vi.fn() 14 | } 15 | } 16 | }, 17 | system: { 18 | afterEvents: { 19 | scriptEventReceive: { 20 | subscribe: vi.fn() 21 | } 22 | }, 23 | runJob: vi.fn() 24 | } 25 | })); 26 | 27 | vi.mock("@minecraft/server-ui", () => ({ 28 | ModalFormData: vi.fn() 29 | })); 30 | 31 | describe('Canopy module', () => { 32 | it('should export Commands', () => { 33 | expect(Canopy.Commands).toBeDefined(); 34 | }); 35 | 36 | it('should export Command', () => { 37 | expect(Canopy.Command).toBeDefined(); 38 | }); 39 | 40 | it('should export Rules', () => { 41 | expect(Canopy.Rules).toBeDefined(); 42 | }); 43 | 44 | it('should export Rule', () => { 45 | expect(Canopy.Rule).toBeDefined(); 46 | }); 47 | 48 | it('should export GlobalRule', () => { 49 | expect(Canopy.GlobalRule).toBeDefined(); 50 | }); 51 | 52 | it('should export InfoDisplayRule', () => { 53 | expect(Canopy.InfoDisplayRule).toBeDefined(); 54 | }); 55 | 56 | it('should export RuleHelpEntry', () => { 57 | expect(Canopy.RuleHelpEntry).toBeDefined(); 58 | }); 59 | 60 | it('should export CommandHelpEntry', () => { 61 | expect(Canopy.CommandHelpEntry).toBeDefined(); 62 | }); 63 | 64 | it('should export InfoDisplayRuleHelpEntry', () => { 65 | expect(Canopy.InfoDisplayRuleHelpEntry).toBeDefined(); 66 | }); 67 | 68 | it('should export RuleHelpPage', () => { 69 | expect(Canopy.RuleHelpPage).toBeDefined(); 70 | }); 71 | 72 | it('should export CommandHelpPage', () => { 73 | expect(Canopy.CommandHelpPage).toBeDefined(); 74 | }); 75 | 76 | it('should export InfoDisplayRuleHelpPage', () => { 77 | expect(Canopy.InfoDisplayRuleHelpPage).toBeDefined(); 78 | }); 79 | 80 | it('should export HelpBook', () => { 81 | expect(Canopy.HelpBook).toBeDefined(); 82 | }); 83 | 84 | it('should export Extensions', () => { 85 | expect(Canopy.Extensions).toBeDefined(); 86 | }); 87 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/GlobalRule.test.js: -------------------------------------------------------------------------------- 1 | import { GlobalRule } from "../../../../../Canopy [BP]/scripts/lib/canopy/GlobalRule"; 2 | import { describe, it, expect, vi, beforeEach } from "vitest"; 3 | import { Rules } from "../../../../../Canopy [BP]/scripts/lib/canopy/Rules"; 4 | 5 | vi.mock('@minecraft/server', () => ({ 6 | world: { 7 | beforeEvents: { 8 | chatSend: { 9 | subscribe: vi.fn() 10 | } 11 | }, 12 | afterEvents: { 13 | worldLoad: { 14 | subscribe: (callback) => { 15 | callback(); 16 | } 17 | } 18 | }, 19 | getDynamicProperty: vi.fn() 20 | }, 21 | system: { 22 | afterEvents: { 23 | scriptEventReceive: { 24 | subscribe: vi.fn() 25 | } 26 | }, 27 | runJob: vi.fn() 28 | } 29 | })); 30 | 31 | describe('GlobalRule', () => { 32 | beforeEach(() => { 33 | Rules.clear(); 34 | }); 35 | 36 | it('should create a new rule in the Rules category', () => { 37 | const testRule = new GlobalRule({ 38 | identifier: 'testRule', 39 | description: { text: 'Test Description' } 40 | }); 41 | expect(Rules.get(testRule.getID())).toBeDefined(); 42 | }); 43 | 44 | it('should autofill the category with the global rules magic string', () => { 45 | const testRule = new GlobalRule({ 46 | identifier: 'testRule', 47 | description: { text: 'Test Description' } 48 | }); 49 | expect(testRule.getCategory()).toBe('Rules'); 50 | }); 51 | 52 | it('should autofill the description if it is missing', () => { 53 | const testRule = new GlobalRule({ 54 | identifier: 'testRule' 55 | }); 56 | expect(testRule.getDescription()).toEqual({ translate: `rules.${testRule.getID()}`}) 57 | }) 58 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/help/CommandHelpEntry.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { CommandHelpEntry } from "../../../../../../Canopy [BP]/scripts/lib/canopy/help/CommandHelpEntry"; 3 | import { Commands } from "../../../../../../Canopy [BP]/scripts/lib/canopy/Commands"; 4 | 5 | vi.mock('@minecraft/server', () => ({ 6 | world: { 7 | beforeEvents: { 8 | chatSend: { 9 | subscribe: vi.fn() 10 | } 11 | }, 12 | afterEvents: { 13 | worldLoad: { 14 | subscribe: vi.fn() 15 | } 16 | } 17 | }, 18 | system: { 19 | afterEvents: { 20 | scriptEventReceive: { 21 | subscribe: vi.fn() 22 | } 23 | }, 24 | runJob: vi.fn() 25 | } 26 | })); 27 | 28 | describe('CommandHelpEntry', () => { 29 | const mockCommand = { 30 | getName: () => 'testCommand', 31 | getDescription: () => 'This is a test command', 32 | getUsage: () => `${Commands.getPrefix()}test`, 33 | getHelpEntries: () => [ 34 | { usage: 'subcommand1', description: 'Description for subcommand1' }, 35 | { usage: 'subcommand2', description: 'Description for subcommand2' } 36 | ] 37 | }; 38 | 39 | it('should create an instance with the correct properties', () => { 40 | const entry = new CommandHelpEntry(mockCommand); 41 | expect(entry.command).toBe(mockCommand); 42 | expect(entry.title).toBe('testCommand'); 43 | expect(entry.description).toEqual({ text: 'This is a test command' }); 44 | }); 45 | 46 | it('should generate the correct raw message', () => { 47 | const entry = new CommandHelpEntry(mockCommand); 48 | const rawMessage = entry.toRawMessage(); 49 | expect(rawMessage).toEqual({ 50 | rawtext: [ 51 | { text: `§2${Commands.getPrefix()}test§8 - ` }, 52 | { text: 'This is a test command' }, 53 | { rawtext: [{ text: `\n §7> §2${Commands.getPrefix()}subcommand1§8 - ` }, 'Description for subcommand1'] }, 54 | { rawtext: [{ text: `\n §7> §2${Commands.getPrefix()}subcommand2§8 - ` }, 'Description for subcommand2'] } 55 | ] 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/help/HelpEntry.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import { describe, it, expect } from "vitest"; 3 | import { HelpEntry } from "../../../../../../Canopy [BP]/scripts/lib/canopy/help/HelpEntry"; 4 | 5 | describe('HelpEntry', () => { 6 | it('should throw an error when instantiated directly', () => { 7 | expect(() => new HelpEntry('Title', 'Description')).toThrow(TypeError); 8 | }); 9 | 10 | it('should set title and description properties when instantiated through a subclass', () => { 11 | class TestHelpEntry extends HelpEntry { 12 | toRawMessage() { 13 | return `${this.title}: ${this.description}`; 14 | } 15 | } 16 | 17 | const entry = new TestHelpEntry('Test Title', 'Test Description'); 18 | expect(entry.title).toBe('Test Title'); 19 | expect(entry.description).toEqual({ text: 'Test Description' }); 20 | }); 21 | 22 | it('should throw an error when toRawMessage is not implemented in a subclass', () => { 23 | class TestHelpEntry extends HelpEntry {} 24 | 25 | const entry = new TestHelpEntry('Test Title', 'Test Description'); 26 | expect(() => entry.toRawMessage()).toThrow(TypeError); 27 | }); 28 | 29 | it('should not throw an error when toRawMessage is implemented in a subclass', () => { 30 | class TestHelpEntry extends HelpEntry { 31 | toRawMessage() { 32 | return { rawtext: [ 33 | { text: this.title }, 34 | this.description 35 | ]}; 36 | } 37 | } 38 | 39 | const entry = new TestHelpEntry('Test Title', 'Test Description'); 40 | expect(entry.toRawMessage()).toEqual({ rawtext: [ 41 | { text: "Test Title" }, 42 | { text: "Test Description" } 43 | ]}); 44 | }); 45 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/help/InfoDisplayRuleHelpEntry.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { InfoDisplayRuleHelpEntry } from '../../../../../../Canopy [BP]/scripts/lib/canopy/help/InfoDisplayRuleHelpEntry'; 3 | import { InfoDisplayRule } from '../../../../../../Canopy [BP]/scripts/lib/canopy/InfoDisplayRule'; 4 | import { Rules } from '../../../../../../Canopy [BP]/scripts/lib/canopy/Rules'; 5 | 6 | vi.mock('@minecraft/server', () => ({ 7 | world: { 8 | beforeEvents: { 9 | chatSend: { 10 | subscribe: vi.fn() 11 | } 12 | }, 13 | afterEvents: { 14 | worldLoad: { 15 | subscribe: vi.fn() 16 | } 17 | } 18 | }, 19 | system: { 20 | afterEvents: { 21 | scriptEventReceive: { 22 | subscribe: vi.fn() 23 | } 24 | }, 25 | runJob: vi.fn() 26 | } 27 | })); 28 | 29 | describe('InfoDisplayRuleHelpEntry', () => { 30 | let entry; 31 | beforeEach(() => { 32 | Rules.clear(); 33 | const infoDisplayRule = new InfoDisplayRule({ identifier: 'testRule', description: 'This is a test rule' }); 34 | entry = new InfoDisplayRuleHelpEntry(infoDisplayRule, 'player'); 35 | }); 36 | 37 | describe('constructor', () => { 38 | it('should throw a TypeError if infoDisplayRule is not an instance of InfoDisplayRule', () => { 39 | expect(() => new InfoDisplayRuleHelpEntry({}, 'player')).toThrow(TypeError); 40 | }); 41 | 42 | it('should create an instance of InfoDisplayRuleHelpEntry', () => { 43 | expect(entry).toBeInstanceOf(InfoDisplayRuleHelpEntry); 44 | }); 45 | }); 46 | 47 | describe('fetchColoredValue', () => { 48 | it('should return the correct colored value', async () => { 49 | vi.spyOn(entry.rule, 'getValue').mockResolvedValue(true); 50 | const result = await entry.fetchColoredValue(); 51 | expect(result).toBe('§atrue§r'); 52 | }); 53 | 54 | it('should return the correct colored value when false', async () => { 55 | vi.spyOn(entry.rule, 'getValue').mockResolvedValue(false); 56 | const result = await entry.fetchColoredValue(); 57 | expect(result).toBe('§cfalse§r'); 58 | }); 59 | }); 60 | 61 | describe('toRawMessage', () => { 62 | it('should return the correct raw message', async () => { 63 | vi.spyOn(entry.rule, 'getValue').mockResolvedValue(true); 64 | const result = await entry.toRawMessage(); 65 | expect(result).toEqual({ 66 | rawtext: [ 67 | { text: '§7testRule: §atrue§r§8 - ' }, 68 | { text: 'This is a test rule' } 69 | ] 70 | }); 71 | }); 72 | }); 73 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/help/InfoDisplayRuleHelpPage.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from 'vitest'; 2 | import { InfoDisplayRuleHelpPage } from '../../../../../../Canopy [BP]/scripts/lib/canopy/help/InfoDisplayRuleHelpPage'; 3 | import { InfoDisplayRule } from '../../../../../../Canopy [BP]/scripts/lib/canopy/InfoDisplayRule'; 4 | import { InfoDisplayRuleHelpEntry } from '../../../../../../Canopy [BP]/scripts/lib/canopy/help/InfoDisplayRuleHelpEntry'; 5 | import { Rules } from '../../../../../../Canopy [BP]/scripts/lib/canopy/Rules'; 6 | 7 | vi.mock("@minecraft/server", () => ({ 8 | world: { 9 | beforeEvents: { 10 | chatSend: { 11 | subscribe: vi.fn() 12 | } 13 | }, 14 | afterEvents: { 15 | worldLoad: { 16 | subscribe: vi.fn() 17 | } 18 | }, 19 | getDynamicProperty: vi.fn() 20 | }, 21 | system: { 22 | afterEvents: { 23 | scriptEventReceive: { 24 | subscribe: vi.fn() 25 | } 26 | }, 27 | runJob: vi.fn() 28 | } 29 | })); 30 | 31 | describe('InfoDisplayRuleHelpPage', () => { 32 | describe('constructor', () => { 33 | it('should create an instance of InfoDisplayRuleHelpPage', () => { 34 | const page = new InfoDisplayRuleHelpPage({ title: 'Test', description: 'Test description', usage: 'Test usage' }); 35 | expect(page).toBeInstanceOf(InfoDisplayRuleHelpPage); 36 | }); 37 | }); 38 | 39 | describe('addEntry', () => { 40 | beforeEach(() => { 41 | Rules.clear(); 42 | }); 43 | 44 | it('should add an entry if it is an instance of InfoDisplayRule', () => { 45 | const page = new InfoDisplayRuleHelpPage({ title: 'Test', description: 'Test description', usage: 'Test usage' }); 46 | const rule = new InfoDisplayRule({ identifier: 'testRule', description: 'Test rule description' }); 47 | page.addEntry(rule, 'player1'); 48 | expect(page.entries).toHaveLength(1); 49 | expect(page.entries[0]).toBeInstanceOf(InfoDisplayRuleHelpEntry); 50 | }); 51 | 52 | it('should throw an error if the entry is not an instance of InfoDisplayRule', () => { 53 | const page = new InfoDisplayRuleHelpPage({ title: 'Test', description: 'Test description', usage: 'Test usage' }); 54 | expect(() => page.addEntry({}, 'player1')).toThrow('[HelpPage] Entry must be an instance of InfoDisplayRule'); 55 | }); 56 | 57 | it('should not add duplicate entries', () => { 58 | const page = new InfoDisplayRuleHelpPage({ title: 'Test', description: 'Test description', usage: 'Test usage' }); 59 | const rule = new InfoDisplayRule({ identifier: 'testRule', description: 'Test rule description' }); 60 | page.addEntry(rule, 'player1'); 61 | page.addEntry(rule, 'player1'); 62 | expect(page.entries).toHaveLength(1); 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/lib/canopy/help/RuleHelpEntry.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { RuleHelpEntry } from "../../../../../../Canopy [BP]/scripts/lib/canopy/help/RuleHelpEntry"; 3 | 4 | describe('RuleHelpEntry', () => { 5 | describe('constructor', () => { 6 | it('should create an instance with the correct properties', () => { 7 | const mockRule = { 8 | getID: () => 'testRule', 9 | getDescription: () => 'This is a test rule', 10 | getValue: () => true 11 | }; 12 | const entry = new RuleHelpEntry(mockRule); 13 | expect(entry.rule).toBe(mockRule); 14 | expect(entry.title).toBe('testRule'); 15 | expect(entry.description).toEqual({ text: 'This is a test rule' }); 16 | }); 17 | }); 18 | 19 | describe('fetchColoredValue', () => { 20 | it('should generate the correct raw message', async () => { 21 | const mockRule = { 22 | getID: () => 'testRule', 23 | getDescription: () => 'This is a test rule', 24 | getValue: vi.fn().mockResolvedValue(true) 25 | }; 26 | const entry = new RuleHelpEntry(mockRule); 27 | const rawMessage = await entry.toRawMessage(); 28 | expect(rawMessage).toEqual({ 29 | rawtext: [ 30 | { text: '§7testRule: §atrue§r§8 - ' }, 31 | { text: 'This is a test rule' } 32 | ] 33 | }); 34 | }); 35 | 36 | it('should generate the correct raw message when value is false', async () => { 37 | const mockRule = { 38 | getID: () => 'testRule', 39 | getDescription: () => 'This is a test rule', 40 | getValue: vi.fn().mockResolvedValue(false) 41 | }; 42 | const entry = new RuleHelpEntry(mockRule); 43 | const rawMessage = await entry.toRawMessage(); 44 | expect(rawMessage).toEqual({ 45 | rawtext: [ 46 | { text: '§7testRule: §cfalse§r§8 - ' }, 47 | { text: 'This is a test rule' } 48 | ] 49 | }); 50 | }); 51 | }); 52 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/src/classes/Profiler.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { Profiler } from "../../../../../Canopy [BP]/scripts/src/classes/Profiler"; 3 | 4 | vi.mock("@minecraft/server", () => ({ 5 | system: { 6 | runInterval: vi.fn((callback, interval) => { 7 | const intervalId = setInterval(callback, interval * 50); 8 | return { 9 | clear: () => clearInterval(intervalId) 10 | }; 11 | }), 12 | runTimeout: vi.fn((callback, timeout) => { 13 | const timeoutId = setTimeout(callback, timeout * 50); 14 | return { 15 | clear: () => clearTimeout(timeoutId) 16 | }; 17 | }), 18 | clearRun: vi.fn((runner) => { 19 | runner.clear(); 20 | }) 21 | } 22 | })); 23 | 24 | vi.mock("@minecraft/server-ui", () => ({ 25 | ModalFormData: vi.fn() 26 | })); 27 | 28 | describe('Profiler', () => { 29 | it('should have a getter for the instant MS', () => { 30 | expect(Profiler.tickMs).toBeDefined(); 31 | }); 32 | 33 | it('should have a getter for the instant TPS', () => { 34 | expect(Profiler.tickTps).toBeDefined(); 35 | }); 36 | 37 | it('should have a getter for the smoothed TPS', () => { 38 | expect(Profiler.tps).toBeDefined(); 39 | }); 40 | 41 | it('should start profiling when start() is called', () => { 42 | Profiler.start(); 43 | expect(Profiler.lastTickDate).toBeLessThanOrEqual(Date.now()); 44 | }); 45 | 46 | it('should not start profiling if it is already running', () => { 47 | Profiler.start(); 48 | const lastTickDate = Profiler.lastTickDate; 49 | Profiler.start(); 50 | expect(Profiler.lastTickDate).toBeLessThanOrEqual(lastTickDate); 51 | }); 52 | 53 | it.skip('should profile both TPS and MSPT when profile() is called', async () => { 54 | // Gametest 55 | }); 56 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/src/commands/health.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import { healthCommand } from "../../../../../Canopy [BP]/scripts/src/commands/health"; 3 | 4 | vi.mock("@minecraft/server", () => ({ 5 | world: { 6 | beforeEvents: { 7 | chatSend: { 8 | subscribe: vi.fn() 9 | } 10 | }, 11 | afterEvents: { 12 | worldLoad: { 13 | subscribe: vi.fn() 14 | } 15 | } 16 | }, 17 | system: { 18 | afterEvents: { 19 | scriptEventReceive: { 20 | subscribe: vi.fn() 21 | } 22 | 23 | }, 24 | runJob: vi.fn(), 25 | runInterval: vi.fn(), 26 | runTimeout: vi.fn() 27 | }, 28 | DimensionTypes: { 29 | getAll: () => [ 30 | { typeId: "minecraft:overworld" }, 31 | { typeId: "minecraft:nether" }, 32 | { typeId: "minecraft:the_end" } 33 | ] 34 | } 35 | })); 36 | 37 | vi.mock("@minecraft/server-ui", () => ({ 38 | ModalFormData: vi.fn() 39 | })); 40 | 41 | describe('healthCommand', () => { 42 | it('should profile the tps & mspt', () => { 43 | const sender = { 44 | sendMessage: vi.fn() 45 | }; 46 | healthCommand(sender); 47 | expect(sender.sendMessage).toHaveBeenCalled(); 48 | }); 49 | 50 | it('should print the dimension entities', () => { 51 | const sender = { 52 | sendMessage: vi.fn() 53 | }; 54 | healthCommand(sender); 55 | expect(sender.sendMessage).toHaveBeenCalled(); 56 | }); 57 | }); -------------------------------------------------------------------------------- /__tests__/BP/scripts/src/rules/allowBubbleColumnPlacement.test.js: -------------------------------------------------------------------------------- 1 | import { allowBubbleColumnPlacement } from "../../../../../Canopy [BP]/scripts/src/rules/allowBubbleColumnPlacement"; 2 | import { expect, test, describe, vi, afterEach } from "vitest"; 3 | 4 | vi.mock("@minecraft/server", () => ({ 5 | system: { 6 | afterEvents: { 7 | scriptEventReceive: { 8 | subscribe: vi.fn() 9 | }, 10 | playerPlaceBlock: { 11 | subscribe: vi.fn() 12 | } 13 | }, 14 | runJob: vi.fn(), 15 | run: vi.fn((callback) => callback()) 16 | }, 17 | world: { 18 | beforeEvents: { 19 | chatSend: { 20 | subscribe: vi.fn() 21 | }, 22 | playerPlaceBlock: { 23 | subscribe: vi.fn(), 24 | unsubscribe: vi.fn() 25 | } 26 | }, 27 | afterEvents: { 28 | worldLoad: { 29 | subscribe: vi.fn() 30 | } 31 | }, 32 | getDynamicProperty: vi.fn(), 33 | setDynamicProperty: vi.fn(), 34 | structureManager: { 35 | place: vi.fn() 36 | } 37 | } 38 | })); 39 | 40 | vi.mock("@minecraft/server-ui", () => ({ 41 | ModalFormData: vi.fn() 42 | })); 43 | 44 | describe('allowBubbleColumnPlacement', () => { 45 | afterEach(() => { 46 | vi.clearAllMocks(); 47 | }); 48 | 49 | test('should subscribe to player block placements when enabled', () => { 50 | const subscribeSpy = vi.spyOn(allowBubbleColumnPlacement, 'subscribeToEvent'); 51 | allowBubbleColumnPlacement.onEnable(); 52 | expect(subscribeSpy).toHaveBeenCalled(); 53 | subscribeSpy.mockRestore(); 54 | }); 55 | 56 | test('should unsubscribe from player block placements when disabled', () => { 57 | const unsubscribeSpy = vi.spyOn(allowBubbleColumnPlacement, 'unsubscribeFromEvent'); 58 | allowBubbleColumnPlacement.onDisable(); 59 | expect(unsubscribeSpy).toHaveBeenCalled(); 60 | unsubscribeSpy.mockRestore(); 61 | }); 62 | 63 | test('should ensure placement when placing bubble column', () => { 64 | const placeBubbleColumnSpy = vi.spyOn(allowBubbleColumnPlacement, 'placeBubbleColumn') 65 | const event = { 66 | player: { getComponent: vi.fn(() => ({ getEquipment: vi.fn(() => ({ typeId: 'minecraft:bubble_column' }) ) }) ) }, 67 | dimension: 'overworld', 68 | block: { location: { x: 0, y: 0, z: 0 } } 69 | }; 70 | allowBubbleColumnPlacement.onPlayerPlaceBlock(event); 71 | expect(placeBubbleColumnSpy).toHaveBeenCalledWith('overworld', { x: 0, y: 0, z: 0 }); 72 | }); 73 | 74 | test('should do nothing if placed by a Simulated Player', () => { 75 | const placeBubbleColumnSpy = vi.spyOn(allowBubbleColumnPlacement, 'placeBubbleColumn') 76 | const event = { 77 | player: void 0, 78 | dimension: 'overworld', 79 | block: { location: { x: 0, y: 0, z: 0 } } 80 | }; 81 | allowBubbleColumnPlacement.onPlayerPlaceBlock(event); 82 | expect(placeBubbleColumnSpy).not.toHaveBeenCalled(); 83 | }); 84 | }); -------------------------------------------------------------------------------- /__tests__/manifests.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeAll } from "vitest"; 2 | import fs from "fs"; 3 | import path from 'path'; 4 | import { PACK_VERSION, MC_VERSION } from "../Canopy [BP]/scripts/constants"; 5 | 6 | const manifestPathBP = path.resolve('Canopy [BP]/manifest.json'); 7 | const manifestPathRP = path.resolve('Canopy [RP]/manifest.json'); 8 | 9 | function getManifestObject(manifestPath) { 10 | const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); 11 | return JSON.parse(manifestContent); 12 | } 13 | 14 | function getCanopyManifestVersion() { 15 | const manifestContent = fs.readFileSync(manifestPathBP, 'utf-8'); 16 | const content = JSON.parse(manifestContent); 17 | const version = content.header.version; 18 | return `${version[0]}.${version[1]}.${version[2]}`; 19 | } 20 | 21 | describe('Manifests', () => { 22 | describe('BP', () => { 23 | let manifestContentBP; 24 | let packVersion; 25 | let mcVersion; 26 | beforeAll(() => { 27 | manifestContentBP = getManifestObject(manifestPathBP); 28 | packVersion = PACK_VERSION.split('.').map(Number); 29 | mcVersion = MC_VERSION.split('.').map(Number); 30 | }); 31 | 32 | it('should include version number in its name', () => { 33 | expect(manifestContentBP.header.name).toBe(`Canopy [BP] v${getCanopyManifestVersion()}`); 34 | }); 35 | 36 | it('should match constants version number', () => { 37 | expect(manifestContentBP.header.version).toEqual(packVersion); 38 | }); 39 | 40 | it('should match constants min_engine_version', () => { 41 | expect(manifestContentBP.header.min_engine_version).toEqual(mcVersion.slice(0, -1)); 42 | }); 43 | }); 44 | 45 | describe('RP', () => { 46 | it('RP name should include version number', () => { 47 | const manifestContentRP = getManifestObject(manifestPathRP); 48 | expect(manifestContentRP.header.name).toBe(`Canopy [RP] v${getCanopyManifestVersion()}`); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /amelix-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/amelix-logo.gif -------------------------------------------------------------------------------- /canopylogo_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ForestOfLight/Canopy/55a34e54a7b2afbd41da63274e879a6864c11a07/canopylogo_banner.jpg -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | 4 | import { includeIgnoreFile } from "@eslint/compat"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | 8 | const filename = fileURLToPath(import.meta.url); 9 | const dirname = path.dirname(filename); 10 | const gitignorePath = path.resolve(dirname, ".gitignore"); 11 | 12 | export default [ 13 | { 14 | ignores: [ 15 | '**/scripts/lib/mt.js', 16 | '**/scripts/lib/MCBE-IPC/', 17 | '**/scripts/lib/SRCItemDatabase/', 18 | '**/scripts/lib/chestui/' 19 | ] 20 | }, 21 | js.configs.recommended, 22 | { 23 | languageOptions: { 24 | sourceType: "module", 25 | globals: { 26 | ...globals.node 27 | } 28 | } 29 | }, 30 | includeIgnoreFile(gitignorePath), 31 | { 32 | rules: { 33 | "no-constructor-return": "error", 34 | "no-duplicate-imports": "error", 35 | "no-template-curly-in-string": "error", 36 | "no-unreachable-loop": "error", 37 | "no-use-before-define": ["error", { "functions": false }], 38 | "no-useless-assignment": "error", 39 | // Suggestions: 40 | "arrow-body-style": "error", 41 | "block-scoped-var": "error", 42 | "camelcase": [ "warn", { "ignoreImports": true } ], 43 | "curly": ["error", "multi-or-nest", "consistent"], 44 | "default-case": "error", 45 | "default-case-last": "error", 46 | "eqeqeq": ["error", "smart"], 47 | "func-style": ["error", "declaration", { "allowArrowFunctions": true }], 48 | "max-classes-per-file": ["error", 1], // { ignoreExpressions: true } 49 | "max-depth": ["warn"], 50 | "max-lines": ["warn"], 51 | "max-lines-per-function": ["warn"], 52 | "max-params": ["warn"], 53 | "new-cap": "error", 54 | "no-else-return": "error", 55 | "no-lonely-if": "error", 56 | "no-negated-condition": "error", 57 | "no-nested-ternary": "error", 58 | "no-return-assign": "error", 59 | "no-shadow": "error", 60 | "no-throw-literal": "error", 61 | "no-underscore-dangle": "error", 62 | "no-unneeded-ternary": "error", 63 | "no-useless-computed-key": "error", 64 | "no-useless-concat": "error", 65 | "no-useless-constructor": "error", 66 | "no-useless-return": "error", 67 | "no-var": "error", 68 | "no-warning-comments": "warn", 69 | "one-var": ["error", "never"], 70 | "operator-assignment": "error", 71 | "prefer-const": "error", 72 | "require-await": "error", 73 | "yoda": "error" 74 | } 75 | }, 76 | { 77 | files: ["**/__tests__/**"], 78 | rules: { 79 | "max-lines-per-function": "off", 80 | "max-lines": "off" 81 | } 82 | } 83 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canopy", 3 | "author": { 4 | "name": "ForestOfLight", 5 | "email": "forestpupoozzle@gmail.com" 6 | }, 7 | "type": "module", 8 | "bugs": { 9 | "url": "https://github.com/ForestOfLight/Canopy/issues" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "@minecraft/server": "^2.0.0-beta.1.21.70-stable", 14 | "@minecraft/server-ui": "^2.0.0-beta.1.21.70-stable" 15 | }, 16 | "scripts": { 17 | "test": "vitest run --coverage", 18 | "lint": "eslint . --fix" 19 | }, 20 | "devDependencies": { 21 | "@eslint/compat": "^1.2.5", 22 | "@eslint/js": "^9.19.0", 23 | "@vitest/coverage-v8": "^3.0.4", 24 | "axios": "^1.7.9", 25 | "eslint": "^9.19.0", 26 | "globals": "^15.14.0", 27 | "strip-json-comments": "^5.0.1", 28 | "vitest": "^3.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | server: { 5 | deps: { 6 | external: ['@minecraft/server', '@minecraft/server-ui'], 7 | } 8 | }, 9 | resolve: { 10 | alias: { 11 | '@minecraft/server': `__mocks__/@minecraft/server`, 12 | '@minecraft/server-ui': `__mocks__/@minecraft/server-ui`, 13 | } 14 | }, 15 | test: { 16 | testFiles: '**/__tests__/**/*.test.js', 17 | files: '**/__tests__/**', 18 | env: { 19 | NODE_ENV: 'test' 20 | } 21 | } 22 | }); --------------------------------------------------------------------------------