From 0974e689df93585ee82672db56562e8e946808e4 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 15 Feb 2026 01:43:56 -0800 Subject: [PATCH 01/16] fix code blocks in changelog getting removed --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3bc6fe..59ed1b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload to CurseForge + env: + RELEASE_BODY: ${{ github.event.release.body }} run: | VERSION=${{ github.event.release.tag_name }} @@ -61,7 +63,7 @@ jobs: echo "Found CurseForge version IDs for game version $ENGINE_PREFIX: $GAME_VERSION_IDS" # Use the Github release body as the changelog - CHANGELOG=$(jq -Rs . <<< "${{ github.event.release.body }}") + CHANGELOG=$(printf "%s" "$RELEASE_BODY" | jq -Rs .) ADDON_FILENAME=Canopy-$VERSION.mcaddon # Upload the addon From 9c8bb0f905657aa2378925d3786ba9f86bec9284 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 15 Feb 2026 18:31:36 -0800 Subject: [PATCH 02/16] add entitySeparation --- Canopy [BP]/scripts/main.js | 1 + .../scripts/src/rules/entitySeparation.js | 78 +++++++++++ Canopy [RP]/texts/en_US.lang | 1 + .../src/rules/entitySeparation.test.js | 126 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 Canopy [BP]/scripts/src/rules/entitySeparation.js create mode 100644 __tests__/BP/scripts/src/rules/entitySeparation.test.js diff --git a/Canopy [BP]/scripts/main.js b/Canopy [BP]/scripts/main.js index ff31db8..16c8c5d 100644 --- a/Canopy [BP]/scripts/main.js +++ b/Canopy [BP]/scripts/main.js @@ -78,6 +78,7 @@ import './src/rules/echoShardsEnableShriekers' import './src/rules/collisionBoxes' import './src/rules/potionBoostedBreeding' import './src/rules/serverSideCollisionBoxes' +import './src/rules/entitySeparation' // Load Time Processes import './src/onStart' diff --git a/Canopy [BP]/scripts/src/rules/entitySeparation.js b/Canopy [BP]/scripts/src/rules/entitySeparation.js new file mode 100644 index 0000000..9be5b06 --- /dev/null +++ b/Canopy [BP]/scripts/src/rules/entitySeparation.js @@ -0,0 +1,78 @@ +import { world } from "@minecraft/server"; +import { BooleanRule, GlobalRule } from "../../lib/canopy/Canopy"; +import { Vector } from "../../lib/Vector"; + +export class EntitySeparation extends BooleanRule { + activationBlockType = 'minecraft:dropper'; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'entitySeparation', + onEnableCallback: () => this.subscribeToEvent(), + onDisableCallback: () => this.unsubscribeFromEvent() + })); + this.onPressurePlatePushBound = this.onPressurePlatePush.bind(this); + } + + subscribeToEvent() { + world.afterEvents.pressurePlatePush.subscribe(this.onPressurePlatePushBound); + } + + unsubscribeFromEvent() { + world.afterEvents.pressurePlatePush.unsubscribe(this.onPressurePlatePushBound); + } + + onPressurePlatePush(event) { + const entity = event.source; + const activationBlock = this.findActivationBlock(event.block); + if (entity?.isValid && activationBlock && this.numEntitiesStacked(entity) > 1) { + const facingDirection = activationBlock.permutation.getState('facing_direction'); + const offset = this.getOffsetFromFacingDirection(facingDirection); + this.separateEntity(entity, offset); + } + } + + separateEntity(entity, offset) { + const newLocation = Vector.from(entity.location).add(offset); + const teleportOptions = {}; + if (entity.typeId !== 'minecraft:player') + teleportOptions.keepVelocity = true; + entity.teleport(newLocation, teleportOptions); + } + + findActivationBlock(block) { + const directionsToCheck = [ + Vector.up, + Vector.down, + Vector.left, + Vector.right, + Vector.forward, + Vector.backward + ]; + for (const offset of directionsToCheck) { + const neighborBlock = block.offset(offset); + if (neighborBlock.typeId === this.activationBlockType) + return neighborBlock; + } + return void 0; + } + + getOffsetFromFacingDirection(facingDirection) { + const facingDirectionToOffset = { + 0: Vector.down, + 1: Vector.up, + 2: Vector.backward, + 3: Vector.forward, + 4: Vector.left, + 5: Vector.right + }; + return facingDirectionToOffset[facingDirection]; + } + + numEntitiesStacked(entity) { + const blockLocation = Vector.from(entity.location).floor(); + return entity.dimension.getEntitiesAtBlockLocation(blockLocation).length; + } +} + +export const entitySeparation = new EntitySeparation(); \ No newline at end of file diff --git a/Canopy [RP]/texts/en_US.lang b/Canopy [RP]/texts/en_US.lang index d63be26..577d1af 100644 --- a/Canopy [RP]/texts/en_US.lang +++ b/Canopy [RP]/texts/en_US.lang @@ -377,6 +377,7 @@ rules.durabilityNotifier.alert=§cDurability remaining: %s rules.durabilitySwap=Swaps 0 durability items out of your hand. rules.echoShardsEnableShriekers=Using an echo shard on a sculk shrieker allows it to summon wardens. rules.entityInstantDeath=Removes the 20gt death animation. Entities will also not drop xp. +rules.entitySeparation=When stacked entities trigger a pressure plate, one entity will be unstacked in the direction a neighboring dropper is facing. rules.explosionChainReactionOnly=Makes explosions only affect TNT blocks. rules.explosionNoBlockDamage=Makes explosions not affect blocks. rules.explosionOff=Disables explosions entirely. diff --git a/__tests__/BP/scripts/src/rules/entitySeparation.test.js b/__tests__/BP/scripts/src/rules/entitySeparation.test.js new file mode 100644 index 0000000..c696959 --- /dev/null +++ b/__tests__/BP/scripts/src/rules/entitySeparation.test.js @@ -0,0 +1,126 @@ +import { vi, it, describe, expect, beforeEach } from "vitest"; +import { entitySeparation } from "../../../../../Canopy [BP]/scripts/src/rules/entitySeparation"; +import { Vector } from "../../../../../Canopy [BP]/scripts/lib/Vector"; + +vi.mock("@minecraft/server", () => ({ + system: { + afterEvents: { + scriptEventReceive: { + subscribe: vi.fn() + } + }, + runJob: vi.fn(), + run: vi.fn((callback) => callback()) + }, + world: { + beforeEvents: { + chatSend: { + subscribe: vi.fn() + } + }, + afterEvents: { + worldLoad: { + subscribe: vi.fn() + }, + pressurePlatePush: { + subscribe: vi.fn(), + unsubscribe: vi.fn() + } + }, + getDynamicProperty: vi.fn(), + setDynamicProperty: vi.fn(), + structureManager: { + place: vi.fn() + } + } +})); + +vi.mock("@minecraft/server-ui", () => ({ + ModalFormData: vi.fn() +})); + +describe('entitySeparation', () => { + let successfulEvent = {}; + let sourceEntity = {}; + beforeEach(() => { + sourceEntity = { + id: 0, + isValid: true, + typeId: 'minecraft:test_entity', + teleport: vi.fn(), + location: { x: 0, y: 0, z: 0 }, + dimension: { + getEntitiesAtBlockLocation: vi.fn(() => ([ + sourceEntity, + { id: 1 } + ])) + } + }; + successfulEvent = { + block: { + offset: vi.fn(() => ({ + typeId: 'minecraft:dropper', + permutation: { + getState: vi.fn(() => 0) + } + })) + }, + source: sourceEntity + }; + vi.restoreAllMocks(); + }); + + it('should subscribe to pressure plate pushes when enabled', () => { + const subscribeSpy = vi.spyOn(entitySeparation, 'subscribeToEvent'); + entitySeparation.onEnable(); + expect(subscribeSpy).toHaveBeenCalled(); + }); + + it('should unsubscribe from pressure plate pushes when disabled', () => { + const unsubscribeSpy = vi.spyOn(entitySeparation, 'unsubscribeFromEvent'); + entitySeparation.onDisable(); + expect(unsubscribeSpy).toHaveBeenCalled(); + }); + + it('should move one entity on success', () => { + entitySeparation.onPressurePlatePush(successfulEvent); + expect(sourceEntity.teleport).toHaveBeenCalled(); + }); + + it('should not succeed if a dropper is not next to the pressure plate', () => { + successfulEvent.block.offset.mockReturnValue({ typeId: 'minecraft:air' }); + entitySeparation.onPressurePlatePush(successfulEvent); + expect(sourceEntity.teleport).not.toHaveBeenCalled(); + }); + + it('should not succeed if there is only one entity on the pressure plate', () => { + sourceEntity.dimension.getEntitiesAtBlockLocation.mockReturnValue([{ id: 0 }]); + entitySeparation.onPressurePlatePush(successfulEvent); + expect(sourceEntity.teleport).not.toHaveBeenCalled(); + }); + + it.each([ + [0, Vector.down], + [1, Vector.up], + [2, Vector.backward], + [3, Vector.forward], + [4, Vector.left], + [5, Vector.right] + ])('when the dropper facing_direction is %i, should separate with offset %o', (facingDirection, offset) => { + const separateEntitySpy = vi.spyOn(entitySeparation, 'separateEntity'); + successfulEvent.block.offset = vi.fn(() => ({ + typeId: 'minecraft:dropper', + permutation: { + getState: vi.fn(() => facingDirection) + } + })); + entitySeparation.onPressurePlatePush(successfulEvent); + expect(separateEntitySpy).toHaveBeenCalledWith(sourceEntity, offset); + }); + + it('should not keep player velocity after separation', () => { + successfulEvent.source.typeId = 'minecraft:player'; + entitySeparation.onPressurePlatePush(successfulEvent); + expect(sourceEntity.teleport).toHaveBeenCalledWith(new Vector(0, -1, 0), {}); + }); +}); \ No newline at end of file From 4c176f064e550853bf72d1346543638535634edc Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 16 Feb 2026 13:30:08 -0800 Subject: [PATCH 03/16] destory invalid entities during serverSideCollisionBoxes --- .../src/classes/debugdisplay/DebugDisplayShapeElement.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Canopy [BP]/scripts/src/classes/debugdisplay/DebugDisplayShapeElement.js b/Canopy [BP]/scripts/src/classes/debugdisplay/DebugDisplayShapeElement.js index 393311a..f2e8985 100644 --- a/Canopy [BP]/scripts/src/classes/debugdisplay/DebugDisplayShapeElement.js +++ b/Canopy [BP]/scripts/src/classes/debugdisplay/DebugDisplayShapeElement.js @@ -56,6 +56,10 @@ export class DebugDisplayShapeElement extends DebugDisplayElement { } updateServerSidePosition() { + if (!this.entity?.isValid) { + this.destroy(); + return; + } const dimensionLocation = this.getClientSideLocation().add(this.entity.location); dimensionLocation.dimension = this.entity.dimension; this.shapes.forEach((debugShape) => debugShape.setLocation(dimensionLocation)); From 95fef34a115c500451ab4d15814e3b95e8c525e9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 16 Feb 2026 14:19:45 -0800 Subject: [PATCH 04/16] make entitySeparation easier to deal with the last entity removed the "must be stacked" requirement this allows for the conveyer behavior as well --- Canopy [BP]/scripts/src/rules/entitySeparation.js | 7 +------ __tests__/BP/scripts/src/rules/entitySeparation.test.js | 6 ------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Canopy [BP]/scripts/src/rules/entitySeparation.js b/Canopy [BP]/scripts/src/rules/entitySeparation.js index 9be5b06..14dfcfc 100644 --- a/Canopy [BP]/scripts/src/rules/entitySeparation.js +++ b/Canopy [BP]/scripts/src/rules/entitySeparation.js @@ -25,7 +25,7 @@ export class EntitySeparation extends BooleanRule { onPressurePlatePush(event) { const entity = event.source; const activationBlock = this.findActivationBlock(event.block); - if (entity?.isValid && activationBlock && this.numEntitiesStacked(entity) > 1) { + if (entity?.isValid && activationBlock) { const facingDirection = activationBlock.permutation.getState('facing_direction'); const offset = this.getOffsetFromFacingDirection(facingDirection); this.separateEntity(entity, offset); @@ -68,11 +68,6 @@ export class EntitySeparation extends BooleanRule { }; return facingDirectionToOffset[facingDirection]; } - - numEntitiesStacked(entity) { - const blockLocation = Vector.from(entity.location).floor(); - return entity.dimension.getEntitiesAtBlockLocation(blockLocation).length; - } } export const entitySeparation = new EntitySeparation(); \ No newline at end of file diff --git a/__tests__/BP/scripts/src/rules/entitySeparation.test.js b/__tests__/BP/scripts/src/rules/entitySeparation.test.js index c696959..fd75733 100644 --- a/__tests__/BP/scripts/src/rules/entitySeparation.test.js +++ b/__tests__/BP/scripts/src/rules/entitySeparation.test.js @@ -93,12 +93,6 @@ describe('entitySeparation', () => { expect(sourceEntity.teleport).not.toHaveBeenCalled(); }); - it('should not succeed if there is only one entity on the pressure plate', () => { - sourceEntity.dimension.getEntitiesAtBlockLocation.mockReturnValue([{ id: 0 }]); - entitySeparation.onPressurePlatePush(successfulEvent); - expect(sourceEntity.teleport).not.toHaveBeenCalled(); - }); - it.each([ [0, Vector.down], [1, Vector.up], From 2b5adb28d5c600f8d3cde7fca7c8be1c5f0b9bca Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 27 Feb 2026 20:45:48 -0800 Subject: [PATCH 05/16] add enderPearlChunkLoading Adds a new IntegerRule, where the radius of chunks around the pearl that should be ticked are the value of the rule. Set to -1 to disable. Also makes sure to start/stop ticking for valid pearl entities in the world during rule value modifications. --- Canopy [BP]/entities/ender_pearl.json | 151 ++++++++++++++++++ Canopy [BP]/manifest.json | 2 +- Canopy [BP]/scripts/main.js | 1 + .../src/rules/enderPearlChunkLoading.js | 71 ++++++++ Canopy [RP]/texts/en_US.lang | 1 + 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 Canopy [BP]/entities/ender_pearl.json create mode 100644 Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js diff --git a/Canopy [BP]/entities/ender_pearl.json b/Canopy [BP]/entities/ender_pearl.json new file mode 100644 index 0000000..5202488 --- /dev/null +++ b/Canopy [BP]/entities/ender_pearl.json @@ -0,0 +1,151 @@ +{ + "format_version": "1.12.0", + "minecraft:entity": { + "description": { + "identifier": "minecraft:ender_pearl", + "is_spawnable": false, + "is_summonable": false, + "is_experimental": false + }, + "component_groups": { + "minecraft:no_spawn": { + "minecraft:projectile": { + "on_hit": { + "teleport_owner": { }, + "remove_on_hit": { } + }, + "power": 1.5, + "gravity": 0.025, + "angle_offset": 0.0, + "inertia": 1, + "liquid_inertia": 1 + } + }, + "canopy:tick_world_r2": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 2 + } + }, + "canopy:tick_world_r3": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 3 + } + }, + "canopy:tick_world_r4": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 4 + } + }, + "canopy:tick_world_r5": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 5 + } + }, + "canopy:tick_world_r6": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 6 + } + } + }, + + "components": { + "minecraft:collision_box": { + "width": 0.25, + "height": 0.25 + }, + "minecraft:projectile": { + "on_hit": { + "teleport_owner": { }, + "spawn_chance": { + "first_spawn_percent_chance": 5.0, + "first_spawn_count": 1, + "spawn_definition": "minecraft:endermite" + }, + "remove_on_hit": { } + }, + "power": 1.5, + "gravity": 0.025, + "angle_offset": 0.0, + "inertia": 1, + "liquid_inertia": 1 + }, + "minecraft:physics": { + }, + "minecraft:pushable": { + "is_pushable": true, + "is_pushable_by_piston": true + }, + "minecraft:conditional_bandwidth_optimization": { + "default_values": { + "max_optimized_distance": 80.0, + "max_dropped_ticks": 7, + "use_motion_prediction_hints": true + } + } + }, + + "events": { + "minecraft:entity_spawned": { + "sequence": [ + { + "filters": {"test": "is_game_rule", "domain": "domobspawning", "value": false}, + "add": { + "component_groups": [ "minecraft:no_spawn" ] + } + } + ] + }, + "canopy:start_ticking_r2": { + "add": { + "component_groups": [ + "canopy:tick_world_r2" + ] + } + }, + "canopy:start_ticking_r3": { + "add": { + "component_groups": [ + "canopy:tick_world_r3" + ] + } + }, + "canopy:start_ticking_r4": { + "add": { + "component_groups": [ + "canopy:tick_world_r4" + ] + } + }, + "canopy:start_ticking_r5": { + "add": { + "component_groups": [ + "canopy:tick_world_r5" + ] + } + }, + "canopy:start_ticking_r6": { + "add": { + "component_groups": [ + "canopy:tick_world_r6" + ] + } + }, + "canopy:stop_ticking": { + "remove": { + "component_groups": [ + "canopy:tick_world_r2", + "canopy:tick_world_r3", + "canopy:tick_world_r4", + "canopy:tick_world_r5", + "canopy:tick_world_r6" + ] + } + } + } + } +} \ No newline at end of file diff --git a/Canopy [BP]/manifest.json b/Canopy [BP]/manifest.json index 5d38fb5..fa5d94f 100644 --- a/Canopy [BP]/manifest.json +++ b/Canopy [BP]/manifest.json @@ -26,7 +26,7 @@ "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.6.0-beta" + "version": "2.7.0-beta" }, { "module_name": "@minecraft/server-ui", diff --git a/Canopy [BP]/scripts/main.js b/Canopy [BP]/scripts/main.js index 16c8c5d..6c38a6f 100644 --- a/Canopy [BP]/scripts/main.js +++ b/Canopy [BP]/scripts/main.js @@ -79,6 +79,7 @@ import './src/rules/collisionBoxes' import './src/rules/potionBoostedBreeding' import './src/rules/serverSideCollisionBoxes' import './src/rules/entitySeparation' +import './src/rules/enderPearlChunkLoading' // Load Time Processes import './src/onStart' diff --git a/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js b/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js new file mode 100644 index 0000000..2534c4a --- /dev/null +++ b/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js @@ -0,0 +1,71 @@ +import { getEntitiesByType } from "../../include/utils"; +import { GlobalRule, IntegerRule } from "../../lib/canopy/Canopy"; +import { world } from '@minecraft/server'; + +class EnderPearlChunkLoading extends IntegerRule { + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'enderPearlChunkLoading', + onModifyCallback: (newValue) => this.tryStartTicking(newValue), + defaultValue: -1, + valueRange: { range: { min: 2, max: 6 }, other: [-1] } + })); + this.onEntityAppearBound = this.onEntityAppear.bind(this); + } + + tryStartTicking(newValue) { + if (newValue === -1) { + this.unsubscribeFromEvents(); + this.disableAllTickingPearls(); + } else { + this.subscribeToEvents(); + this.enableAllTickingPearls(); + } + } + + subscribeToEvents() { + world.afterEvents.entitySpawn.subscribe(this.onEntityAppearBound); + world.afterEvents.entityLoad.subscribe(this.onEntityAppearBound); + } + + unsubscribeFromEvents() { + world.afterEvents.entitySpawn.unsubscribe(this.onEntityAppearBound); + world.afterEvents.entityLoad.unsubscribe(this.onEntityAppearBound); + } + + onEntityAppear(event) { + const pearl = event.entity; + if (pearl?.typeId !== 'minecraft:ender_pearl') + return; + this.startTicking(pearl); + } + + enableAllTickingPearls() { + getEntitiesByType('minecraft:ender_pearl').forEach(pearl => { + this.startTicking(pearl); + }); + } + + disableAllTickingPearls() { + getEntitiesByType('minecraft:ender_pearl').forEach(pearl => { + this.safelyTriggerEvent(pearl, 'canopy:stop_ticking'); + }); + } + + startTicking(pearl) { + const chunkRadiusToTick = this.getNativeValue(); + const eventName = `canopy:start_ticking_r${chunkRadiusToTick}`; + this.safelyTriggerEvent(pearl, eventName); + } + + safelyTriggerEvent(pearl, eventName) { + try { + pearl?.triggerEvent(eventName); + } catch(error) { + if (error.includes(`${eventName} does not exist on minecraft:ender_pearl`)) + throw new Error(`[Canopy] ${eventName} could not be triggered on minecraft:ender_pearl. Are you using another pack that overrides ender pearls?`); + } + } +} + +export const enderPearlChunkLoading = new EnderPearlChunkLoading(); \ No newline at end of file diff --git a/Canopy [RP]/texts/en_US.lang b/Canopy [RP]/texts/en_US.lang index 577d1af..e9fa083 100644 --- a/Canopy [RP]/texts/en_US.lang +++ b/Canopy [RP]/texts/en_US.lang @@ -376,6 +376,7 @@ rules.durabilityNotifier=Enables a clink sound and tip when your tool has %s dur rules.durabilityNotifier.alert=§cDurability remaining: %s rules.durabilitySwap=Swaps 0 durability items out of your hand. rules.echoShardsEnableShriekers=Using an echo shard on a sculk shrieker allows it to summon wardens. +rules.enderPearlChunkLoading=Allows ender pearls to tick the specified chunk radius (square) around them. rules.entityInstantDeath=Removes the 20gt death animation. Entities will also not drop xp. rules.entitySeparation=When stacked entities trigger a pressure plate, one entity will be unstacked in the direction a neighboring dropper is facing. rules.explosionChainReactionOnly=Makes explosions only affect TNT blocks. From dfb83481b5fd09a66293e07582c8ea38b43123fe Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 14:28:20 -0700 Subject: [PATCH 06/16] universalChunkLoading -> minecartChunkLoading Also features changing to an IntegerRule --- Canopy [BP]/entities/minecart.json | 63 +++++++++++++++++-- Canopy [BP]/scripts/main.js | 2 +- .../scripts/src/rules/minecartChunkLoading.js | 62 ++++++++++++++++++ .../src/rules/universalChunkLoading.js | 21 ------- Canopy [RP]/texts/en_US.lang | 4 +- 5 files changed, 122 insertions(+), 30 deletions(-) create mode 100644 Canopy [BP]/scripts/src/rules/minecartChunkLoading.js delete mode 100644 Canopy [BP]/scripts/src/rules/universalChunkLoading.js diff --git a/Canopy [BP]/entities/minecart.json b/Canopy [BP]/entities/minecart.json index cfa70f8..40619dc 100644 --- a/Canopy [BP]/entities/minecart.json +++ b/Canopy [BP]/entities/minecart.json @@ -13,15 +13,39 @@ "looping": false, "time": 10, "time_down_event": { - "event": "canopy:disable_ticking" + "event": "canopy:stop_ticking" } } }, - "canopy:ticking": { + "canopy:tick_world_r2": { "minecraft:tick_world": { "never_despawn": true, "radius": 2 } + }, + "canopy:tick_world_r3": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 3 + } + }, + "canopy:tick_world_r4": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 4 + } + }, + "canopy:tick_world_r5": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 5 + } + }, + "canopy:tick_world_r6": { + "minecraft:tick_world": { + "never_despawn": true, + "radius": 6 + } } }, @@ -73,14 +97,41 @@ } }, "events": { - "canopy:tick_tenSeconds": { + "canopy:tick_tenSeconds_r2": { + "add": { + "component_groups": [ "canopy:tick_world_r2", "canopy:ticking_timer"] + } + }, + "canopy:tick_tenSeconds_r3": { + "add": { + "component_groups": [ "canopy:tick_world_r3", "canopy:ticking_timer"] + } + }, + "canopy:tick_tenSeconds_r4": { + "add": { + "component_groups": [ "canopy:tick_world_r4", "canopy:ticking_timer"] + } + }, + "canopy:tick_tenSeconds_r5": { + "add": { + "component_groups": [ "canopy:tick_world_r5", "canopy:ticking_timer"] + } + }, + "canopy:tick_tenSeconds_r6": { "add": { - "component_groups": [ "canopy:ticking", "canopy:ticking_timer"] + "component_groups": [ "canopy:tick_world_r6", "canopy:ticking_timer"] } }, - "canopy:disable_ticking": { + "canopy:stop_ticking": { "remove": { - "component_groups": [ "canopy:ticking", "canopy:ticking_timer" ] + "component_groups": [ + "canopy:tick_world_r2", + "canopy:tick_world_r3", + "canopy:tick_world_r4", + "canopy:tick_world_r5", + "canopy:tick_world_r6", + "canopy:ticking_timer" + ] } } } diff --git a/Canopy [BP]/scripts/main.js b/Canopy [BP]/scripts/main.js index 6c38a6f..613930e 100644 --- a/Canopy [BP]/scripts/main.js +++ b/Canopy [BP]/scripts/main.js @@ -45,7 +45,7 @@ import './src/commands/scriptevents/generator' import './src/rules/infodisplay/InfoDisplay' import './src/rules/explosionNoBlockDamage' import './src/rules/autoItemPickup' -import './src/rules/universalChunkLoading' +import './src/rules/minecartChunkLoading' import './src/rules/creativeNoTileDrops' import './src/rules/flippinArrows' import './src/rules/tntPrimeMomentum' diff --git a/Canopy [BP]/scripts/src/rules/minecartChunkLoading.js b/Canopy [BP]/scripts/src/rules/minecartChunkLoading.js new file mode 100644 index 0000000..0cf6c3f --- /dev/null +++ b/Canopy [BP]/scripts/src/rules/minecartChunkLoading.js @@ -0,0 +1,62 @@ +import { getEntitiesByType } from "../../include/utils"; +import { GlobalRule, IntegerRule } from "../../lib/canopy/Canopy"; +import { world } from '@minecraft/server'; + +class MinecartChunkLoading extends IntegerRule { + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'minecartChunkLoading', + onModifyCallback: (newValue) => this.tryStartTicking(newValue), + defaultValue: -1, + valueRange: { range: { min: 2, max: 6 }, other: [-1] } + })); + this.onEntityAppearBound = this.onEntityAppear.bind(this); + } + + tryStartTicking(newValue) { + if (newValue === -1) { + this.unsubscribeFromEvent(); + this.disableAllTickingMinecarts(); + } else { + this.subscribeToEvent(); + } + } + + subscribeToEvent() { + world.afterEvents.entitySpawn.subscribe(this.onEntityAppearBound); + } + + unsubscribeFromEvent() { + world.afterEvents.entitySpawn.unsubscribe(this.onEntityAppearBound); + } + + onEntityAppear(event) { + const minecart = event.entity; + if (minecart?.typeId !== 'minecraft:minecart') + return; + this.startTicking(minecart); + } + + disableAllTickingMinecarts() { + getEntitiesByType('minecraft:minecart').forEach(minecart => { + this.safelyTriggerEvent(minecart, 'canopy:stop_ticking'); + }); + } + + startTicking(minecart) { + const chunkRadiusToTick = this.getNativeValue(); + const eventName = `canopy:tick_tenSeconds_r${chunkRadiusToTick}`; + this.safelyTriggerEvent(minecart, eventName); + } + + safelyTriggerEvent(minecart, eventName) { + try { + minecart?.triggerEvent(eventName); + } catch(error) { + if (error.message.includes(`${eventName} does not exist on minecraft:minecart`)) + throw new Error(`[Canopy] ${eventName} could not be triggered on minecraft:minecart. Are you using another pack that overrides minecarts?`); + } + } +} + +export const minecartChunkLoading = new MinecartChunkLoading(); diff --git a/Canopy [BP]/scripts/src/rules/universalChunkLoading.js b/Canopy [BP]/scripts/src/rules/universalChunkLoading.js deleted file mode 100644 index d63c4b8..0000000 --- a/Canopy [BP]/scripts/src/rules/universalChunkLoading.js +++ /dev/null @@ -1,21 +0,0 @@ -import { BooleanRule, Rules } from "../../lib/canopy/Canopy"; -import { world } from "@minecraft/server"; - -new BooleanRule({ - category: 'Rules', - identifier: 'universalChunkLoading', - description: { translate: 'rules.universalChunkLoading' } -}); - -const EVENT_IDENTIFIER = 'canopy:tick_tenSeconds'; - -world.afterEvents.entitySpawn.subscribe((event) => { - if (event.entity.typeId !== 'minecraft:minecart' || !Rules.getNativeValue('universalChunkLoading')) return; - try { - event.entity.triggerEvent(EVENT_IDENTIFIER); - } catch(error) { - if (error.includes(`${EVENT_IDENTIFIER} does not exist on minecraft:minecart`)) - throw new Error(`[Canopy] ${EVENT_IDENTIFIER} could not be triggered on minecraft:minecart. Are you using another pack that overrides minecarts?`); - throw error; - } -}); diff --git a/Canopy [RP]/texts/en_US.lang b/Canopy [RP]/texts/en_US.lang index e9fa083..a970e16 100644 --- a/Canopy [RP]/texts/en_US.lang +++ b/Canopy [RP]/texts/en_US.lang @@ -367,6 +367,7 @@ rules.carefulBreak=Automatically picks up items when breaking blocks and sneakin rules.cauldronConcreteConversion=Concrete powder items inside a cauldron filled with water will convert to concrete items. rules.chunkBorders=Enables chunk border visualization when an arrow is in the inventory slot 13 (top middle). rules.collisionBoxes=Enables entity collision box visualization when an arrow is in inventory slot 14 (next to the top middle). +rules.creativeHotbarSwitching=Allows for quick switching between multiple hotbars. Put an arrow in inventory slot 17 (top right), then sneak and scroll to switch. rules.creativeInstantTame=Instantly tames animals with their respective food in creative mode. rules.creativeNetherWaterPlacement=Allows placing water in the Nether in creative mode. rules.creativeNoTileDrops=Prevents items dropping from blocks broken in creative mode. @@ -383,9 +384,9 @@ rules.explosionChainReactionOnly=Makes explosions only affect TNT blocks. rules.explosionNoBlockDamage=Makes explosions not affect blocks. rules.explosionOff=Disables explosions entirely. rules.flippinArrows=Using an arrow on blocks will flip, rotate, or open them. Putting it in your offhand will flip blocks when placed. -rules.creativeHotbarSwitching=Allows for quick switching between multiple hotbars. Put an arrow in inventory slot 17 (top right), then sneak and scroll to switch. rules.instaminableDeepslate=Makes deepslate instaminable while using netherite, efficiency 5, and haste 2. rules.instaminableEndstone=Makes endstone instaminable while using netherite, efficiency 5, and haste 2. +rules.minecartChunkLoading=Allows minecarts to tick the specified chunk radius (square) around them for 10 seconds after they are spawned. rules.noWelcomeMessage=Disables the §lCanopy§r§8 welcome message. rules.pistonBedrockBreaking=Allows pistons to break bedrock when facing away from a bedrock block and expanding. rules.playerSit=Allows players to sit down after %s quick sneaks. @@ -400,7 +401,6 @@ rules.serverSideCollisionBoxes=Renders collision boxes according to the entity's rules.spawnEggSpawnWithMinecart=When using a spawn egg on a rail, the spawned entity will be placed in a minecart on the rail. rules.tntFuse=The TNT fuse time in ticks. rules.tntPrimeMomentum=Hardcodes the TNT prime momentum. -rules.universalChunkLoading=Makes minecarts tick a 5x5 chunk area around them for 10 seconds after they are spawned. rules.infoDisplay.biome=Shows the biome you are in. rules.infoDisplay.blockStates=Shows the states of the block you are targeting. From 85360dfc791fb20cf05ab1aa9fc5d1e0480feab9 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 14:28:39 -0700 Subject: [PATCH 07/16] fix error message when ender pearls are overriden --- Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js b/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js index 2534c4a..e785d9b 100644 --- a/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js +++ b/Canopy [BP]/scripts/src/rules/enderPearlChunkLoading.js @@ -62,7 +62,7 @@ class EnderPearlChunkLoading extends IntegerRule { try { pearl?.triggerEvent(eventName); } catch(error) { - if (error.includes(`${eventName} does not exist on minecraft:ender_pearl`)) + if (error.message.includes(`${eventName} does not exist on minecraft:ender_pearl`)) throw new Error(`[Canopy] ${eventName} could not be triggered on minecraft:ender_pearl. Are you using another pack that overrides ender pearls?`); } } From 6f5797e9daa97f7035d94a7bfca299d68b33827f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 14:32:50 -0700 Subject: [PATCH 08/16] Revert "fix debug boxes for MC 26.0" This reverts commit ac238fbebc0b0473006aef554f46a78d5b1f5bca. --- Canopy [BP]/scripts/src/classes/BiomeEdgeRenderer.js | 6 +++--- Canopy [BP]/scripts/src/classes/HSSFinder.js | 2 +- Canopy [BP]/scripts/src/classes/HSSRenderer.js | 4 ++-- Canopy [BP]/scripts/src/classes/debugdisplay/AttackBox.js | 5 +++-- .../scripts/src/classes/debugdisplay/CollisionBox.js | 2 +- Canopy [BP]/scripts/src/classes/debugdisplay/EyeLevel.js | 2 +- Canopy [BP]/scripts/src/classes/debugdisplay/HitBox.js | 2 +- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Canopy [BP]/scripts/src/classes/BiomeEdgeRenderer.js b/Canopy [BP]/scripts/src/classes/BiomeEdgeRenderer.js index f96ec80..fc056b4 100644 --- a/Canopy [BP]/scripts/src/classes/BiomeEdgeRenderer.js +++ b/Canopy [BP]/scripts/src/classes/BiomeEdgeRenderer.js @@ -138,7 +138,7 @@ export class BiomeEdgeRenderer { changeInFinalAxis[finalAxis] = quadHeight; const bound = new Vector(...changeInMiddleAxis).add(new Vector(...changeInFinalAxis)); - const worldLocation = Vector.from(this.blockVolume.getMin()).add(new Vector(...localLocation)); + const worldLocation = Vector.from(this.blockVolume.getMin()).add(bound.multiply(0.5)).add(new Vector(...localLocation)); worldLocation.dimension = this.dimension; const sidedBox = new DebugBox(worldLocation); sidedBox.bound = bound; @@ -168,7 +168,7 @@ export class BiomeEdgeRenderer { } renderAnalysisLocation(location) { - const dimensionLocation = Vector.from(location); + const dimensionLocation = Vector.from(location).add(new Vector(0.5, 0.5, 0.5)); dimensionLocation.dimension = this.dimension; const tempBox = new DebugBox(dimensionLocation); tempBox.color = { red: 1, green: 1, blue: 1 }; @@ -181,7 +181,7 @@ export class BiomeEdgeRenderer { drawAnalysisBoundingBox() { if (this.analysisBoundingBoxShape) this.analysisBoundingBoxShape.remove(); - const dimensionLocation = Vector.from(this.blockVolume.getMin()); + const dimensionLocation = Vector.from(this.blockVolume.getMin()).add(Vector.from(this.blockVolume.getSpan()).multiply(0.5)); dimensionLocation.dimension = this.dimension; const boundingBox = new DebugBox(dimensionLocation); boundingBox.bound = this.blockVolume.getSpan(); diff --git a/Canopy [BP]/scripts/src/classes/HSSFinder.js b/Canopy [BP]/scripts/src/classes/HSSFinder.js index bccd882..39a0746 100644 --- a/Canopy [BP]/scripts/src/classes/HSSFinder.js +++ b/Canopy [BP]/scripts/src/classes/HSSFinder.js @@ -95,7 +95,7 @@ export class HSSFinder { const top = this.findStructureTop(dimension, flooredLocation, "minecraft:fortress"); const height = top.y - bottom.y + 1; - const dimensionLocation = bottom; + const dimensionLocation = bottom.add(new Vector(0.5, height * 0.5, 0.5)); dimensionLocation.dimension = dimension; const box = new DebugBox(dimensionLocation); box.bound = new Vector(1, height, 1); diff --git a/Canopy [BP]/scripts/src/classes/HSSRenderer.js b/Canopy [BP]/scripts/src/classes/HSSRenderer.js index 2c3c9a9..02270a2 100644 --- a/Canopy [BP]/scripts/src/classes/HSSRenderer.js +++ b/Canopy [BP]/scripts/src/classes/HSSRenderer.js @@ -23,7 +23,7 @@ export class HSSRenderer { } renderBoundingBox() { - const dimensionLocation = this.structureBounds.getMin(); + const dimensionLocation = this.structureBounds.getCenterpoint(); dimensionLocation.dimension = this.dimension; const box = new DebugBox(dimensionLocation); box.bound = this.structureBounds.getSize(); @@ -37,7 +37,7 @@ export class HSSRenderer { } renderSingleHSS(location) { - const bottom = new Vector(location.x, this.structureBounds.getMin().y, location.z); + const bottom = new Vector(location.x + 0.5, this.structureBounds.getCenterpoint().y, location.z + 0.5); bottom.dimension = this.dimension; const box = new DebugBox(bottom); box.bound = new Vector(1, this.structureBounds.getSize().y, 1); diff --git a/Canopy [BP]/scripts/src/classes/debugdisplay/AttackBox.js b/Canopy [BP]/scripts/src/classes/debugdisplay/AttackBox.js index 4e18a64..1c8fb1a 100644 --- a/Canopy [BP]/scripts/src/classes/debugdisplay/AttackBox.js +++ b/Canopy [BP]/scripts/src/classes/debugdisplay/AttackBox.js @@ -29,13 +29,13 @@ export class AttackBox extends DebugDisplayShapeElement { if (isProjectile) { const marginFromCenter = this.getProjectileMargin(); return { - location: Vector.from(AABB.center).subtract(marginFromCenter), + location: new Vector(0, AABB.extent.y, 0), size: marginFromCenter.multiply(2) }; } const marginFromCollisionBox = new Vector(0.8, 0, 0.8); return { - location: new Vector(-AABB.extent.x, 0, -AABB.extent.z).subtract(marginFromCollisionBox), + location: new Vector(0, AABB.extent.y, 0), size: Vector.from(AABB.extent).add(marginFromCollisionBox).multiply(2) }; } @@ -58,3 +58,4 @@ export class AttackBox extends DebugDisplayShapeElement { return this.getAttackBox().location; } } + diff --git a/Canopy [BP]/scripts/src/classes/debugdisplay/CollisionBox.js b/Canopy [BP]/scripts/src/classes/debugdisplay/CollisionBox.js index cb945d4..18b923e 100644 --- a/Canopy [BP]/scripts/src/classes/debugdisplay/CollisionBox.js +++ b/Canopy [BP]/scripts/src/classes/debugdisplay/CollisionBox.js @@ -22,7 +22,7 @@ export class CollisionBox extends DebugDisplayShapeElement { getCollisionBox() { const AABB = this.entity.getAABB(); return { - location: new Vector(-AABB.extent.x, 0, -AABB.extent.z), + location: new Vector(0, AABB.extent.y, 0), size: Vector.from(AABB.extent).multiply(2) }; } diff --git a/Canopy [BP]/scripts/src/classes/debugdisplay/EyeLevel.js b/Canopy [BP]/scripts/src/classes/debugdisplay/EyeLevel.js index 100fc0a..28ab168 100644 --- a/Canopy [BP]/scripts/src/classes/debugdisplay/EyeLevel.js +++ b/Canopy [BP]/scripts/src/classes/debugdisplay/EyeLevel.js @@ -22,7 +22,7 @@ export class EyeLevel extends DebugDisplayShapeElement { getEyeLevelBoxBounds() { const AABB = this.entity.getAABB(); return { - location: new Vector(-AABB.extent.x, this.entity.getHeadLocation().y - this.entity.location.y, -AABB.extent.z), + location: new Vector(0, this.entity.getHeadLocation().y - this.entity.location.y, 0), size: new Vector(AABB.extent.x * 2, 0, AABB.extent.z * 2) } } diff --git a/Canopy [BP]/scripts/src/classes/debugdisplay/HitBox.js b/Canopy [BP]/scripts/src/classes/debugdisplay/HitBox.js index aa5c062..59dc960 100644 --- a/Canopy [BP]/scripts/src/classes/debugdisplay/HitBox.js +++ b/Canopy [BP]/scripts/src/classes/debugdisplay/HitBox.js @@ -24,7 +24,7 @@ export class HitBox extends DebugDisplayShapeElement { const AABB = this.entity.getAABB(); const marginFromCollisionBox = this.getMargin(); return { - location: new Vector(-AABB.extent.x, 0, -AABB.extent.z).subtract(marginFromCollisionBox), + location: new Vector(0, AABB.extent.y, 0), size: Vector.from(AABB.extent).add(marginFromCollisionBox).multiply(2) }; } From e027693b5bb10c479666128351431f19dd979788 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 15:01:03 -0700 Subject: [PATCH 09/16] remove regolith from watch task --- .vscode/launch.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8293d8e..a7f82d2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,8 +8,7 @@ "mode": "listen", "targetModuleUuid": "3d753132-e3c9-4305-a995-eae30b486093", "localRoot": "${workspaceFolder}/Canopy [BP]/scripts", - "port": 19144, - "preLaunchTask": "regolith: watch" + "port": 19144 } ] } \ No newline at end of file From d2c5d0e2558f859cc82b6528fb1e9c68156239f5 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 15:01:23 -0700 Subject: [PATCH 10/16] refillHand works for nonstackables --- Canopy [BP]/scripts/src/rules/refillHand.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Canopy [BP]/scripts/src/rules/refillHand.js b/Canopy [BP]/scripts/src/rules/refillHand.js index 789a952..d23c645 100644 --- a/Canopy [BP]/scripts/src/rules/refillHand.js +++ b/Canopy [BP]/scripts/src/rules/refillHand.js @@ -39,7 +39,7 @@ class RefillHand extends AbilityRule { let minSlotIndex = -1; for (let slotIndex = 0; slotIndex < playerInventory.size; slotIndex++) { const slot = playerInventory.getSlot(slotIndex); - if (slot.hasItem() && slot.isStackableWith(beforeItemStack)) { + if (slot.hasItem() && this.canFillInFor(slot, beforeItemStack)) { if (slot.amount < minAmount) { minAmount = slot.amount; minSlotIndex = slotIndex; @@ -53,6 +53,12 @@ class RefillHand extends AbilityRule { hasRunOutOfItems(beforeItemStack, afterItemStack) { return beforeItemStack?.typeId !== afterItemStack?.typeId; } + + canFillInFor(slot, beforeItemStack) { + if (beforeItemStack.maxAmount === 1) + return slot.typeId === beforeItemStack.typeId; + return slot.isStackableWith(beforeItemStack); + } } export const refillHand = new RefillHand(); \ No newline at end of file From f7a3865cb14a3b0bc28579d755d6b2f31c979642 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 15:28:37 -0700 Subject: [PATCH 11/16] fix nonexistant light_gray and light_blue counters & visual improvements to hopperCounterCounts --- Canopy [BP]/scripts/src/classes/ItemCounterChannels.js | 4 ++-- .../scripts/src/rules/infodisplay/HopperCounterCounts.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Canopy [BP]/scripts/src/classes/ItemCounterChannels.js b/Canopy [BP]/scripts/src/classes/ItemCounterChannels.js index 919309c..35b4c81 100644 --- a/Canopy [BP]/scripts/src/classes/ItemCounterChannels.js +++ b/Canopy [BP]/scripts/src/classes/ItemCounterChannels.js @@ -5,8 +5,8 @@ class ItemCounterChannels { onTickRunner; constructor(ChannelClass, controllingRuleID) { - ItemCounterChannels.colors = Object.freeze(['red', 'orange', 'yellow', 'lime', 'green', 'cyan', 'lightBlue', 'blue', 'purple', - 'pink', 'magenta', 'brown', 'black', 'white', 'lightGray', 'gray']); + ItemCounterChannels.colors = Object.freeze(['white', 'light_gray', 'gray', 'black', 'brown', 'red', 'orange', 'yellow', 'lime', 'green', 'cyan', + 'light_blue', 'blue', 'purple', 'magenta', 'pink']); ItemCounterChannels.modes = Object.freeze(['count', 'hr', 'min', 'sec']); this.colors = ItemCounterChannels.colors; this.modes = ItemCounterChannels.modes; diff --git a/Canopy [BP]/scripts/src/rules/infodisplay/HopperCounterCounts.js b/Canopy [BP]/scripts/src/rules/infodisplay/HopperCounterCounts.js index 0d28d20..d36ecbf 100644 --- a/Canopy [BP]/scripts/src/rules/infodisplay/HopperCounterCounts.js +++ b/Canopy [BP]/scripts/src/rules/infodisplay/HopperCounterCounts.js @@ -24,8 +24,7 @@ class HopperCounterCounts extends InfoDisplayElement { output += channel.getModedTotalCount(); output += '§r '; } - output += '\n'; - return { text: output.trim() }; + return { text: output }; } getFormattedDataSharedLine() { From 189946e77458c039dda739db81302a11d36cddd0 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 15:29:08 -0700 Subject: [PATCH 12/16] change purple and brown color codes pack-wide --- Canopy [BP]/scripts/include/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Canopy [BP]/scripts/include/utils.js b/Canopy [BP]/scripts/include/utils.js index de71747..41bdb51 100644 --- a/Canopy [BP]/scripts/include/utils.js +++ b/Canopy [BP]/scripts/include/utils.js @@ -56,10 +56,10 @@ export function getColorCode(color) { case 'cyan': return '§3'; case 'light_blue': return '§b'; case 'blue': return '§9'; - case 'purple': return '§5'; + case 'purple': return '§u'; case 'pink': return '§d'; - case 'magenta': return '§d'; - case 'brown': return '§6'; + case 'magenta': return '§5'; + case 'brown': return '§n'; case 'black': return '§0'; case 'white': return '§f'; case 'light_gray': return '§7'; From 7b41341264eadc537ba45a330268b5dc38fb9200 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 16:16:02 -0700 Subject: [PATCH 13/16] refactor utils tests --- Canopy [BP]/scripts/include/utils.js | 2 +- __tests__/BP/scripts/include/utils.test.js | 410 ++++++--------------- 2 files changed, 109 insertions(+), 303 deletions(-) diff --git a/Canopy [BP]/scripts/include/utils.js b/Canopy [BP]/scripts/include/utils.js index 41bdb51..0f685ad 100644 --- a/Canopy [BP]/scripts/include/utils.js +++ b/Canopy [BP]/scripts/include/utils.js @@ -34,7 +34,7 @@ export function getClosestTarget(player, blockRayResult, entityRayResult) { } export function parseName(target, includePrefix = true) { - if (target.typeId.replace('minecraft:', '') === 'player') + if (target.typeId === 'minecraft:player') return `§o${target.name}§r`; return includePrefix ? target.typeId : target.typeId.replace('minecraft:', ''); } diff --git a/__tests__/BP/scripts/include/utils.test.js b/__tests__/BP/scripts/include/utils.test.js index f624db0..3d9f5a9 100644 --- a/__tests__/BP/scripts/include/utils.test.js +++ b/__tests__/BP/scripts/include/utils.test.js @@ -16,14 +16,12 @@ vi.mock("@minecraft/server-ui", () => ({ })); describe('calcDistance()', () => { - it('returns correct distance (no Y)', () => { - const distance = calcDistance({ x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 4 }, false); - expect(distance).toBe(Math.sqrt(3*3 + 4*4)); - }); - - it('returns correct distance (with Y)', () => { - const distance = calcDistance({ x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 4 }, true); - expect(distance).toBe(Math.sqrt(3*3 + 4*4 + 4*4)); + it.each([ + [false, { x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 4 }, Math.sqrt(3*3 + 4*4)], + [true, { x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 4 }, Math.sqrt(3*3 + 4*4 + 4*4)] + ])('returns correct distance (including Y: %s)', (useY, from, to, expected) => { + const distance = calcDistance(from, to, useY); + expect(distance).toBe(expected); }); }); @@ -145,99 +143,41 @@ describe('parseName()', () => { }); describe('stringifyLocation()', () => { - it('returns correct string with default precision', () => { - const location = { x: 1.234, y: 5.678, z: 9.012 }; - expect(stringifyLocation(location)).toBe('[1, 6, 9]'); - }); - - it('returns correct string with specified precision', () => { - const location = { x: 1.234, y: 5.678, z: 9.012 }; - expect(stringifyLocation(location, 2)).toBe('[1.23, 5.68, 9.01]'); - }); - - it('returns correct string with zero precision', () => { - const location = { x: 1.234, y: 5.678, z: 9.012 }; - expect(stringifyLocation(location, 0)).toBe('[1, 6, 9]'); + it.each([ + [{ x: 1.234, y: 5.678, z: 9.012 }, '[1, 6, 9]'], + [{ x: 1.234, y: 5.678, z: 9.012 }, '[1.23, 5.68, 9.01]', 2], + [{ x: 1.234, y: 5.678, z: 9.012 }, '[1, 6, 9]', 0], + [{ x: 1.23456789, y: 5.67890123, z: 9.01234567 }, '[1.23456789, 5.67890123, 9.01234567]', 8] + ])('returns correct string, respecting precision', (location, expected, precision) => { + expect(stringifyLocation(location, precision)).toBe(expected); }); it('throws an error when precision is negative', () => { const location = { x: 1.234, y: 5.678, z: 9.012 }; expect(() => stringifyLocation(location, -1)).toThrow('Precision cannot be negative'); }); - - it('returns correct string with large precision', () => { - const location = { x: 1.23456789, y: 5.67890123, z: 9.01234567 }; - expect(stringifyLocation(location, 8)).toBe('[1.23456789, 5.67890123, 9.01234567]'); - }); }); describe('getColorCode()', () => { - it('returns correct color code for red', () => { - expect(getColorCode('red')).toBe('§c'); - }); - - it('returns correct color code for orange', () => { - expect(getColorCode('orange')).toBe('§6'); - }); - - it('returns correct color code for yellow', () => { - expect(getColorCode('yellow')).toBe('§e'); - }); - - it('returns correct color code for lime', () => { - expect(getColorCode('lime')).toBe('§a'); - }); - - it('returns correct color code for green', () => { - expect(getColorCode('green')).toBe('§2'); - }); - - it('returns correct color code for cyan', () => { - expect(getColorCode('cyan')).toBe('§3'); - }); - - it('returns correct color code for light_blue', () => { - expect(getColorCode('light_blue')).toBe('§b'); - }); - - it('returns correct color code for blue', () => { - expect(getColorCode('blue')).toBe('§9'); - }); - - it('returns correct color code for purple', () => { - expect(getColorCode('purple')).toBe('§5'); - }); - - it('returns correct color code for pink', () => { - expect(getColorCode('pink')).toBe('§d'); - }); - - it('returns correct color code for magenta', () => { - expect(getColorCode('magenta')).toBe('§d'); - }); - - it('returns correct color code for brown', () => { - expect(getColorCode('brown')).toBe('§6'); - }); - - it('returns correct color code for black', () => { - expect(getColorCode('black')).toBe('§0'); - }); - - it('returns correct color code for white', () => { - expect(getColorCode('white')).toBe('§f'); - }); - - it('returns correct color code for light_gray', () => { - expect(getColorCode('light_gray')).toBe('§7'); - }); - - it('returns correct color code for gray', () => { - expect(getColorCode('gray')).toBe('§8'); - }); - - it('returns empty string for unknown color', () => { - expect(getColorCode('unknown')).toBe(''); + it.each([ + ['red', '§c'], + ['orange', '§6'], + ['yellow', '§e'], + ['lime', '§a'], + ['green', '§2'], + ['cyan', '§3'], + ['light_blue', '§b'], + ['blue', '§9'], + ['purple', '§u'], + ['pink', '§d'], + ['magenta', '§5'], + ['brown', '§n'], + ['black', '§0'], + ['white', '§f'], + ['light_gray', '§7'], + ['gray', '§8'] + ])('returns correct color code for %s', (color, expectedCode) => { + expect(getColorCode(color)).toBe(expectedCode); }); }); @@ -328,147 +268,67 @@ describe.skip('broadcastActionBar()', () => { // Gametest }); -describe.skip('broadcastActionBar()', () => { - // Gametest -}); - describe('locationInArea()', () => { - it('returns true when position is within the area', () => { - const area = { - dimensionId: 'minecraft:overworld', - posOne: { x: 0, y: 0, z: 0 }, - posTwo: { x: 10, y: 10, z: 10 } - }; - const position = { - dimensionId: 'minecraft:overworld', - location: { x: 5, y: 5, z: 5 } - }; - expect(locationInArea(area, position)).toBe(true); - }); - - it('returns false when position is outside the area', () => { - const area = { - dimensionId: 'minecraft:overworld', - posOne: { x: 0, y: 0, z: 0 }, - posTwo: { x: 10, y: 10, z: 10 } - }; - const position = { - dimensionId: 'minecraft:overworld', - location: { x: 15, y: 15, z: 15 } - }; - expect(locationInArea(area, position)).toBe(false); - }); - - it('returns false when position is in a different dimension', () => { - const area = { - dimensionId: 'minecraft:overworld', - posOne: { x: 0, y: 0, z: 0 }, - posTwo: { x: 10, y: 10, z: 10 } - }; - const position = { - dimensionId: 'minecraft:nether', - location: { x: 5, y: 5, z: 5 } - }; - expect(locationInArea(area, position)).toBe(false); - }); - - it('returns true when position is on the boundary of the area', () => { - const area = { - dimensionId: 'minecraft:overworld', - posOne: { x: 0, y: 0, z: 0 }, - posTwo: { x: 10, y: 10, z: 10 } - }; - const position = { - dimensionId: 'minecraft:overworld', - location: { x: 10, y: 10, z: 10 } - }; - expect(locationInArea(area, position)).toBe(true); - }); - - it('returns false when area is undefined', () => { - const position = { - dimensionId: 'minecraft:overworld', - location: { x: 5, y: 5, z: 5 } - }; - expect(locationInArea(undefined, position)).toBe(false); - }); - - it('returns false when position is undefined', () => { - const area = { - dimensionId: 'minecraft:overworld', - posOne: { x: 0, y: 0, z: 0 }, - posTwo: { x: 10, y: 10, z: 10 } - }; - expect(locationInArea(area, undefined)).toBe(false); + it.each([ + [ + { dimensionId: 'minecraft:overworld', posOne: { x: 0, y: 0, z: 0 }, posTwo: { x: 10, y: 10, z: 10 } }, + { dimensionId: 'minecraft:overworld', location: { x: 5, y: 5, z: 5 } }, + true + ], + [ + { dimensionId: 'minecraft:overworld', posOne: { x: 0, y: 0, z: 0 }, posTwo: { x: 10, y: 10, z: 10 } }, + { dimensionId: 'minecraft:overworld', location: { x: 15, y: 15, z: 15 } }, + false + ], + [ + { dimensionId: 'minecraft:overworld', posOne: { x: 0, y: 0, z: 0 }, posTwo: { x: 10, y: 10, z: 10 } }, + { dimensionId: 'minecraft:nether', location: { x: 5, y: 5, z: 5 } }, + false + ], + [ + { dimensionId: 'minecraft:overworld', posOne: { x: 0, y: 0, z: 0 }, posTwo: { x: 10, y: 10, z: 10 } }, + { dimensionId: 'minecraft:overworld', location: { x: 10, y: 10, z: 10 } }, + true + ], + [ + undefined, + { dimensionId: 'minecraft:overworld', location: { x: 5, y: 5, z: 5 } }, + false + ], + [ + { dimensionId: 'minecraft:overworld', posOne: { x: 0, y: 0, z: 0 }, posTwo: { x: 10, y: 10, z: 10 } }, + undefined, + false + ] + ])('identifies when the position is inside the area', (area, position, expected) => { + expect(locationInArea(area, position)).toBe(expected); }); }); describe('getColoredDimensionName()', () => { - it('returns correct color code for overworld', () => { - expect(getColoredDimensionName('minecraft:overworld')).toBe('§aOverworld'); - expect(getColoredDimensionName('overworld')).toBe('§aOverworld'); - }); - - it('returns correct color code for nether', () => { - expect(getColoredDimensionName('minecraft:nether')).toBe('§cNether'); - expect(getColoredDimensionName('nether')).toBe('§cNether'); - }); - - it('returns correct color code for the end', () => { - expect(getColoredDimensionName('minecraft:the_end')).toBe('§dEnd'); - expect(getColoredDimensionName('the_end')).toBe('§dEnd'); - }); - - it('returns correct color code for unknown dimension', () => { - expect(getColoredDimensionName('unknown_dimension')).toBe('§funknown_dimension'); + it.each([ + [ 'minecraft:overworld', '§aOverworld' ], + [ 'overworld', '§aOverworld' ], + [ 'minecraft:nether', '§cNether' ], + [ 'nether', '§cNether' ], + [ 'minecraft:the_end', '§dEnd' ], + [ 'the_end', '§dEnd' ], + [ 'unknown_dimension', '§funknown_dimension' ] + ])('returns correct colored string for %s', (string, expected) => { + expect(getColoredDimensionName(string)).toBe(expected); }); }); describe('getScriptEventSourceName()', () => { - it('returns correct name for command block', () => { - const event = { - sourceType: 'Block', - sourceBlock: { typeId: 'minecraft:command_block' } - }; - expect(getScriptEventSourceName(event)).toBe('!'); - }); - - it('returns correct name for non-command block', () => { - const event = { - sourceType: 'Block', - sourceBlock: { typeId: 'minecraft:stone' } - }; - expect(getScriptEventSourceName(event)).toBe('minecraft:stone'); - }); - - it('returns correct name for player entity', () => { - const event = { - sourceType: 'Entity', - sourceEntity: { typeId: 'minecraft:player', name: 'Steve' } - }; - expect(getScriptEventSourceName(event)).toBe('Steve'); - }); - - it('returns correct name for non-player entity', () => { - const event = { - sourceType: 'Entity', - sourceEntity: { typeId: 'minecraft:cow' } - }; - expect(getScriptEventSourceName(event)).toBe('minecraft:cow'); - }); - - it('returns "Server" for server source type', () => { - const event = { - sourceType: 'Server' - }; - expect(getScriptEventSourceName(event)).toBe('Server'); - }); - - it('returns "Unknown" for unknown source type', () => { - const event = { - sourceType: 'Unknown' - }; - expect(getScriptEventSourceName(event)).toBe('Unknown'); + it.each([ + ['command block', { sourceType: 'Block', sourceBlock: { typeId: 'minecraft:command_block' } }, '!'], + ['non-command block', { sourceType: 'Block', sourceBlock: { typeId: 'minecraft:stone' } }, 'minecraft:stone'], + ['player entity', { sourceType: 'Entity', sourceEntity: { typeId: 'minecraft:player', name: 'Steve' } }, 'Steve'], + ['non-player entity', { sourceType: 'Entity', sourceEntity: { typeId: 'minecraft:cow' } }, 'minecraft:cow'], + ['server', { sourceType: 'Server' }, 'Server'], + ['unknown', { sourceType: 'Unknown' }, 'Unknown'] + ])('returns correct name for %s', (type, event, expected) => { + expect(getScriptEventSourceName(event)).toBe(expected); }); }); @@ -505,54 +365,19 @@ describe('getScriptEventSourceObject()', () => { }); describe('recolor()', () => { - it('recolors the term in the text with the specified color code', () => { - const result = recolor('Hello World', 'World', '§c'); - expect(result).toBe('Hello §cWorld§f'); - }); - - it('recolors the term in the text with the default color code', () => { - const result = recolor('Hello World', 'World'); - expect(result).toBe('Hello §fWorld§f'); - }); - - it('recolors multiple occurrences of the term in the text', () => { - const result = recolor('Hello World, World!', 'World', '§c'); - expect(result).toBe('Hello §cWorld§f, §cWorld§f!'); - }); - - it('returns the original text if the term is not found', () => { - const result = recolor('Hello World', 'Universe', '§c'); - expect(result).toBe('Hello World'); - }); - - it('is case insensitive when searching for the term', () => { - const result = recolor('Hello World', 'world', '§c'); - expect(result).toBe('Hello §cWorld§f'); - }); - - it('preserves existing color codes in the text', () => { - const result = recolor('Hello §aWorld', 'World', '§c'); - expect(result).toBe('Hello §a§cWorld§a'); - }); - - it('handles text with no color codes correctly', () => { - const result = recolor('Hello World', 'Hello', '§b'); - expect(result).toBe('§bHello§f World'); - }); - - it('handles empty text correctly', () => { - const result = recolor('', 'World', '§c'); - expect(result).toBe(''); - }); - - it('handles empty term correctly', () => { - const result = recolor('Hello World', '', '§c'); - expect(result).toBe('Hello World'); - }); - - it('handles empty color code correctly', () => { - const result = recolor('Hello World', 'World', ''); - expect(result).toBe('Hello World'); + it.each([ + ['Hello World', 'World', '§c', 'Hello §cWorld§f'], + ['Hello World', 'World', void 0, 'Hello §fWorld§f'], + ['Hello World, World!', 'World', '§c', 'Hello §cWorld§f, §cWorld§f!'], + ['Hello World', 'Universe', '§c', 'Hello World'], + ['Hello World', 'world', '§c', 'Hello §cWorld§f'], + ['Hello §aWorld', 'World', '§c', 'Hello §a§cWorld§a'], + ['Hello World', 'Hello', '§b', '§bHello§f World'], + ['', 'World', '§c', ''], + ['Hello World', '', '§c', 'Hello World'], + ['Hello World', 'World', '', 'Hello World'] + ])('recolors the term in the text with the specified color code', (string, term, colorCode, expected) => { + expect(recolor(string, term, colorCode)).toBe(expected); }); }); @@ -565,36 +390,17 @@ describe.skip('getRaycastResults()', () => { }); describe('titleCase()', () => { - it('converts camelCase to Title Case', () => { - expect(titleCase('camelCaseString')).toBe('Camel Case String'); - }); - - it('converts snake_case to Title Case', () => { - expect(titleCase('snake_case_string')).toBe('Snake Case String'); - }); - - it('converts mixedCase to Title Case', () => { - expect(titleCase('mixedCase_string')).toBe('Mixed Case String'); - }); - - it('converts single word to Title Case', () => { - expect(titleCase('word')).toBe('Word'); - }); - - it('converts empty string to empty string', () => { - expect(titleCase('')).toBe(''); - }); - - it('converts string with multiple underscores to Title Case', () => { - expect(titleCase('multiple__underscores')).toBe('Multiple Underscores'); - }); - - it('converts string with multiple camelCase words to Title Case', () => { - expect(titleCase('multipleCamelCaseWords')).toBe('Multiple Camel Case Words'); - }); - - it('converts string with numbers and special characters to Title Case', () => { - expect(titleCase('string_with_123_numbers_and_$pecial_characters')).toBe('String With 123 Numbers And $pecial Characters'); + it.each([ + ['camelCaseString', 'Camel Case String'], + ['snake_case_string', 'Snake Case String'], + ['mixedCase_string', 'Mixed Case String'], + ['word', 'Word'], + ['', ''], + ['multiple__underscores', 'Multiple Underscores'], + ['multipleCamelCaseWords', 'Multiple Camel Case Words'], + ['string_with_123_numbers_and_$pecial_characters', 'String With 123 Numbers And $pecial Characters'] + ])('converts %s to Title Case', (input, expected) => { + expect(titleCase(input)).toBe(expected); }); }); From c4bd6625d5b2020b5399617d3a07bff1eb6e9764 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Wed, 11 Mar 2026 16:20:09 -0700 Subject: [PATCH 14/16] fix tntFuse test to support entities loading in --- __tests__/BP/scripts/src/rules/tntFuse.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/__tests__/BP/scripts/src/rules/tntFuse.test.js b/__tests__/BP/scripts/src/rules/tntFuse.test.js index 331ff34..11b5ab1 100644 --- a/__tests__/BP/scripts/src/rules/tntFuse.test.js +++ b/__tests__/BP/scripts/src/rules/tntFuse.test.js @@ -44,6 +44,9 @@ vi.mock("@minecraft/server", () => ({ }, entitySpawn: { subscribe: vi.fn() + }, + entityLoad: { + subscribe: vi.fn() } }, setDynamicProperty: (identifier, ticks) => { tntFuseDP = ticks }, @@ -82,4 +85,11 @@ describe('tntFuseRule', () => { tntFuseDP = void 0; expect(tntFuseRule.getGlobalFuseTicks()).toBe(80); }); + + it('should restart the fuse when the entity loads', () => { + const startFuseMock = vi.spyOn(tntFuseRule, 'startFuse'); + tntFuseRule.setValue(15); + tntFuseRule.onEntityLoad({ entity: tntEntity }); + expect(startFuseMock).toHaveBeenCalledWith(tntEntity, 15); + }); }); \ No newline at end of file From aab219aa7a87ab4524fb0e477167bc753f8ea51c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 27 Mar 2026 12:53:55 -0700 Subject: [PATCH 15/16] bump pack version to 1.5.5 --- Canopy [BP]/manifest.json | 6 +++--- Canopy [BP]/scripts/constants.js | 4 ++-- Canopy [RP]/manifest.json | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Canopy [BP]/manifest.json b/Canopy [BP]/manifest.json index fa5d94f..92c2146 100644 --- a/Canopy [BP]/manifest.json +++ b/Canopy [BP]/manifest.json @@ -1,10 +1,10 @@ { "format_version": 2, "header": { - "name": "Canopy [BP] v1.5.4", + "name": "Canopy [BP] v1.5.5", "description": "Technical informatics & features addon by §aForestOfLight§r.", "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", - "version": [1, 5, 4], + "version": [1, 5, 5], "min_engine_version": [1, 26, 0] }, "modules": [ @@ -38,7 +38,7 @@ }, { "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", - "version": [1, 5, 4] + "version": [1, 5, 5] } ], "metadata": { diff --git a/Canopy [BP]/scripts/constants.js b/Canopy [BP]/scripts/constants.js index 6801352..5c87a25 100644 --- a/Canopy [BP]/scripts/constants.js +++ b/Canopy [BP]/scripts/constants.js @@ -1,4 +1,4 @@ -const PACK_VERSION = '1.5.4'; -const MC_VERSION = '1.26.0.2'; +const PACK_VERSION = '1.5.5'; +const MC_VERSION = '1.26.10.4'; export { PACK_VERSION, MC_VERSION }; \ No newline at end of file diff --git a/Canopy [RP]/manifest.json b/Canopy [RP]/manifest.json index 889ace6..10b81bb 100644 --- a/Canopy [RP]/manifest.json +++ b/Canopy [RP]/manifest.json @@ -1,11 +1,11 @@ { "format_version": 2, "header": { - "name": "Canopy [RP] v1.5.4", + "name": "Canopy [RP] v1.5.5", "description": "Technical informatics & features addon by §aForestOfLight§r.", "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", - "version": [1, 5, 4], - "min_engine_version": [1, 26, 0] + "version": [1, 5, 5], + "min_engine_version": [1, 26, 10] }, "modules": [ { @@ -17,7 +17,7 @@ "dependencies": [ { "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", - "version": [1, 5, 4] + "version": [1, 5, 5] } ], "capabilities": [ From 331ea35834d54f0208141793e50cd3f9d1992395 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Fri, 27 Mar 2026 12:57:19 -0700 Subject: [PATCH 16/16] fix non-matching minimum version number --- Canopy [BP]/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Canopy [BP]/manifest.json b/Canopy [BP]/manifest.json index 92c2146..41e17a0 100644 --- a/Canopy [BP]/manifest.json +++ b/Canopy [BP]/manifest.json @@ -5,7 +5,7 @@ "description": "Technical informatics & features addon by §aForestOfLight§r.", "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", "version": [1, 5, 5], - "min_engine_version": [1, 26, 0] + "min_engine_version": [1, 26, 10] }, "modules": [ {