From ad6a6fd8c4b0e4dbfbccf778355a5f56d50bce71 Mon Sep 17 00:00:00 2001 From: cayduck Date: Mon, 31 Aug 2026 09:35:30 +0900 Subject: [PATCH 01/26] Update ja_JP.lang --- Canopy[RP]/texts/ja_JP.lang | 149 ++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 16 deletions(-) diff --git a/Canopy[RP]/texts/ja_JP.lang b/Canopy[RP]/texts/ja_JP.lang index 78ac0a70..72ab8e80 100644 --- a/Canopy[RP]/texts/ja_JP.lang +++ b/Canopy[RP]/texts/ja_JP.lang @@ -20,13 +20,51 @@ commands.generic.invalidaction=§c無効なアクションです。詳細は /he commands.help=ヘルプページを表示します。 commands.help.search.noresult=§c「%s」に一致する内容は見つかりませんでした。 -commands.help.search.results=§l§aCanopy§r §2「§r%1§2」のヘルプ検索結果:%2 +commands.help.search.results=§l§aCanopy§r §2「§r%1§2」のヘルプ検索結果: commands.help.page.header=§l§aCanopy§r§2 ヘルプページ: §f%1 commands.help.infodisplay=設定可能なInfoDisplayのルール。 commands.help.rules=設定可能なグローバルルール。 commands.help.extension.rules=§o§a%s§r§8 用の切り替え可能なルール。 commands.help.extension.commands=§o§a%s§r§2 用のコマンド。 +commands.analyzearea=ブロックの範囲を分析し、JavaScript式の結果を表示します。 +commands.analyzearea.overcapacity=§c指定範囲のブロック数が多すぎます (>2^32 ブロック)。 +commands.analyzearea.loadcapacity=§c範囲が多くのチャンクにまたがっています (ティックエリアの上限を超えました)。 +commands.analyzearea.unknownerror=§c不明なエラーが発生しました。 +commands.analyzearea.syntaxerror=§c式を解析できませんでした。 +commands.analyzearea.forbidden=§c式が許可されていないプロパティまたはメソッドにアクセスしています。 +commands.analyzearea.completed=§7%1 個の一致するブロックが見つかりました。 +commands.analyzearea.removed=§7その座標の分析を削除しました。 +commands.analyzearea.removenotfound=§cその座標の分析が見つかりません。 +commands.analyzearea.create.invalid=§c有効な座標と式を入力してください。 +commands.analyzearea.teleport.gamemode=§c一致地点へのテレポートにはクリエイティブまたはスペクテイターモードが必要です。 +commands.analyzearea.stats.header=§l§aエリア分析§r +commands.analyzearea.stats=§f%1 §7-> §f%2\n§f式: §7%3\n§f一致: §a%4\n§fサイズ: §7%5 +commands.analyzearea.stats.capped=§f%1 §7-> §f%2\n§f式: §7%3\n§f一致: §c%4+ (上限に到達)\n§fサイズ: §7%5 +commands.analyzearea.stats.analyzing=§f%1 §7-> §f%2\n§f式: §7%3\n§f分析中... §e%4 +commands.analyzearea.ui.selector.title=§2エリア分析 +commands.analyzearea.ui.selector.new=(+) 新しい分析 +commands.analyzearea.ui.selector.empty=§7分析はまだありません。 +commands.analyzearea.ui.create.title=§2新しいエリア分析 +commands.analyzearea.ui.create.fromX=開始 X +commands.analyzearea.ui.create.fromY=開始 Y +commands.analyzearea.ui.create.fromZ=開始 Z +commands.analyzearea.ui.create.toX=終了 X +commands.analyzearea.ui.create.toY=終了 Y +commands.analyzearea.ui.create.toZ=終了 Z +commands.analyzearea.ui.create.dimension=ディメンション +commands.analyzearea.ui.create.expression=式 +commands.analyzearea.ui.create.submit=作成 +commands.analyzearea.ui.page.title=§2エリア分析 +commands.analyzearea.ui.page.notrun=§7このセッションでは未実行です。再分析を押してください。 +commands.analyzearea.ui.page.prev=< 前へ < +commands.analyzearea.ui.page.next=> 次へ > +commands.analyzearea.ui.page.reanalyze=再分析 +commands.analyzearea.ui.page.enableboxes=一致の描画を有効化 +commands.analyzearea.ui.page.disableboxes=一致の描画を無効化 +commands.analyzearea.ui.page.remove=(-) エリアを削除 +commands.analyzearea.ui.page.back=戻る + commands.biomeedges=指定範囲内のバイオーム境界を検出して表示します。 commands.biomeedges.finderadded=§7バイオーム境界を分析中... commands.biomeedges.finderremoved=§7最新のバイオーム境界範囲を削除しました。 @@ -58,7 +96,7 @@ commands.camera.spectate.hardcore=§cスペクテイターへの切り替えを commands.camera.spectate.flying=§cスペクテイターに切り替えるには地面にいる必要があります。 commands.camera.invalidaction=§c無効なカメラアクションです。 -commands.canopy=ルールを設定します。 +commands.canopy=ルールの値を設定します。数値は引用符で囲みます。例: "16"。 commands.canopy.version=Canopyと全ロード済み拡張機能の現在のバージョンを表示します。 commands.canopy.version.message=§7このサーバーは §l§aCanopy§r を実行しています commands.canopy.version.extensions=§7ロード済み拡張機能: @@ -88,6 +126,7 @@ commands.cleanup.success=§7%1 個のエンティティをクリーンアップ commands.counter=ホッパーカウンターを管理します。(エイリアス: ct) commands.counter.channel.notfound=§c無効な色: %s。羊毛ブロックの色のいずれかを使用してください。 commands.counter.query=指定した色のホッパーカウンターのカウントとレートを表示します。 +commands.counter.query.all=すべてのホッパーカウンターのカウントとレートを表示します。 commands.counter.query.empty=§7使用中のホッパーカウンターがありません。 commands.counter.query.channel=§7%1§7 のアイテム (%2%3 分), 合計: §f%4§7, (§f%5§7): commands.counter.realtime=ゲームティックに基づく時間ではなく、現実世界の時間を使用してカウントとレートを表示します。 @@ -116,7 +155,7 @@ commands.data.components=§aコンポーネント:§r %s commands.data.tags=§aタグ:§r %s commands.data.dynamicProperties=§aダイナミックプロパティ:§r %1 合計バイト数: %2 commands.data.effects=§aエフェクト:§r %s -commands.data.other=§aその他:§r 頭の位置: %1, 回転: [%2, %3], 速度: %4, 視線方向: %5 +commands.data.other=§aその他:§r 頭の位置: %1, 回転: [%2, %3], 速度: %4, 視線方向: %5, AABB: %6 commands.debugentity=エンティティのデバッグ情報を表示します。 commands.debugentity.invalidProperty=§c無効なデバッグプロパティです。 @@ -150,6 +189,7 @@ commands.gamemode.sp=ゲームモードをスペクテイターに設定しま commands.generator=ホッパージェネレーターを管理します。(エイリアス: gt) commands.generator.channel.notfound=§c無効な色: %s。羊毛ブロックの色のいずれかを使用してください。 +commands.generator.query.all=すべてのホッパージェネレーターのカウントとレートを表示します。 commands.generator.query=指定した色のホッパージェネレーターのカウントとレートを表示します。 commands.generator.query.empty=§7使用中のホッパージェネレーターはありません。 commands.generator.query.channel=§7%1§7 の生成アイテム (%2%3 分), 合計: §f%4§7, (§f%5§7): @@ -180,12 +220,9 @@ commands.hss.stopped=§7固定スポーン地点の表示を終了しました commands.hss.alreadyrunning=§c既にネザー要塞の固定スポーン地点を捜索中です。 commands.hss.notrunning=§c固定スポーン地点の捜索は現在実行中ではありません。 -commands.info=InfoDisplayルールを設定します。(エイリアス: i) +commands.info=InfoDisplayルールを設定します。 commands.info.menu=InfoDisplayルール設定用のメニューフォームを表示します。 commands.info.single=単一のInfoDisplayルールを設定します。 -commands.info.multiple=複数のInfoDisplayルールを設定します。 -commands.info.all=すべてのInfoDisplayルールを設定します。 -commands.info.allupdated=§7すべてのInfoDisplayルールが §l commands.info.canopyRule=§cルール '%1' はグローバルルールであり、切り替えるには %2canopy を使用する必要があります。詳細は %2help を参照してください。 commands.jump=ターゲット中のブロックにテレポートします。(エイリアス: j) @@ -230,12 +267,69 @@ commands.lifetime.query.ticktime.unit= gt commands.loop=バニラコマンドを1ティック内で複数回実行します。 commands.peek=ターゲットのインベントリの中身を確認します。クエリに一致するアイテムをハイライト表示できます。 -commands.peek.fail.unloaded=§c%s のターゲットは読み込まれていません。 -commands.peek.fail.noinventory=§c%2 の %1 にインベントリが見つかりません。 -commands.peek.fail.noitems=§c%2 の %1 にアイテムが見つかりません。 +commands.peek.armed=§7ピークを準備しました。コンテナを見ながら操作すると中身を確認できます。 +commands.peek.expired=§7ピークの有効期限が切れました。もう一度コマンドを実行してください。 +commands.peek.fail.notcreative=§cコンテナのピークにはクリエイティブモードが必要です。 +commands.peek.fail.notarget=§cピークするコンテナがありません。ピークをキャンセルしました。 +commands.peek.tip=§7操作: §f上半分§r\n§7スニーク+操作: §f下半分 +commands.peek.view.top=上半分 +commands.peek.view.bottom=下半分 commands.peek.query.cleared=§7ピーククエリをクリアしました。 commands.peek.query.set=§7ピーククエリを '%s' に設定しました。 +commands.playeraction=シミュレートプレイヤーに可変タイミングでアクションを実行させます。 +commands.playeraction.invalidtiming=§c無効な %1 タイミング: %2。 +commands.playeraction.invalidticks=§c無効な '%1' のティック時間: %2。整数を指定してください。 + +commands.playerglide=シミュレートプレイヤーのエリトラ滑空を開始・停止させます。 + +commands.playerinventory=シミュレートプレイヤーのインベントリを表示します。 +commands.playerinventory.noinventory=§cインベントリが見つかりません +commands.playerinventory.empty=§7%s のインベントリは空です。 +commands.playerinventory.header=%s のインベントリ: +commands.playerinventory.item=§7- %1%2§7: %3 x%4 + +commands.playerjoin=新しいシミュレートプレイヤーを現在地に参加させます。 + +commands.playerleave=シミュレートプレイヤーをゲームから退出させます。 + +commands.playerlook=シミュレートプレイヤーに指定した方向を向かせます。 +commands.playerlook.at.missing=§c注視する位置の座標が指定されていません。 +commands.playerlook.rotation.missing=§c向きのヨーまたはピッチが指定されていません。 +commands.playerlook.invalidoption=§c無効な注視オプション: '%s' +commands.playerlook.block.entityonly=§cブロックのターゲット指定はエンティティのみが使用できます。 +commands.playerlook.block.noblock=§c視界内にブロックがありません。 +commands.playerlook.entity.entityonly=§cエンティティのターゲット指定はエンティティのみが使用できます。 +commands.playerlook.entity.noentity=§c視界内にエンティティがありません。 +commands.playerlook.me.noserver=§c自分自身のターゲット指定はサーバーからは使用できません。 + +commands.playermove=シミュレートプレイヤーを指定した方向に移動させます。 +commands.playermove.invalidoption=§c無効な移動オプション: '%s' +commands.playermove.block.entityonly=§cブロックへの移動はエンティティのみが使用できます。 +commands.playermove.block.noblock=§c視界内にブロックがありません。 +commands.playermove.entity.entityonly=§cエンティティへの移動はエンティティのみが使用できます。 +commands.playermove.entity.noentity=§c視界内にエンティティがありません。 +commands.playermove.me.noserver=§c自分自身への移動はサーバーからは使用できません。 + +commands.playerprefix=シミュレートプレイヤーのネームタグの接頭辞を設定します。'-none' で解除します。 +commands.playerprefix.removed=§7シミュレートプレイヤーの接頭辞を解除しました。 +commands.playerprefix.set=§7シミュレートプレイヤーの接頭辞を "§r%s§r§7" に設定しました。 + +commands.playerrejoin=シミュレートプレイヤーを最後の位置で再参加させます。 + +commands.playerselect=シミュレートプレイヤーにホットバースロットを選択させます。 +commands.playerselect.invalidslot=§c無効なスロット番号: %s。0 から 8 の数値を指定してください。 + +commands.playersneak=シミュレートプレイヤーのスニークを開始・停止させます。 + +commands.playersprint=シミュレートプレイヤーのダッシュを開始・停止させます。 + +commands.playerstop=シミュレートプレイヤーのすべてのアクションを停止させます。 + +commands.playerswapheld=シミュレートプレイヤーの手持ちアイテムを自分の手持ちアイテムと交換します。 + +commands.playertp=シミュレートプレイヤーを自分の場所にテレポートさせます。 + commands.pos=現在の位置、または他のプレイヤーの位置を表示します。 commands.pos.self=§aあなたの位置: §f%s commands.pos.other=§a%1 の位置: §f%2 @@ -394,9 +488,12 @@ rules.quickFillContainer=アイテムを持ってコンテナを使うと、そ rules.quickFillContainer.filled=§7すべての %2 を %1 に収納しました rules.quickFillContainer.taken=§7%2 からすべての %1 を取り出しました rules.refillHand=持っているアイテムが無くなったときにインベントリから補充します。インベントリスロット10(左上の右隣)に矢を入れると有効になります。 +rules.renderEndGatewayExits=エンドゲートウェイを通過した後、その出口の位置を描画します。 rules.renewableElytraDropChance=ファントムがシュルカーの弾で倒されると、確率でエリトラをドロップします。 rules.renewableSponge=ガーディアンが雷に打たれるとエルダーガーディアンに変化します。 rules.serverSideCollisionBoxes=エンティティの当たり判定ボックスを、クライアント座標ではなくサーバー座標に基づいて描画します。 +rules.simplayerSaving=シミュレートプレイヤーのプレイヤーデータの保存を有効にします。パフォーマンスは低下しますが、退出して再参加してもインベントリと位置が保持されます。 +rules.simplayerRejoining=ワールドの再読み込み時に、オンラインのシミュレートプレイヤーを再参加させます。 rules.spawnEggSpawnWithMinecart=レールの上でスポーンエッグを使用すると、スポーンしたエンティティがトロッコに乗った状態で現れます。 rules.tntFuse=TNTの起爆時間(ティック単位)。 rules.tntPrimeMomentum=TNT着火時の運動量を固定値にします。 @@ -408,12 +505,16 @@ rules.infoDisplay.cardinalFacing.display=向き: %s rules.infoDisplay.chunkCoords=現在のチャンク座標とチャンク内の位置を表示します。 rules.infoDisplay.chunkCoords.display=チャンク: %1§r in %2 rules.infoDisplay.coords=座標を小数点以下2桁に丸めて表示します。 -rules.infodisplay.dimension=現在のディメンションを表示します。 +rules.infoDisplay.dimension=現在のディメンションを表示します。 rules.infoDisplay.entities=正面にいるエンティティ数を表示します。 rules.infoDisplay.entities.display=エンティティ: %s rules.infoDisplay.eventTrackers=トラッキング中のイベントのカウントを表示します。 -rules.infoDisplay.facing=yawとpitchで向いている方向を表示します。 +rules.infoDisplay.facing=ヨーとピッチで向いている方向を表示します。 rules.infoDisplay.facing.display=向き: %1 %2 +rules.infoDisplay.heldItemDurability=メインハンドに持っているアイテムの耐久値を表示します。 +rules.infoDisplay.heldItemDurability.display=耐久値 %s +rules.infoDisplay.blockBreakSpeed=直近 %1 ティック (%2 秒) における1秒あたりの平均ブロック破壊数を表示します。 +rules.infoDisplay.blockBreakSpeed.display=ブロック破壊速度: %1 b/s rules.infoDisplay.hopperCounterCounts=すべてのアクティブなホッパーカウンターのカウントをそれぞれの色で表示します。ホッパーカウンターモードを変更することで表示を調整できます。 rules.infoDisplay.light=足元のブロックの明るさレベルを表示します。 rules.infoDisplay.light.display=明るさ: %1 §r(空の明るさ %2§r) @@ -428,18 +529,25 @@ rules.infoDisplay.moonPhase.waningCrescent=二十六夜 rules.infoDisplay.moonPhase.waningGibbous=寝待月 rules.infoDisplay.moonPhase.waxingCrescent=三日月 rules.infoDisplay.moonPhase.waxingGibbous=十三夜 +rules.infoDisplay.noFog=霧を無効にします。水と溶岩には影響しません。 rules.infoDisplay.peekInventory=ターゲット中のブロックまたはエンティティのインベントリを表示します。 rules.infoDisplay.peekInventory.empty=空 +rules.infoDisplay.ping=サーバーへの現在のネットワーク遅延を表示します。 +rules.infoDisplay.ping.display=Ping: %s +rules.infoDisplay.renderLightLevel=近くのブロックの上に明るさレベルの値を描画します。警告: 非常に負荷の大きいルールです。 +rules.infoDisplay.renderSignalStrength=近くのレッドストーンダストの上に信号強度の値を描画します。 rules.infoDisplay.sessionTime=ワールドに参加してからの時間を表示します。 rules.infoDisplay.sessionTime.display=セッション: %s rules.infoDisplay.signalStrength=ターゲット中のブロックの信号強度を表示します。 rules.infoDisplay.simulationMap=周囲の読み込み済みチャンクのマップを表示します。simmapコマンドで設定できます。警告: 非常に負荷の大きいルールです。 +rules.infoDisplay.horse=騎乗している馬の速度と最大体力を表示します。 +rules.infoDisplay.horse.display=馬の速度: §a%1§r, 最大体力: §c%2§r rules.infoDisplay.slimeChunk=現在のチャンクがスライムチャンクかどうかを表示します。 rules.infoDisplay.slimeChunk.display=スライムチャンク: %s rules.infoDisplay.speed=現在の速度をメートル毎秒[m/s]で表示します。 -rules.infodisplay.structures=現在地の自然生成構造物を表示します。 -rules.infodisplay.structures.display=構造物: -rules.infodisplay.structures.display.none=なし +rules.infoDisplay.structures=現在地の自然生成構造物を表示します。 +rules.infoDisplay.structures.display=構造物: +rules.infoDisplay.structures.display.none=なし rules.infoDisplay.target=ターゲット中のブロックまたはエンティティのIDを表示します。 rules.infoDisplay.timeOfDay=Minecraftの時刻を12時間制のデジタル時計形式で表示します。 rules.infoDisplay.tps=サーバーの1秒あたりの処理ティック数(TPS: Ticks Per Second)を表示します。 @@ -449,4 +557,13 @@ rules.infoDisplay.velocity=現在のx, y, z速度をメートル毎チック[m/t rules.infoDisplay.weather=現在のディメンションの天気を表示します。 rules.infoDisplay.weather.display=天気: %s rules.infoDisplay.worldDay=ワールド開始からのMinecraft日数を表示します。 -rules.infoDisplay.worldDay.display=日数: %s \ No newline at end of file +rules.infoDisplay.worldDay.display=日数: %s + +## simplayer +simplayer.notonline=§cシミュレートプレイヤー '%s' はオンラインではありません。 +simplayer.alreadyonline=§cシミュレートプレイヤー '%s' は既にオンラインです。 +simplayer.leave.broadcast=§e%s がゲームから退出しました +simplayer.swapheld.error=§cアイテムの交換中にエラーが発生しました: %s +simplayer.editor.tip=§7操作: §fホットバーと防具§r\n§7スニーク+操作: §fインベントリ +simplayer.editor.view.hotbarAndArmor=ホットバーと防具 +simplayer.editor.view.inventory=インベントリ \ No newline at end of file From 519aa6f5eadb152ebd71e45ddf012f2dc5d6889c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 6 Sep 2026 20:53:06 -0700 Subject: [PATCH 02/26] refactor: autoItemPickup & carefulBreak also sets the pickup distance to 4 (from 2) --- Canopy[BP]/scripts/include/utils.js | 26 +-- Canopy[BP]/scripts/main.js | 1 + .../scripts/src/classes/BlockRotator.js | 7 +- .../scripts/src/classes/InventoryUtils.js | 99 ++++++++++++ .../scripts/src/rules/autoItemPickup.js | 150 +++++++----------- Canopy[BP]/scripts/src/rules/carefulBreak.js | 76 +++++++++ __tests__/BP/scripts/include/utils.test.js | 68 -------- .../src/classes/InventoryUtils.test.js | 66 ++++++++ 8 files changed, 305 insertions(+), 188 deletions(-) create mode 100644 Canopy[BP]/scripts/src/classes/InventoryUtils.js create mode 100644 Canopy[BP]/scripts/src/rules/carefulBreak.js create mode 100644 __tests__/BP/scripts/src/classes/InventoryUtils.test.js diff --git a/Canopy[BP]/scripts/include/utils.js b/Canopy[BP]/scripts/include/utils.js index d498d070..20b12285 100644 --- a/Canopy[BP]/scripts/include/utils.js +++ b/Canopy[BP]/scripts/include/utils.js @@ -1,4 +1,4 @@ -import { world, system, ItemStack, DimensionTypes } from '@minecraft/server'; +import { world, system, DimensionTypes } from '@minecraft/server'; import { FormCancelationReason, uiManager } from '@minecraft/server-ui'; import { ProxyInventoryEntity } from '../src/classes/proxy/ProxyInventoryEntity'; import { PeekCaptureEntity } from '../src/classes/peek/PeekCaptureEntity'; @@ -79,30 +79,6 @@ export function wait(ms) { return { startTime, endTime }; } -export function getInventory(block) { - const container = block.getComponent('inventory')?.container; - if (container === undefined) return {}; - const items = {}; - for (let i = 0; i < container.size; i++) { - const itemStack = container.getItem(i); - if (itemStack === undefined) continue; - items[i] = { typeId: itemStack.type.id, amount: itemStack.amount }; - } - return items; -} - -export function restoreInventory(block, items) { - const container = block.getComponent('inventory')?.container; - if (container === undefined) - return; - for (let i = 0; i < container.size; i++) { - const item = items[i]; - if (item === undefined) - continue; - container.getSlot(i).setItem(new ItemStack(item.typeId, item.amount)); - } -} - export function broadcastActionBar(message, sender) { let players; if (sender) diff --git a/Canopy[BP]/scripts/main.js b/Canopy[BP]/scripts/main.js index 2f844b1d..c3d530ec 100644 --- a/Canopy[BP]/scripts/main.js +++ b/Canopy[BP]/scripts/main.js @@ -98,6 +98,7 @@ import './src/rules/serverSideCollisionBoxes' import './src/rules/entitySeparation' import './src/rules/enderPearlChunkLoading' import './src/rules/renderEndGatewayExits' +import './src/rules/carefulBreak' // Simulated Player Rules import './src/rules/simplayer/simplayerSaving' diff --git a/Canopy[BP]/scripts/src/classes/BlockRotator.js b/Canopy[BP]/scripts/src/classes/BlockRotator.js index 9c70276b..b587141f 100644 --- a/Canopy[BP]/scripts/src/classes/BlockRotator.js +++ b/Canopy[BP]/scripts/src/classes/BlockRotator.js @@ -1,6 +1,6 @@ import { world, StructureSaveMode, StructureRotation, BlockPermutation, StructureMirrorAxis } from "@minecraft/server"; import DirectionStateFinder from "../classes/DirectionState"; -import { getInventory, restoreInventory } from "../../include/utils"; +import { InventoryUtils } from "./InventoryUtils"; class BlockRotator { static idPrefix = 'canopy:rotator-'; @@ -48,7 +48,7 @@ class BlockRotator { if (!this.isValidId(structureId)) return console.warn('[BlockRotator] Invalid structure ID.'); const mirroredDirection = DirectionStateFinder.getMirroredDirection(block); let axis; - const items = getInventory(block); + const items = InventoryUtils.getInventory(block); if ([StructureMirrorAxis.X, StructureMirrorAxis.Z].includes(mirroredDirection)) { axis = mirroredDirection; } else { // block data has to be rebuilt manually 🎉 @@ -66,7 +66,8 @@ class BlockRotator { mirror: axis }; this.place(structureId, block, structurePlaceOptions); - if (Object.keys(items).length > 0) restoreInventory(block, items); + if (Object.keys(items).length > 0) + InventoryUtils.restoreInventory(block, items); } static place(structureId, block, structurePlaceOptions) { diff --git a/Canopy[BP]/scripts/src/classes/InventoryUtils.js b/Canopy[BP]/scripts/src/classes/InventoryUtils.js new file mode 100644 index 00000000..9231cc69 --- /dev/null +++ b/Canopy[BP]/scripts/src/classes/InventoryUtils.js @@ -0,0 +1,99 @@ +import { EntityComponentTypes, ItemStack } from "@minecraft/server"; + +export class InventoryUtils { + static getInventory(block) { + const container = block.getComponent('inventory')?.container; + if (container === undefined) return {}; + const items = {}; + for (let i = 0; i < container.size; i++) { + const itemStack = container.getItem(i); + if (itemStack === undefined) continue; + items[i] = { typeId: itemStack.type.id, amount: itemStack.amount }; + } + return items; + } + + static restoreInventory(block, items) { + const container = block.getComponent('inventory')?.container; + if (container === undefined) + return; + for (let i = 0; i < container.size; i++) { + const item = items[i]; + if (item === undefined) + continue; + container.getSlot(i).setItem(new ItemStack(item.typeId, item.amount)); + } + } + + static pickupItemEntity(entityWithInventory, itemEntity) { + const itemComponent = itemEntity.getComponent(EntityComponentTypes.Item); + const itemStack = itemComponent.itemStack; + const playerInventoryComponent = entityWithInventory?.getComponent(EntityComponentTypes.Inventory); + const inventory = playerInventoryComponent.container; + if (!itemStack || !inventory) + return; + const canAdd = InventoryUtils.tryAddItemLikeVanilla(inventory, itemStack); + if (canAdd) + itemEntity.remove(); + } + + static tryAddItemLikeVanilla(inventory, itemStack) { + if (InventoryUtils.canAddItem(inventory, itemStack)) { + InventoryUtils.#addItemLikeVanilla(inventory, itemStack); + return true; + } + return false; + } + + static canAddItem(inventory, itemStack) { + if (inventory.emptySlotsCount !== 0) + return true; + for (let i = 0; i < inventory.size; i++) { + const slot = inventory.getSlot(i); + if (InventoryUtils.#itemFitsInPartiallyFilledSlot(slot, itemStack)) + return true; + } + return false; + } + + static #itemFitsInPartiallyFilledSlot(slot, itemStack) { + return slot.hasItem() && slot.isStackableWith(itemStack) && slot.amount + itemStack.amount <= slot.maxAmount; + } + + static #addItemLikeVanilla(inventory, itemStack) { + const isItemDeposited = InventoryUtils.#partiallyFilledSlotPass(inventory, itemStack); + if (!isItemDeposited) + InventoryUtils.#emptySlotPass(inventory, itemStack); + } + + static #partiallyFilledSlotPass(inventory, itemStack) { + for (let slotNum = 0; slotNum < inventory.size; slotNum++) { + const slot = inventory.getSlot(slotNum); + if (InventoryUtils.#isSlotAvailableForStacking(slot, itemStack)) { + const remainderAmount = Math.max(0, (slot.amount + itemStack.amount) - slot.maxAmount); + slot.amount += itemStack.amount - remainderAmount; + if (remainderAmount > 0) { + const remainderStack = new ItemStack(itemStack.typeId, remainderAmount); + InventoryUtils.#addItemLikeVanilla(inventory, remainderStack); + } + return true; + } + } + return false; + } + + static #emptySlotPass(inventory, itemStack) { + for (let slotNum = 0; slotNum < inventory.size; slotNum++) { + const slot = inventory.getSlot(slotNum); + if (!slot.hasItem()) { + slot.setItem(itemStack); + return true; + } + } + return false; + } + + static #isSlotAvailableForStacking(slot, itemStack) { + return slot.hasItem() && slot.isStackableWith(itemStack) && slot.amount !== slot.maxAmount; + } +} \ No newline at end of file diff --git a/Canopy[BP]/scripts/src/rules/autoItemPickup.js b/Canopy[BP]/scripts/src/rules/autoItemPickup.js index 484bfb9e..12b33ec9 100644 --- a/Canopy[BP]/scripts/src/rules/autoItemPickup.js +++ b/Canopy[BP]/scripts/src/rules/autoItemPickup.js @@ -1,110 +1,76 @@ -import { BooleanRule, Rules } from "../../lib/canopy/Canopy"; -import { ItemStack, system, world, GameMode } from "@minecraft/server"; +import { BooleanRule, GlobalRule } from "../../lib/canopy/Canopy"; +import { system, world, GameMode } from "@minecraft/server"; import { calcDistance } from "../../include/utils"; +import { InventoryUtils } from "../classes/InventoryUtils"; -new BooleanRule({ - category: 'Rules', - identifier: 'autoItemPickup', - description: { translate: 'rules.autoItemPickup' }, - wikiDescription: 'Enables/disables the automatic pickup of items that drop when you break a block.' -}); - -new BooleanRule({ - category: 'Rules', - identifier: 'carefulBreak', - description: { translate: 'rules.carefulBreak' } -}) - -let brokenBlockEventsThisTick = []; - -system.runInterval(() => { +export class AutoItemPickup extends BooleanRule { + static PICKUP_RANGE = 4; brokenBlockEventsThisTick = []; -}); - -world.afterEvents.playerBreakBlock.subscribe((blockEvent) => { - if (!shouldPickup(blockEvent.player)) return; - if (blockEvent.player?.getGameMode() === GameMode.Creative) return; - brokenBlockEventsThisTick.push(blockEvent); -}); - -function shouldPickup(player) { - return (Rules.getNativeValue('carefulBreak') && player?.isSneaking) - || Rules.getNativeValue('autoItemPickup'); -} - -world.afterEvents.entitySpawn.subscribe((entityEvent) => { - if (entityEvent.cause !== 'Spawned' || entityEvent.entity?.typeId !== 'minecraft:item') return; - if (!Rules.getNativeValue('carefulBreak') && !Rules.getNativeValue('autoItemPickup')) return; + #runner = void 0; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'autoItemPickup', + wikiDescription: 'Enables the automatic pickup of items that drop when you break a block.', + onEnableCallback: () => this.subscribeToEvents(), + onDisableCallback: () => this.unsubscribeFromEvents(), + independentRules: ['carefulBreak'] + })); + this.onPlayerBreakBlockBound = this.onPlayerBreakBlock.bind(this); + this.onEntitySpawnBound = this.onEntitySpawn.bind(this); + } - const item = entityEvent.entity; - let brokenBlockEvent; - try { - brokenBlockEvent = brokenBlockEventsThisTick.find(blockEvent => calcDistance(blockEvent.block.location, item.location) < 2); - } catch { - // Could not access block or item, ignore + subscribeToEvents() { + this.#runner = system.runInterval(() => this.onTick()); + world.afterEvents.playerBreakBlock.subscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.subscribe(this.onEntitySpawnBound); } - if (!brokenBlockEvent) - return; - const itemStack = item.getComponent('minecraft:item').itemStack; - const inventory = brokenBlockEvent.player?.getComponent('minecraft:inventory').container; - if (!itemStack || !inventory) - return; - if (canAddItem(inventory, itemStack)) { - addItem(inventory, itemStack); - item.remove(); + unsubscribeFromEvents() { + if (this.#runner !== void 0) + system.clearRun(this.#runner); + world.afterEvents.playerBreakBlock.unsubscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.unsubscribe(this.onEntitySpawnBound); } -}); -function canAddItem(inventory, itemStack) { - if (inventory.emptySlotsCount !== 0) return true; - for (let i = 0; i < inventory.size; i++) { - const slot = inventory.getSlot(i); - if (itemFitsInPartiallyFilledSlot(slot, itemStack)) return true; + onTick() { + this.brokenBlockEventsThisTick = []; } - return false; -} -function itemFitsInPartiallyFilledSlot(slot, itemStack) { - return slot.hasItem() && slot.isStackableWith(itemStack) && slot.amount + itemStack.amount <= slot.maxAmount; -} + onPlayerBreakBlock(event) { + if (!this.shouldPickup(event.player)) + return; + this.brokenBlockEventsThisTick.push(event); + } -function addItem(inventory, itemStack) { - const isItemDeposited = partiallyFilledSlotPass(inventory, itemStack); - if (!isItemDeposited) - emptySlotPass(inventory, itemStack); + shouldPickup(player) { + return player?.getGameMode() === GameMode.Survival; + } -} - -function partiallyFilledSlotPass(inventory, itemStack) { - for (let slotNum = 0; slotNum < inventory.size; slotNum++) { - const slot = inventory.getSlot(slotNum); - if (isSlotAvailableForStacking(slot, itemStack)) { - const remainderAmount = Math.max(0, (slot.amount + itemStack.amount) - slot.maxAmount); - slot.amount += itemStack.amount - remainderAmount; - if (remainderAmount > 0) { - const remainderStack = new ItemStack(itemStack.typeId, remainderAmount); - addItem(inventory, remainderStack); - } - return true; - } + onEntitySpawn(event) { + if (event.cause !== 'Spawned' || event.entity?.typeId !== 'minecraft:item') + return; + + const itemEntity = event.entity; + const brokenBlockEvent = this.findBrokenBlockEventWhichDropped(itemEntity); + if (!brokenBlockEvent) + return; + InventoryUtils.pickupItemEntity(brokenBlockEvent.player, itemEntity); } - return false; -} -function emptySlotPass(inventory, itemStack) { - for (let slotNum = 0; slotNum < inventory.size; slotNum++) { - const slot = inventory.getSlot(slotNum); - if (!slot.hasItem()) { - slot.setItem(itemStack); - return true; + findBrokenBlockEventWhichDropped(itemEntity) { + let brokenBlockEvent; + try { + brokenBlockEvent = this.brokenBlockEventsThisTick.find(blockEvent => this.entityCameFromBlock(blockEvent, itemEntity)); + } catch { + /* pass */ } + return brokenBlockEvent; + } + + entityCameFromBlock(blockEvent, itemEntity) { + return calcDistance(blockEvent.block.location, itemEntity.location) < this.PICKUP_RANGE; } - return false; -} - -function isSlotAvailableForStacking(slot, itemStack) { - return slot.hasItem() && slot.isStackableWith(itemStack) && slot.amount !== slot.maxAmount; } -export default { brokenBlockEventsThisTick } +export const autoItemPickup = new AutoItemPickup(); diff --git a/Canopy[BP]/scripts/src/rules/carefulBreak.js b/Canopy[BP]/scripts/src/rules/carefulBreak.js new file mode 100644 index 00000000..b9de5b80 --- /dev/null +++ b/Canopy[BP]/scripts/src/rules/carefulBreak.js @@ -0,0 +1,76 @@ +import { BooleanRule, GlobalRule } from "../../lib/canopy/Canopy"; +import { system, world, GameMode } from "@minecraft/server"; +import { calcDistance } from "../../include/utils"; +import { InventoryUtils } from "../classes/InventoryUtils"; +import { AutoItemPickup } from "./autoItemPickup"; + +export class CarefulBreak extends BooleanRule { + static PICKUP_RANGE = AutoItemPickup.PICKUP_RANGE; + brokenBlockEventsThisTick = []; + #runner = void 0; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'carefulBreak', + onEnableCallback: () => this.subscribeToEvents(), + onDisableCallback: () => this.unsubscribeFromEvents(), + independentRules: ['autoItemPickup'] + })); + this.onPlayerBreakBlockBound = this.onPlayerBreakBlock.bind(this); + this.onEntitySpawnBound = this.onEntitySpawn.bind(this); + } + + subscribeToEvents() { + this.#runner = system.runInterval(() => this.onTick()); + world.afterEvents.playerBreakBlock.subscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.subscribe(this.onEntitySpawnBound); + } + + unsubscribeFromEvents() { + if (this.#runner !== void 0) + system.clearRun(this.#runner); + world.afterEvents.playerBreakBlock.unsubscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.unsubscribe(this.onEntitySpawnBound); + } + + onTick() { + this.brokenBlockEventsThisTick = []; + } + + onPlayerBreakBlock(event) { + if (!this.shouldPickup(event.player)) + return; + this.brokenBlockEventsThisTick.push(event); + } + + shouldPickup(player) { + return player?.isSneaking && player?.getGameMode() === GameMode.Survival; + } + + onEntitySpawn(event) { + if (event.cause !== 'Spawned' || event.entity?.typeId !== 'minecraft:item') + return; + + const itemEntity = event.entity; + const brokenBlockEvent = this.findBrokenBlockEventWhichDropped(itemEntity); + if (!brokenBlockEvent) + return; + InventoryUtils.pickupItemEntity(brokenBlockEvent.player, itemEntity); + } + + findBrokenBlockEventWhichDropped(itemEntity) { + let brokenBlockEvent; + try { + brokenBlockEvent = this.brokenBlockEventsThisTick.find(blockEvent => this.entityCameFromBlock(blockEvent, itemEntity)); + } catch { + /* pass */ + } + return brokenBlockEvent; + } + + entityCameFromBlock(blockEvent, itemEntity) { + return calcDistance(blockEvent.block.location, itemEntity.location) < CarefulBreak.PICKUP_RANGE; + } +} + +export const carefulBreak = new CarefulBreak(); diff --git a/__tests__/BP/scripts/include/utils.test.js b/__tests__/BP/scripts/include/utils.test.js index 28df0c73..dad014fe 100644 --- a/__tests__/BP/scripts/include/utils.test.js +++ b/__tests__/BP/scripts/include/utils.test.js @@ -187,74 +187,6 @@ describe('wait()', () => { }); }); -describe('getInventory()', () => { - it('returns correct inventory items', () => { - const block = { - getComponent: vi.fn().mockReturnValue({ - container: { - size: 3, - getItem: vi.fn((i) => { - if (i === 0) return { type: { id: 'minecraft:stone' }, amount: 10 }; - if (i === 1) return { type: { id: 'minecraft:dirt' }, amount: 5 }; - if (i === 2) return { type: { id: 'minecraft:stone' }, amount: 15 }; - }) - } - }) - }; - const items = getInventory(block); - expect(items).toEqual({ - 0: { typeId: 'minecraft:stone', amount: 10 }, - 1: { typeId: 'minecraft:dirt', amount: 5 }, - 2: { typeId: 'minecraft:stone', amount: 15 } - }); - }); - - it('returns empty object for block without inventory', () => { - const block = { - getComponent: vi.fn().mockReturnValue(undefined) - }; - const items = getInventory(block); - expect(items).toEqual({}); - }); - - it('returns empty object for empty inventory', () => { - const block = { - getComponent: vi.fn().mockReturnValue({ - container: { - size: 3, - getItem: vi.fn(() => undefined) - } - }) - }; - const items = getInventory(block); - expect(items).toEqual({}); - }); - - it('handles inventory with undefined (air) slots', () => { - const block = { - getComponent: vi.fn().mockReturnValue({ - container: { - size: 3, - getItem: vi.fn((i) => { - if (i === 0) return { type: { id: 'minecraft:stone' }, amount: 10 }; - if (i === 1) return undefined; - if (i === 2) return { type: { id: 'minecraft:stone' }, amount: 15 }; - }) - } - }) - }; - const items = getInventory(block); - expect(items).toEqual({ - 0: { typeId: 'minecraft:stone', amount: 10 }, - 2: { typeId: 'minecraft:stone', amount: 15 } - }); - }); -}); - -describe.skip('restoreInventory()', () => { - // Gametest -}); - describe.skip('broadcastActionBar()', () => { // Gametest }); diff --git a/__tests__/BP/scripts/src/classes/InventoryUtils.test.js b/__tests__/BP/scripts/src/classes/InventoryUtils.test.js new file mode 100644 index 00000000..3b61ea54 --- /dev/null +++ b/__tests__/BP/scripts/src/classes/InventoryUtils.test.js @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; +import { InventoryUtils } from "../../../../../Canopy[BP]/scripts/src/classes/InventoryUtils"; + +describe('getInventory()', () => { + it('returns correct inventory items', () => { + const block = { + getComponent: vi.fn().mockReturnValue({ + container: { + size: 3, + getItem: vi.fn((i) => { + if (i === 0) return { type: { id: 'minecraft:stone' }, amount: 10 }; + if (i === 1) return { type: { id: 'minecraft:dirt' }, amount: 5 }; + if (i === 2) return { type: { id: 'minecraft:stone' }, amount: 15 }; + }) + } + }) + }; + const items = InventoryUtils.getInventory(block); + expect(items).toEqual({ + 0: { typeId: 'minecraft:stone', amount: 10 }, + 1: { typeId: 'minecraft:dirt', amount: 5 }, + 2: { typeId: 'minecraft:stone', amount: 15 } + }); + }); + + it('returns empty object for block without inventory', () => { + const block = { + getComponent: vi.fn().mockReturnValue(undefined) + }; + const items = InventoryUtils.getInventory(block); + expect(items).toEqual({}); + }); + + it('returns empty object for empty inventory', () => { + const block = { + getComponent: vi.fn().mockReturnValue({ + container: { + size: 3, + getItem: vi.fn(() => undefined) + } + }) + }; + const items = InventoryUtils.getInventory(block); + expect(items).toEqual({}); + }); + + it('handles inventory with undefined (air) slots', () => { + const block = { + getComponent: vi.fn().mockReturnValue({ + container: { + size: 3, + getItem: vi.fn((i) => { + if (i === 0) return { type: { id: 'minecraft:stone' }, amount: 10 }; + if (i === 1) return undefined; + if (i === 2) return { type: { id: 'minecraft:stone' }, amount: 15 }; + }) + } + }) + }; + const items = InventoryUtils.getInventory(block); + expect(items).toEqual({ + 0: { typeId: 'minecraft:stone', amount: 10 }, + 2: { typeId: 'minecraft:stone', amount: 15 } + }); + }); +}); \ No newline at end of file From 69d542b266a761fe30062e3516cc31552d8f621c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 15:19:45 -0700 Subject: [PATCH 03/26] feat: instaminable quicker rising edge and refactor --- .../scripts/src/classes/Instaminable.js | 42 ++++++++++--------- .../src/rules/instaminableDeepslate.js | 33 +++++++++------ .../scripts/src/rules/instaminableEndstone.js | 33 +++++++++------ 3 files changed, 63 insertions(+), 45 deletions(-) diff --git a/Canopy[BP]/scripts/src/classes/Instaminable.js b/Canopy[BP]/scripts/src/classes/Instaminable.js index bbc3c7fc..1231f555 100644 --- a/Canopy[BP]/scripts/src/classes/Instaminable.js +++ b/Canopy[BP]/scripts/src/classes/Instaminable.js @@ -1,20 +1,29 @@ -import { Rules} from "../../lib/canopy/Canopy"; import { system, world } from "@minecraft/server"; const beaconRefreshOffset = {}; const BEACON_REFRESH_RATE = 80; -class Instaminable { - constructor(litmusCallback, ruleId) { - this.litmusCallback = litmusCallback; - this.ruleId = ruleId; - this.initEvents(); +export class Instaminable { + #isBlockTypeCallback; + #runner = void 0; + + constructor(blockTypeCallback) { + this.#isBlockTypeCallback = blockTypeCallback; + this.onPlayerStartBreakingBlockBound = this.onPlayerStartBreakingBlock.bind(this); + this.onEffectAddBound = this.onEffectAdd.bind(this); + } + + subscribeToEvents() { + this.#runner = system.runInterval(() => this.onTick()); + world.afterEvents.playerStartBreakingBlock.subscribe(this.onPlayerStartBreakingBlockBound); + world.afterEvents.effectAdd.subscribe(this.onEffectAddBound); } - initEvents() { - system.runInterval(this.onTick.bind(this), 1); - world.beforeEvents.playerBreakBlock.subscribe((event) => this.onPlayerBreakBlock(event)); - world.afterEvents.effectAdd.subscribe((event) => this.onEffectAdd(event)); + unsubscribeFromEvents() { + if (this.#runner !== void 0) + system.clearRun(this.#runner); + world.afterEvents.playerStartBreakingBlock.unsubscribe(this.onPlayerStartBreakingBlockBound); + world.afterEvents.effectAdd.unsubscribe(this.onEffectAddBound); } onTick() { @@ -26,15 +35,12 @@ class Instaminable { } } - onPlayerBreakBlock(event) { - const blockId = event.block.typeId; - if (Rules.getNativeValue(this.ruleId) !== true) - return; - if (!this.litmusCallback(blockId)) + onPlayerStartBreakingBlock(event) { + if (!this.#isBlockTypeCallback(event.block.typeId)) return; const player = event.player; - if (this.isEfficiencyFiveNetheritePick(event.itemStack) && this.hasHasteTwo(player)) { - const duration = player.getEffect('haste')?.duration; + if (this.isEfficiencyFiveNetheritePick(event.heldItemStack) && this.hasHasteTwo(player)) { + const duration = player.getEffect("haste")?.duration; if (duration > 0) system.run(() => player.addEffect('haste', duration, { amplifier: 2 })); } @@ -63,5 +69,3 @@ class Instaminable { return haste?.amplifier === 1; } } - -export default Instaminable; \ No newline at end of file diff --git a/Canopy[BP]/scripts/src/rules/instaminableDeepslate.js b/Canopy[BP]/scripts/src/rules/instaminableDeepslate.js index 90dc6949..931e076a 100644 --- a/Canopy[BP]/scripts/src/rules/instaminableDeepslate.js +++ b/Canopy[BP]/scripts/src/rules/instaminableDeepslate.js @@ -1,15 +1,22 @@ -import { BooleanRule } from 'lib/canopy/Canopy'; -import Instaminable from 'src/classes/Instaminable'; - -const instamineableDeepslateRule = new BooleanRule({ - category: 'Rules', - identifier: 'instaminableDeepslate', - description: { translate: 'rules.instaminableDeepslate' }, - wikiDescription: 'Makes deepslate and its variants instaminable when using an efficiency 5 netherite pickaxe with haste 2.' -}); - -function isDeepslate(value) { - return value.includes('deepslate') +import { BooleanRule, GlobalRule } from '../../lib/canopy/Canopy'; +import { Instaminable } from '../classes/Instaminable'; + +export class InstaminableDeepslate extends BooleanRule { + instaminable; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'instaminableDeepslate', + wikiDescription: 'Makes deepslate and its variants instaminable when using an efficiency 5 netherite pickaxe with haste 2.', + onEnableCallback: () => this.instaminable.subscribeToEvents(), + onDisableCallback: () => this.instaminable.unsubscribeFromEvents() + })) + this.instaminable = new Instaminable(InstaminableDeepslate.isDeepslate); + } + + static isDeepslate(blockId) { + return blockId.includes('deepslate'); + } } -new Instaminable(isDeepslate, instamineableDeepslateRule.getID()); +export const instaminableDeepslate = new InstaminableDeepslate(); diff --git a/Canopy[BP]/scripts/src/rules/instaminableEndstone.js b/Canopy[BP]/scripts/src/rules/instaminableEndstone.js index c51a0521..f22743c1 100644 --- a/Canopy[BP]/scripts/src/rules/instaminableEndstone.js +++ b/Canopy[BP]/scripts/src/rules/instaminableEndstone.js @@ -1,15 +1,22 @@ -import { BooleanRule } from 'lib/canopy/Canopy'; -import Instaminable from 'src/classes/Instaminable'; - -const instamineableEndstoneRule = new BooleanRule({ - category: 'Rules', - identifier: 'instaminableEndstone', - description: { translate: 'rules.instaminableEndstone' }, - wikiDescription: 'Makes endstone and its variants instaminable when using an efficiency 5 netherite pickaxe with haste 2.' -}); - -function isEndStone(value) { - return value.includes('end_stone'); +import { BooleanRule, GlobalRule } from '../../lib/canopy/Canopy'; +import { Instaminable } from '../classes/Instaminable'; + +export class InstaminableEndstone extends BooleanRule { + instaminable; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'instaminableEndstone', + wikiDescription: 'Makes endstone and its variants instaminable when using an efficiency 5 netherite pickaxe with haste 2.', + onEnableCallback: () => this.instaminable.subscribeToEvents(), + onDisableCallback: () => this.instaminable.unsubscribeFromEvents() + })) + this.instaminable = new Instaminable(InstaminableEndstone.isEndStone); + } + + static isEndStone(blockId) { + return blockId.includes('end_stone'); + } } -new Instaminable(isEndStone, instamineableEndstoneRule.getID()); +export const instaminableEndstone = new InstaminableEndstone(); From cad2cf9a7f76e97131d1d0ea149928e7ce26edb5 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 15:20:22 -0700 Subject: [PATCH 04/26] refactor: autoItemPickup & carefulBreak This change uses the same pattern as instamineable --- Canopy[BP]/scripts/src/classes/ItemPickup.js | 66 +++++++++++++++++ .../scripts/src/rules/autoItemPickup.js | 68 +++--------------- Canopy[BP]/scripts/src/rules/carefulBreak.js | 71 +++---------------- 3 files changed, 83 insertions(+), 122 deletions(-) create mode 100644 Canopy[BP]/scripts/src/classes/ItemPickup.js diff --git a/Canopy[BP]/scripts/src/classes/ItemPickup.js b/Canopy[BP]/scripts/src/classes/ItemPickup.js new file mode 100644 index 00000000..21f52c8c --- /dev/null +++ b/Canopy[BP]/scripts/src/classes/ItemPickup.js @@ -0,0 +1,66 @@ +import { system, world } from "@minecraft/server"; +import { calcDistance } from "../../include/utils"; +import { InventoryUtils } from "./InventoryUtils"; + +class ItemPickup { + static PICKUP_RANGE = 4; + brokenBlockEventsThisTick = []; + shouldPickup; + #runner = void 0; + + constructor(pickupConditionCallback) { + this.shouldPickup = pickupConditionCallback; + this.onPlayerBreakBlockBound = this.onPlayerBreakBlock.bind(this); + this.onEntitySpawnBound = this.onEntitySpawn.bind(this); + } + + subscribeToEvents() { + this.#runner = system.runInterval(() => this.onTick()); + world.afterEvents.playerBreakBlock.subscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.subscribe(this.onEntitySpawnBound); + } + + unsubscribeFromEvents() { + if (this.#runner !== void 0) + system.clearRun(this.#runner); + world.afterEvents.playerBreakBlock.unsubscribe(this.onPlayerBreakBlockBound); + world.afterEvents.entitySpawn.unsubscribe(this.onEntitySpawnBound); + } + + onTick() { + this.brokenBlockEventsThisTick = []; + } + + onPlayerBreakBlock(event) { + if (!this.shouldPickup(event.player)) + return; + this.brokenBlockEventsThisTick.push(event); + } + + onEntitySpawn(event) { + if (event.cause !== 'Spawned' || event.entity?.typeId !== 'minecraft:item') + return; + const itemEntity = event.entity; + const brokenBlockEvent = this.findBrokenBlockEventWhichDropped(itemEntity); + if (!brokenBlockEvent) + return; + InventoryUtils.pickupItemEntity(brokenBlockEvent.player, itemEntity); + } + + findBrokenBlockEventWhichDropped(itemEntity) { + let brokenBlockEvent; + try { + const itemEntityLocation = itemEntity.location; + brokenBlockEvent = this.brokenBlockEventsThisTick.find(blockEvent => this.entityCameFromBlock(blockEvent, itemEntityLocation)); + } catch { + /* pass */ + } + return brokenBlockEvent; + } + + entityCameFromBlock(blockEvent, itemEntityLocation) { + return calcDistance(blockEvent.block.location, itemEntityLocation) < ItemPickup.PICKUP_RANGE; + } +} + +export default ItemPickup; diff --git a/Canopy[BP]/scripts/src/rules/autoItemPickup.js b/Canopy[BP]/scripts/src/rules/autoItemPickup.js index 12b33ec9..2a8b43cd 100644 --- a/Canopy[BP]/scripts/src/rules/autoItemPickup.js +++ b/Canopy[BP]/scripts/src/rules/autoItemPickup.js @@ -1,76 +1,24 @@ import { BooleanRule, GlobalRule } from "../../lib/canopy/Canopy"; -import { system, world, GameMode } from "@minecraft/server"; -import { calcDistance } from "../../include/utils"; -import { InventoryUtils } from "../classes/InventoryUtils"; +import { GameMode } from "@minecraft/server"; +import ItemPickup from "../classes/ItemPickup"; export class AutoItemPickup extends BooleanRule { - static PICKUP_RANGE = 4; - brokenBlockEventsThisTick = []; - #runner = void 0; + itemPickup; constructor() { super(GlobalRule.morphOptions({ identifier: 'autoItemPickup', wikiDescription: 'Enables the automatic pickup of items that drop when you break a block.', - onEnableCallback: () => this.subscribeToEvents(), - onDisableCallback: () => this.unsubscribeFromEvents(), + onEnableCallback: () => this.itemPickup.subscribeToEvents(), + onDisableCallback: () => this.itemPickup.unsubscribeFromEvents(), independentRules: ['carefulBreak'] - })); - this.onPlayerBreakBlockBound = this.onPlayerBreakBlock.bind(this); - this.onEntitySpawnBound = this.onEntitySpawn.bind(this); + })) + this.itemPickup = new ItemPickup(AutoItemPickup.isSurvivalPlayer); } - subscribeToEvents() { - this.#runner = system.runInterval(() => this.onTick()); - world.afterEvents.playerBreakBlock.subscribe(this.onPlayerBreakBlockBound); - world.afterEvents.entitySpawn.subscribe(this.onEntitySpawnBound); - } - - unsubscribeFromEvents() { - if (this.#runner !== void 0) - system.clearRun(this.#runner); - world.afterEvents.playerBreakBlock.unsubscribe(this.onPlayerBreakBlockBound); - world.afterEvents.entitySpawn.unsubscribe(this.onEntitySpawnBound); - } - - onTick() { - this.brokenBlockEventsThisTick = []; - } - - onPlayerBreakBlock(event) { - if (!this.shouldPickup(event.player)) - return; - this.brokenBlockEventsThisTick.push(event); - } - - shouldPickup(player) { + static isSurvivalPlayer(player) { return player?.getGameMode() === GameMode.Survival; } - - onEntitySpawn(event) { - if (event.cause !== 'Spawned' || event.entity?.typeId !== 'minecraft:item') - return; - - const itemEntity = event.entity; - const brokenBlockEvent = this.findBrokenBlockEventWhichDropped(itemEntity); - if (!brokenBlockEvent) - return; - InventoryUtils.pickupItemEntity(brokenBlockEvent.player, itemEntity); - } - - findBrokenBlockEventWhichDropped(itemEntity) { - let brokenBlockEvent; - try { - brokenBlockEvent = this.brokenBlockEventsThisTick.find(blockEvent => this.entityCameFromBlock(blockEvent, itemEntity)); - } catch { - /* pass */ - } - return brokenBlockEvent; - } - - entityCameFromBlock(blockEvent, itemEntity) { - return calcDistance(blockEvent.block.location, itemEntity.location) < this.PICKUP_RANGE; - } } export const autoItemPickup = new AutoItemPickup(); diff --git a/Canopy[BP]/scripts/src/rules/carefulBreak.js b/Canopy[BP]/scripts/src/rules/carefulBreak.js index b9de5b80..6ea09601 100644 --- a/Canopy[BP]/scripts/src/rules/carefulBreak.js +++ b/Canopy[BP]/scripts/src/rules/carefulBreak.js @@ -1,76 +1,23 @@ import { BooleanRule, GlobalRule } from "../../lib/canopy/Canopy"; -import { system, world, GameMode } from "@minecraft/server"; -import { calcDistance } from "../../include/utils"; -import { InventoryUtils } from "../classes/InventoryUtils"; -import { AutoItemPickup } from "./autoItemPickup"; +import { GameMode } from "@minecraft/server"; +import ItemPickup from "../classes/ItemPickup"; -export class CarefulBreak extends BooleanRule { - static PICKUP_RANGE = AutoItemPickup.PICKUP_RANGE; - brokenBlockEventsThisTick = []; - #runner = void 0; +export class CarefulBreak extends BooleanRule { + itemPickup; constructor() { super(GlobalRule.morphOptions({ identifier: 'carefulBreak', - onEnableCallback: () => this.subscribeToEvents(), - onDisableCallback: () => this.unsubscribeFromEvents(), + onEnableCallback: () => this.itemPickup.subscribeToEvents(), + onDisableCallback: () => this.itemPickup.unsubscribeFromEvents(), independentRules: ['autoItemPickup'] - })); - this.onPlayerBreakBlockBound = this.onPlayerBreakBlock.bind(this); - this.onEntitySpawnBound = this.onEntitySpawn.bind(this); + })) + this.itemPickup = new ItemPickup(CarefulBreak.isSneakingSurvivalPlayer); } - subscribeToEvents() { - this.#runner = system.runInterval(() => this.onTick()); - world.afterEvents.playerBreakBlock.subscribe(this.onPlayerBreakBlockBound); - world.afterEvents.entitySpawn.subscribe(this.onEntitySpawnBound); - } - - unsubscribeFromEvents() { - if (this.#runner !== void 0) - system.clearRun(this.#runner); - world.afterEvents.playerBreakBlock.unsubscribe(this.onPlayerBreakBlockBound); - world.afterEvents.entitySpawn.unsubscribe(this.onEntitySpawnBound); - } - - onTick() { - this.brokenBlockEventsThisTick = []; - } - - onPlayerBreakBlock(event) { - if (!this.shouldPickup(event.player)) - return; - this.brokenBlockEventsThisTick.push(event); - } - - shouldPickup(player) { + static isSneakingSurvivalPlayer(player) { return player?.isSneaking && player?.getGameMode() === GameMode.Survival; } - - onEntitySpawn(event) { - if (event.cause !== 'Spawned' || event.entity?.typeId !== 'minecraft:item') - return; - - const itemEntity = event.entity; - const brokenBlockEvent = this.findBrokenBlockEventWhichDropped(itemEntity); - if (!brokenBlockEvent) - return; - InventoryUtils.pickupItemEntity(brokenBlockEvent.player, itemEntity); - } - - findBrokenBlockEventWhichDropped(itemEntity) { - let brokenBlockEvent; - try { - brokenBlockEvent = this.brokenBlockEventsThisTick.find(blockEvent => this.entityCameFromBlock(blockEvent, itemEntity)); - } catch { - /* pass */ - } - return brokenBlockEvent; - } - - entityCameFromBlock(blockEvent, itemEntity) { - return calcDistance(blockEvent.block.location, itemEntity.location) < CarefulBreak.PICKUP_RANGE; - } } export const carefulBreak = new CarefulBreak(); From 114c66eb5d4078bab75c6f31506e743c69f94837 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 15:20:34 -0700 Subject: [PATCH 05/26] feat: add fastMineGlass --- Canopy[BP]/scripts/main.js | 1 + Canopy[BP]/scripts/src/classes/FastMine.js | 74 +++++++++++++++++++ Canopy[BP]/scripts/src/rules/fastMineGlass.js | 22 ++++++ 3 files changed, 97 insertions(+) create mode 100644 Canopy[BP]/scripts/src/classes/FastMine.js create mode 100644 Canopy[BP]/scripts/src/rules/fastMineGlass.js diff --git a/Canopy[BP]/scripts/main.js b/Canopy[BP]/scripts/main.js index c3d530ec..0708ffb2 100644 --- a/Canopy[BP]/scripts/main.js +++ b/Canopy[BP]/scripts/main.js @@ -99,6 +99,7 @@ import './src/rules/entitySeparation' import './src/rules/enderPearlChunkLoading' import './src/rules/renderEndGatewayExits' import './src/rules/carefulBreak' +import './src/rules/fastMineGlass' // Simulated Player Rules import './src/rules/simplayer/simplayerSaving' diff --git a/Canopy[BP]/scripts/src/classes/FastMine.js b/Canopy[BP]/scripts/src/classes/FastMine.js new file mode 100644 index 00000000..5c883b44 --- /dev/null +++ b/Canopy[BP]/scripts/src/classes/FastMine.js @@ -0,0 +1,74 @@ +import { EntityComponentTypes, world } from "@minecraft/server"; +import { autoItemPickup } from "../rules/autoItemPickup"; +import { carefulBreak } from "../rules/carefulBreak"; +import { InventoryUtils } from "./InventoryUtils"; + +export class FastMine { + #isBlockTypeCallback; + + constructor(blockTypeCallback) { + this.#isBlockTypeCallback = blockTypeCallback; + this.onPlayerStartBreakingBlockBound = this.onPlayerStartBreakingBlock.bind(this); + } + + subscribeToEvents() { + world.afterEvents.playerStartBreakingBlock.subscribe(this.onPlayerStartBreakingBlockBound); + } + + unsubscribeFromEvents() { + world.afterEvents.playerStartBreakingBlock.unsubscribe(this.onPlayerStartBreakingBlockBound); + } + + onPlayerStartBreakingBlock(event) { + if (!this.#isBlockTypeCallback(event.block.typeId)) + return; + const player = event.player; + if (this.isEfficiencyFiveNetheritePick(event.heldItemStack)) + this.breakBlock(player, event.block, event.heldItemStack); + } + + isEfficiencyFiveNetheritePick(itemStack) { + if (itemStack && itemStack.typeId === 'minecraft:netherite_pickaxe') { + const enchants = itemStack.getComponent('minecraft:enchantable').getEnchantments(); + return enchants.some(enchant => enchant.type.id === 'efficiency' && enchant.level === 5); + } + return false; + } + + breakBlock(player, block, heldItemStack) { + const dimension = block.dimension; + const blockCenter = { x: block.x + 0.5, y: block.y + 0.5, z: block.z + 0.5 }; + dimension.playSound("dig.stone", blockCenter, { volume: 1.0 }); + this.createLoot(dimension, blockCenter, block, player, heldItemStack); + block.setType("minecraft:air"); + } + + createLoot(dimension, blockCenter, block, player, heldItemStack) { + const lootTableManager = world.getLootTableManager(); + try { + const loot = lootTableManager.generateLootFromBlock(block, heldItemStack); + if (this.shouldAutoPickup(player)) + this.addLootToPlayerInventory(player, loot); + else + this.spawnLoot(dimension, blockCenter, loot); + } catch (error) { + console.warn("[Canopy] Failed to generate and spawn loot for fast mine. Error", error, error.stack); + } + } + + shouldAutoPickup(player) { + return (autoItemPickup.getNativeValue() && autoItemPickup.itemPickup.shouldPickup(player)) + || (carefulBreak.getNativeValue() && carefulBreak.itemPickup.shouldPickup(player)); + } + + spawnLoot(dimension, location, loot) { + for (const itemStack of loot) + dimension.spawnItem(itemStack, location); + } + + addLootToPlayerInventory(player, loot) { + const playerInventory = player.getComponent(EntityComponentTypes.Inventory); + for (const itemStack of loot) + InventoryUtils.tryAddItemLikeVanilla(playerInventory?.container, itemStack); + } +} diff --git a/Canopy[BP]/scripts/src/rules/fastMineGlass.js b/Canopy[BP]/scripts/src/rules/fastMineGlass.js new file mode 100644 index 00000000..2891885c --- /dev/null +++ b/Canopy[BP]/scripts/src/rules/fastMineGlass.js @@ -0,0 +1,22 @@ +import { BooleanRule, GlobalRule } from '../../lib/canopy/Canopy'; +import { FastMine } from '../classes/FastMine'; + +export class FastMineGlass extends BooleanRule { + fastMine; + + constructor() { + super(GlobalRule.morphOptions({ + identifier: 'fastMineGlass', + wikiDescription: 'Makes glass and its variants break quickly when using an efficiency 5 netherite pickaxe.', + onEnableCallback: () => this.fastMine.subscribeToEvents(), + onDisableCallback: () => this.fastMine.unsubscribeFromEvents() + })) + this.fastMine = new FastMine(FastMineGlass.isGlass); + } + + static isGlass(blockId) { + return blockId.includes("glass"); + } +} + +export const instaminableEndstone = new FastMineGlass(); From a0fbade2a7c2d4324417e384b5ffbe37c388a490 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 15:32:10 -0700 Subject: [PATCH 06/26] fix: glass break sound for fastMineGlass --- Canopy[BP]/scripts/src/classes/FastMine.js | 6 ++++-- Canopy[BP]/scripts/src/rules/fastMineGlass.js | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Canopy[BP]/scripts/src/classes/FastMine.js b/Canopy[BP]/scripts/src/classes/FastMine.js index 5c883b44..e3b4dc36 100644 --- a/Canopy[BP]/scripts/src/classes/FastMine.js +++ b/Canopy[BP]/scripts/src/classes/FastMine.js @@ -5,9 +5,11 @@ import { InventoryUtils } from "./InventoryUtils"; export class FastMine { #isBlockTypeCallback; + #playBreakSoundCallback; - constructor(blockTypeCallback) { + constructor(blockTypeCallback, playBreakSoundCallback) { this.#isBlockTypeCallback = blockTypeCallback; + this.#playBreakSoundCallback = playBreakSoundCallback; this.onPlayerStartBreakingBlockBound = this.onPlayerStartBreakingBlock.bind(this); } @@ -38,7 +40,7 @@ export class FastMine { breakBlock(player, block, heldItemStack) { const dimension = block.dimension; const blockCenter = { x: block.x + 0.5, y: block.y + 0.5, z: block.z + 0.5 }; - dimension.playSound("dig.stone", blockCenter, { volume: 1.0 }); + this.#playBreakSoundCallback(dimension, blockCenter); this.createLoot(dimension, blockCenter, block, player, heldItemStack); block.setType("minecraft:air"); } diff --git a/Canopy[BP]/scripts/src/rules/fastMineGlass.js b/Canopy[BP]/scripts/src/rules/fastMineGlass.js index 2891885c..0f3bb9ec 100644 --- a/Canopy[BP]/scripts/src/rules/fastMineGlass.js +++ b/Canopy[BP]/scripts/src/rules/fastMineGlass.js @@ -11,12 +11,16 @@ export class FastMineGlass extends BooleanRule { onEnableCallback: () => this.fastMine.subscribeToEvents(), onDisableCallback: () => this.fastMine.unsubscribeFromEvents() })) - this.fastMine = new FastMine(FastMineGlass.isGlass); + this.fastMine = new FastMine(FastMineGlass.isGlass, FastMineGlass.playBreakSound); } static isGlass(blockId) { return blockId.includes("glass"); } + + static playBreakSound(dimension, location) { + dimension.playSound("random.glass", location, { volume: 1.0 }); + } } export const instaminableEndstone = new FastMineGlass(); From 82911a0ab39f27608557900607e20da6618e6fc6 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 15:40:10 -0700 Subject: [PATCH 07/26] fix: add missing description for fastMineGlass --- Canopy[RP]/texts/en_US.lang | 1 + 1 file changed, 1 insertion(+) diff --git a/Canopy[RP]/texts/en_US.lang b/Canopy[RP]/texts/en_US.lang index 1f890ca7..574fdf87 100644 --- a/Canopy[RP]/texts/en_US.lang +++ b/Canopy[RP]/texts/en_US.lang @@ -476,6 +476,7 @@ rules.entitySeparation=When stacked entities trigger a pressure plate, one entit rules.explosionChainReactionOnly=Makes explosions only affect TNT blocks. rules.explosionNoBlockDamage=Makes explosions not affect blocks. rules.explosionOff=Disables explosions entirely. +rules.fastMineGlass=Makes glass mine near-instantly when using a netherite pickaxe with efficiency 5. rules.flippinArrows=Using an arrow on blocks will flip, rotate, or open them. Putting it in your offhand will flip blocks when placed. 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. From 751c4a23fefdbe2d4676f25e3eeb5cb413a70225 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 7 Sep 2026 20:54:56 -0700 Subject: [PATCH 08/26] feat: translated entity list has a count of names --- Canopy[BP]/scripts/include/utils.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Canopy[BP]/scripts/include/utils.js b/Canopy[BP]/scripts/include/utils.js index 20b12285..63b1a5e9 100644 --- a/Canopy[BP]/scripts/include/utils.js +++ b/Canopy[BP]/scripts/include/utils.js @@ -233,15 +233,23 @@ export async function forceShow(player, form, { timeout = Infinity, showBusyMess export function getTranslatedEntityList(entities) { const message = { rawtext: [] }; - for (let i = 0; i < entities.length; i++) { - const entity = entities[i]; - if (entity.nameTag) - message.rawtext.push({ translate: entity.nameTag }); + const groupedEntities = new Map(); + for (const entity of entities) { + const name = entity.nameTag || entity.localizationKey; + const group = groupedEntities.get(name); + if (group) + group.count++; else - message.rawtext.push({ translate: entities[i].localizationKey }); - if (i !== entities.length - 1) - message.rawtext.push({ rawtext: [{ text: ', ' }] }); - } + groupedEntities.set(name, { entity, count: 1 }); + } + + let index = 0; + for (const { entity, count } of groupedEntities.values()) { + message.rawtext.push({ translate: entity.nameTag || entity.localizationKey }); + message.rawtext.push({ text: ` x${count}` }); + if (index++ !== groupedEntities.size - 1) + message.rawtext.push({ text: ', ' }); + } return message; } From 6f99c3c02fce4480bb74b9486bbccad2adb9d805 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 14 Sep 2026 16:48:19 -0700 Subject: [PATCH 09/26] feat: bump to v1.6.2 and MC 26.50 preview --- Canopy[BP]/manifest.json | 10 +++++----- Canopy[BP]/scripts/constants.js | 4 ++-- Canopy[RP]/manifest.json | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Canopy[BP]/manifest.json b/Canopy[BP]/manifest.json index 15ec165e..c9020324 100644 --- a/Canopy[BP]/manifest.json +++ b/Canopy[BP]/manifest.json @@ -1,18 +1,18 @@ { "format_version": 2, "header": { - "name": "Canopy [BP] v1.6.1", + "name": "Canopy [BP] v1.6.2", "description": "Technical informatics & features addon by §aForestOfLight§r.", "uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", "version": [ 1, 6, - 1 + 2 ], "min_engine_version": [ 1, 26, - 40 + 50 ] }, "modules": [ @@ -42,11 +42,11 @@ "dependencies": [ { "module_name": "@minecraft/server", - "version": "2.10.0-beta" + "version": "2.11.0-beta" }, { "module_name": "@minecraft/server-ui", - "version": "2.2.0-beta" + "version": "2.3.0-beta" }, { "module_name": "@minecraft/debug-utilities", diff --git a/Canopy[BP]/scripts/constants.js b/Canopy[BP]/scripts/constants.js index 4f944baa..55eda080 100644 --- a/Canopy[BP]/scripts/constants.js +++ b/Canopy[BP]/scripts/constants.js @@ -1,4 +1,4 @@ -const PACK_VERSION = '1.6.1'; -const MC_VERSION = '1.26.40.05'; +const PACK_VERSION = '1.6.2'; +const MC_VERSION = '1.26.50.27'; export { PACK_VERSION, MC_VERSION }; \ No newline at end of file diff --git a/Canopy[RP]/manifest.json b/Canopy[RP]/manifest.json index 960f1906..b4c2dcc8 100644 --- a/Canopy[RP]/manifest.json +++ b/Canopy[RP]/manifest.json @@ -1,18 +1,18 @@ { "format_version": 2, "header": { - "name": "Canopy [RP] v1.6.1", + "name": "Canopy [RP] v1.6.2", "description": "Technical informatics & features addon by §aForestOfLight§r.", "uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", "version": [ 1, 6, - 1 + 2 ], "min_engine_version": [ 1, 26, - 40 + 50 ] }, "modules": [ From 4c35cd467687f7307bc3207b180d0cb00e8087cb Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Mon, 14 Sep 2026 16:56:19 -0700 Subject: [PATCH 10/26] feat: isTamed works on pre-canopy entities --- .../scripts/src/classes/debugdisplay/Tame.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js b/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js index c3c4f451..ecf5e27b 100644 --- a/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js +++ b/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js @@ -19,7 +19,7 @@ export class Tame extends DebugDisplayTextElement { getFormattedData() { this.updateTamedToPlayerIdCache(); this.populateComponents(); - if (this.isTamed()) + if (this.hasIsTamedComponent()) return this.getTamedText(); let untamedText = `§3false`; if (this.hasPlayerIdProperty()) @@ -33,22 +33,25 @@ export class Tame extends DebugDisplayTextElement { let output = `§3true`; if (this.tamedToPlayerIdCache) output += `§7, By: ${getNameFromEntityId(this.tamedToPlayerIdCache)}`; + else if (this.isTamed?.tamedToPlayerId !== void 0) + output += `§7, By: ${getNameFromEntityId(this.isTamed.tamedToPlayerId)}`; return output; } getTamedToPlayerIdText() { - return `§7, By: ${getNameFromEntityId(this.tamedToPlayerIdCache) ?? 'None'}`; + return `§7, By: ${getNameFromEntityId(this.tamedToPlayerIdCache || this.isTamed?.tamedToPlayerId) ?? 'None'}`; } getTameableText() { const tameItems = this.tameable.getTameItems; const tameItemsText = tameItems.length === 0 ? '§7None' : tameItems.map(item => item?.typeId ?? 'Unknown').join(', '); return `\n§7Probability: ${this.tameable.probability.toFixed(2)}, Items: ${tameItemsText}`; - } + } populateComponents() { this.tameable = this.entity.getComponent(EntityComponentTypes.Tameable); this.tameMount = this.entity.getComponent(EntityComponentTypes.TameMount); + this.isTamed = this.entity.getComponent(EntityComponentTypes.IsTamed); if (this.tameable) this.tameItems = this.tameable.getTameItems; } @@ -58,16 +61,16 @@ export class Tame extends DebugDisplayTextElement { this.tamedToPlayerIdCache = playerId; } - isTamed() { - return this.entity.hasComponent(EntityComponentTypes.IsTamed); + hasIsTamedComponent() { + return this.isTamed?.isValid; } hasPlayerIdProperty() { - return (this.tameMount && this.tameMount.isValid) || (this.tameable && this.tameable.isValid) || this.tamedToPlayerIdCache; + return this.tameMount?.isValid || this.tameable?.isValid || this.tamedToPlayerIdCache || (this.isTamed?.isValid && this.isTamed?.tamedToPlayerId); } hasTameableComponent() { - return this.tameable && this.tameable.isValid; + return this.tameable?.isValid; } tryPortDPToNewUpdate() { From 1d9bb3229eeb6bc29fb2cc947528c348bdeebc42 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 15:51:34 -0700 Subject: [PATCH 11/26] fix: remove unused imports in utils test --- __tests__/BP/scripts/include/utils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/BP/scripts/include/utils.test.js b/__tests__/BP/scripts/include/utils.test.js index dad014fe..84a9b5b1 100644 --- a/__tests__/BP/scripts/include/utils.test.js +++ b/__tests__/BP/scripts/include/utils.test.js @@ -1,7 +1,7 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { calcDistance, isString, isNumeric, parseName, getClosestTarget, stringifyLocation, - getColorCode, wait, getInventory, locationInArea, getColoredDimensionName, + getColorCode, wait, locationInArea, getColoredDimensionName, getScriptEventSourceName, getScriptEventSourceObject, recolor, formatColorStr } from '../../../../Canopy[BP]/scripts/include/utils.js'; From d116d7e765b92a3f7144a6644ed2ae1c39f5e852 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 16:20:33 -0700 Subject: [PATCH 12/26] fix: bump RP/BP dependency version --- Canopy[BP]/manifest.json | 2 +- Canopy[RP]/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Canopy[BP]/manifest.json b/Canopy[BP]/manifest.json index c9020324..46395d17 100644 --- a/Canopy[BP]/manifest.json +++ b/Canopy[BP]/manifest.json @@ -61,7 +61,7 @@ "version": [ 1, 6, - 1 + 2 ] } ], diff --git a/Canopy[RP]/manifest.json b/Canopy[RP]/manifest.json index b4c2dcc8..f12dc3c0 100644 --- a/Canopy[RP]/manifest.json +++ b/Canopy[RP]/manifest.json @@ -32,7 +32,7 @@ "version": [ 1, 6, - 1 + 2 ] } ], From ea64a41e917d094a06bd6839f3dbf4fa2da69bfe Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 16:41:17 -0700 Subject: [PATCH 13/26] fix: bump Minecraft version badge in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be893e83..281ed5fd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![GitHub Downloads](https://img.shields.io/github/downloads/ForestOfLight/Canopy/total?label=Github%20downloads&logo=github)](https://github.com/ForestOfLight/Canopy/releases/latest) [![Curseforge Downloads](https://cf.way2muchnoise.eu/full_1062078_downloads.svg)](https://www.curseforge.com/minecraft-bedrock/addons/canopy) - [![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.40_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) + [![Minecraft - Version](https://img.shields.io/badge/Minecraft-v26.50_(Bedrock)-brightgreen)](https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs) [![CI](https://github.com/ForestOfLight/Canopy/actions/workflows/ci.yml/badge.svg)](https://github.com/ForestOfLight/Canopy/actions/workflows/ci.yml) [![Discord](https://badgen.net/discord/members/9KGche8fxm?icon=discord&label=Discord&list=what)](https://discord.gg/9KGche8fxm) [![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-donate-yellow.svg)](https://buymeacoffee.com/forestoflight) From 0760e639a8fde459c139da0315199976ce06a7c1 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 16:55:29 -0700 Subject: [PATCH 14/26] refactor: getColoredDimensionName & getColorByDimension remove code duplication --- Canopy[BP]/scripts/include/utils.js | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/Canopy[BP]/scripts/include/utils.js b/Canopy[BP]/scripts/include/utils.js index 63b1a5e9..9eb1ae04 100644 --- a/Canopy[BP]/scripts/include/utils.js +++ b/Canopy[BP]/scripts/include/utils.js @@ -99,36 +99,30 @@ export function locationInArea(area, position) { return inX && inY && inZ; } -export function getColoredDimensionName(dimensionId) { +function getDimensionStyle(dimensionId) { switch (dimensionId) { case 'minecraft:overworld': case 'overworld': - return '§aOverworld'; + return { color: '§a', name: 'Overworld' }; case 'minecraft:nether': case 'nether': - return '§cNether'; + return { color: '§c', name: 'Nether' }; case 'minecraft:the_end': case 'the_end': - return '§dEnd'; + return { color: '§d', name: 'End' }; default: - return '§f' + dimensionId; + return { color: '§f', name: dimensionId }; } } +export function getColoredDimensionName(dimensionId) { + const style = getDimensionStyle(dimensionId); + return style.color + style.name; +} + export function getColorByDimension(dimensionId) { - switch (dimensionId) { - case 'minecraft:overworld': - case 'overworld': - return '§a'; - case 'minecraft:nether': - case 'nether': - return '§c'; - case 'minecraft:the_end': - case 'the_end': - return '§d'; - default: - return '§f'; - } + const style = getDimensionStyle(dimensionId); + return style.color; } export function getScriptEventSourceName(event) { From 649a45f830eb7dc3555f597b3b2e60060cf97730 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 17:04:10 -0700 Subject: [PATCH 15/26] chore: bump dependencies --- package-lock.json | 38 +++++++++++++++++++------------------- package.json | 8 ++++---- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e44a742..3c12f506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,10 @@ "name": "canopy", "license": "MIT", "dependencies": { - "@minecraft/debug-utilities": "1.0.0-beta.1.26.40-stable", - "@minecraft/server": "2.10.0-beta.1.26.40-stable", - "@minecraft/server-gametest": "1.0.0-beta.1.26.40-stable", - "@minecraft/server-ui": "2.2.0-beta.1.26.40-stable" + "@minecraft/debug-utilities": "1.0.0-beta.1.26.50-stable", + "@minecraft/server": "2.11.0-beta.1.26.50-stable", + "@minecraft/server-gametest": "1.0.0-beta.1.26.50-stable", + "@minecraft/server-ui": "2.3.0-beta.1.26.50-stable" }, "devDependencies": { "@eslint/compat": "^1.2.5", @@ -878,19 +878,19 @@ "peer": true }, "node_modules/@minecraft/debug-utilities": { - "version": "1.0.0-beta.1.26.40-stable", - "resolved": "https://registry.npmjs.org/@minecraft/debug-utilities/-/debug-utilities-1.0.0-beta.1.26.40-stable.tgz", - "integrity": "sha512-4JvxODw+8w0U3srQ23vZzmzt8HTginiOApszde9baW4NvS/nEz4KAlwlLRXqNcLTCCS2JNcg6vbgpjjlkHz4gg==", + "version": "1.0.0-beta.1.26.50-stable", + "resolved": "https://registry.npmjs.org/@minecraft/debug-utilities/-/debug-utilities-1.0.0-beta.1.26.50-stable.tgz", + "integrity": "sha512-L9awSCUH4NwA6j46O4BfFX5tpGKLH1rSM0wQx2XVjEy37RAFbSMzt9z0AQYU0YLTGFxw0qrmMXAfUCZzFnSR1A==", "license": "MIT", "peerDependencies": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "^1.17.0 || ^2.0.0 || ^2.10.0-beta.1.26.40-stable" + "@minecraft/server": "^1.17.0 || ^2.0.0 || ^2.11.0-beta.1.26.50-stable" } }, "node_modules/@minecraft/server": { - "version": "2.10.0-beta.1.26.40-stable", - "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.10.0-beta.1.26.40-stable.tgz", - "integrity": "sha512-PygRGiom/LRXV/fxcZ0ygCzCzF8pRp+PoqnVX+saazdOMXUvu940fZZjXCaTdjvasLp8tN4i3EgJV1aTxidKAQ==", + "version": "2.11.0-beta.1.26.50-stable", + "resolved": "https://registry.npmjs.org/@minecraft/server/-/server-2.11.0-beta.1.26.50-stable.tgz", + "integrity": "sha512-l0ax/JeelRa1dXfPlKtIB4XLEgS896/LmTyYiWshyg63j+Kh9+eOffh4XAZ4/3/xgkQfYuICfUwmYhja77usew==", "license": "MIT", "peerDependencies": { "@minecraft/common": "^1.2.0", @@ -898,23 +898,23 @@ } }, "node_modules/@minecraft/server-gametest": { - "version": "1.0.0-beta.1.26.40-stable", - "resolved": "https://registry.npmjs.org/@minecraft/server-gametest/-/server-gametest-1.0.0-beta.1.26.40-stable.tgz", - "integrity": "sha512-xr3WTrEZ8BV37Qvf/UGsQJ/7keJWV1FVrkNu+IJ8Qs/tUXbQqqyZQ6wjGaGwzxPz9lr11VE6EH4UYp+8Mll0Dg==", + "version": "1.0.0-beta.1.26.50-stable", + "resolved": "https://registry.npmjs.org/@minecraft/server-gametest/-/server-gametest-1.0.0-beta.1.26.50-stable.tgz", + "integrity": "sha512-JrPSqkvcOSnAbaKRNcRKLuXL7frNKbWOBdrNZkU6oLDSblmuDGeMX+XI6DjxDrxS16fdzJolCb2pEAyvomkMyw==", "license": "MIT", "peerDependencies": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "^1.17.0 || ^2.0.0 || ^2.10.0-beta.1.26.40-stable" + "@minecraft/server": "^1.17.0 || ^2.0.0 || ^2.11.0-beta.1.26.50-stable" } }, "node_modules/@minecraft/server-ui": { - "version": "2.2.0-beta.1.26.40-stable", - "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.2.0-beta.1.26.40-stable.tgz", - "integrity": "sha512-Ab/k5lYMktAjVttv3u//SCCc/AUmKnzJYA+ZvvAoKxQ1hEDwLC2OhGF+YF/S/mkSX6oPK8hD6I8bJBEq2CyCOw==", + "version": "2.3.0-beta.1.26.50-stable", + "resolved": "https://registry.npmjs.org/@minecraft/server-ui/-/server-ui-2.3.0-beta.1.26.50-stable.tgz", + "integrity": "sha512-5Ez1duL+SIMCmPvL1IPcsDcKE+MTRaqdHNT+upoMutLYEYnzNFXxmFb5BEBiK22bfCBuHi28KBHWX2JCr6AVuA==", "license": "MIT", "peerDependencies": { "@minecraft/common": "^1.0.0", - "@minecraft/server": "^2.0.0 || ^2.10.0-beta.1.26.40-stable" + "@minecraft/server": "^2.0.0 || ^2.11.0-beta.1.26.50-stable" } }, "node_modules/@minecraft/vanilla-data": { diff --git a/package.json b/package.json index 95555755..e79d20d0 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ }, "license": "MIT", "dependencies": { - "@minecraft/debug-utilities": "1.0.0-beta.1.26.40-stable", - "@minecraft/server": "2.10.0-beta.1.26.40-stable", - "@minecraft/server-gametest": "1.0.0-beta.1.26.40-stable", - "@minecraft/server-ui": "2.2.0-beta.1.26.40-stable" + "@minecraft/debug-utilities": "1.0.0-beta.1.26.50-stable", + "@minecraft/server": "2.11.0-beta.1.26.50-stable", + "@minecraft/server-gametest": "1.0.0-beta.1.26.50-stable", + "@minecraft/server-ui": "2.3.0-beta.1.26.50-stable" }, "scripts": { "test": "vitest run --config vitest.config.js --coverage", From 3db9e4a06de138589ebc9e5ed5e857d449078e7c Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 17:11:38 -0700 Subject: [PATCH 16/26] fix: handle custom dimensions in various features --- Canopy[BP]/scripts/src/classes/EntityMovementLog.js | 5 +++-- Canopy[BP]/scripts/src/classes/WorldLifetimeTracker.js | 5 ++--- Canopy[BP]/scripts/src/commands/entitydensity.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Canopy[BP]/scripts/src/classes/EntityMovementLog.js b/Canopy[BP]/scripts/src/classes/EntityMovementLog.js index 084d42eb..90c3540a 100644 --- a/Canopy[BP]/scripts/src/classes/EntityMovementLog.js +++ b/Canopy[BP]/scripts/src/classes/EntityMovementLog.js @@ -1,5 +1,5 @@ import { EntityLog } from "./EntityLog"; -import { world, system } from "@minecraft/server"; +import { world, system, DimensionTypes } from "@minecraft/server"; class EntityMovementLog extends EntityLog { validTypes = ['projectiles', 'falling_blocks']; @@ -42,7 +42,8 @@ class EntityMovementLog extends EntityLog { updateEntityLists() { this.thisTickEntities = []; this.movingEntities = []; - for (const dimensionId of ['overworld', 'nether', 'the_end']) { + const dimensionIds = DimensionTypes.getAll().map(dimensionType => dimensionType.typeId); + for (const dimensionId of dimensionIds) { const dimEntities = world.getDimension(dimensionId).getEntities(); for (const entity of dimEntities) { if (this.hasTrait(entity, this.type)) diff --git a/Canopy[BP]/scripts/src/classes/WorldLifetimeTracker.js b/Canopy[BP]/scripts/src/classes/WorldLifetimeTracker.js index a14568e4..25bd278a 100644 --- a/Canopy[BP]/scripts/src/classes/WorldLifetimeTracker.js +++ b/Canopy[BP]/scripts/src/classes/WorldLifetimeTracker.js @@ -113,10 +113,9 @@ export class WorldLifetimeTracker { } createDimensionRecords() { - this.dimensionToEntityLifetimeRecordMap["minecraft:overworld"] = new EntityLifetimeRecords(this, "minecraft:overworld"); - this.dimensionToEntityLifetimeRecordMap["minecraft:nether"] = new EntityLifetimeRecords(this, "minecraft:nether"); - this.dimensionToEntityLifetimeRecordMap["minecraft:the_end"] = new EntityLifetimeRecords(this, "minecraft:the_end"); const dimensionIds = DimensionTypes.getAll().map(dimensionType => dimensionType.typeId); + for (const dimensionId of dimensionIds) + this.dimensionToEntityLifetimeRecordMap[dimensionId] = new EntityLifetimeRecords(this, dimensionId); for (const dimensionId of dimensionIds) { if (Object.keys(this.dimensionToEntityLifetimeRecordMap).includes(dimensionId)) continue; diff --git a/Canopy[BP]/scripts/src/commands/entitydensity.js b/Canopy[BP]/scripts/src/commands/entitydensity.js index d7d54e1e..5596114e 100644 --- a/Canopy[BP]/scripts/src/commands/entitydensity.js +++ b/Canopy[BP]/scripts/src/commands/entitydensity.js @@ -1,5 +1,5 @@ import { VanillaCommand, PlayerCommandOrigin } from "../../lib/canopy/Canopy"; -import { CommandPermissionLevel, CustomCommandParamType, world } from '@minecraft/server'; +import { CommandPermissionLevel, CustomCommandParamType, DimensionTypes, world } from '@minecraft/server'; import { getColoredDimensionName } from "../../include/utils"; const NUM_RESULTS = 10; @@ -70,7 +70,7 @@ function parseArgs(origin, gridSize, dimension) { function printDimensionEntities(origin) { const dimensionColors = ['§a', '§c', '§d']; let totalEntities = 0; - const dimensionIds = ['minecraft:overworld', 'minecraft:nether', 'minecraft:the_end']; + const dimensionIds = DimensionTypes.getAll().map(dimensionType => dimensionType.typeId); let output = '§7Dimension entities: ' for (let i = 0; i < dimensionIds.length; i++) { const dimensionId = dimensionIds[i]; From f0859dd34d46d44846630471a94190891bc6ecb3 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 21:15:04 -0700 Subject: [PATCH 17/26] feat: /debugentity tame fully rely upon is_tamed component --- .../scripts/src/classes/debugdisplay/Tame.js | 53 ++----------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js b/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js index ecf5e27b..cf86720b 100644 --- a/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js +++ b/Canopy[BP]/scripts/src/classes/debugdisplay/Tame.js @@ -1,29 +1,17 @@ import { DebugDisplayTextElement } from './DebugDisplayTextElement.js'; -import { EntityComponentTypes, world } from '@minecraft/server'; +import { EntityComponentTypes } from '@minecraft/server'; import { getNameFromEntityId } from '../../../include/utils.js'; export class Tame extends DebugDisplayTextElement { tameable; - tameMount; tameItems; - tamedToPlayerIdCache; - - static DP_ID = 'tamedToEntityId'; - - constructor(entity) { - super(entity); - this.tryPortDPToNewUpdate(); - this.tamedToPlayerIdCache = this.entity.getDynamicProperty(Tame.DP_ID); - } + isTamed; getFormattedData() { - this.updateTamedToPlayerIdCache(); this.populateComponents(); if (this.hasIsTamedComponent()) return this.getTamedText(); let untamedText = `§3false`; - if (this.hasPlayerIdProperty()) - untamedText += this.getTamedToPlayerIdText(); if (this.hasTameableComponent()) untamedText += this.getTameableText(); return untamedText; @@ -31,17 +19,12 @@ export class Tame extends DebugDisplayTextElement { getTamedText() { let output = `§3true`; - if (this.tamedToPlayerIdCache) - output += `§7, By: ${getNameFromEntityId(this.tamedToPlayerIdCache)}`; - else if (this.isTamed?.tamedToPlayerId !== void 0) - output += `§7, By: ${getNameFromEntityId(this.isTamed.tamedToPlayerId)}`; + const tamedToPlayerId = this.isTamed?.tamedToPlayerId; + if (tamedToPlayerId !== void 0) + output += `§7, By: ${getNameFromEntityId(tamedToPlayerId)}`; return output; } - getTamedToPlayerIdText() { - return `§7, By: ${getNameFromEntityId(this.tamedToPlayerIdCache || this.isTamed?.tamedToPlayerId) ?? 'None'}`; - } - getTameableText() { const tameItems = this.tameable.getTameItems; const tameItemsText = tameItems.length === 0 ? '§7None' : tameItems.map(item => item?.typeId ?? 'Unknown').join(', '); @@ -50,42 +33,16 @@ export class Tame extends DebugDisplayTextElement { populateComponents() { this.tameable = this.entity.getComponent(EntityComponentTypes.Tameable); - this.tameMount = this.entity.getComponent(EntityComponentTypes.TameMount); this.isTamed = this.entity.getComponent(EntityComponentTypes.IsTamed); if (this.tameable) this.tameItems = this.tameable.getTameItems; } - updateTamedToPlayerIdCache() { - const playerId = this.tamedToPlayerIdCache || this.entity.getDynamicProperty(Tame.DP_ID); - this.tamedToPlayerIdCache = playerId; - } - hasIsTamedComponent() { return this.isTamed?.isValid; } - hasPlayerIdProperty() { - return this.tameMount?.isValid || this.tameable?.isValid || this.tamedToPlayerIdCache || (this.isTamed?.isValid && this.isTamed?.tamedToPlayerId); - } - hasTameableComponent() { return this.tameable?.isValid; } - - tryPortDPToNewUpdate() { - const tamedToPlayerId = this.entity.getDynamicProperty('tamedToPlayerId'); - if (tamedToPlayerId) { - this.entity.setDynamicProperty(Tame.DP_ID, tamedToPlayerId); - this.entity.setDynamicProperty('tamedToPlayerId', void 0); - } - } - - static onEntityTamed(event) { - if (!event.tamingEntity || !event.entity) - return; - event.entity.setDynamicProperty(Tame.DP_ID, event.tamingEntity.id); - } } - -world.afterEvents.entityTamed.subscribe(Tame.onEntityTamed); \ No newline at end of file From 398761f6c712a3e3b7104aaece0cb13ad3885498 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 21:22:07 -0700 Subject: [PATCH 18/26] fix: slashes appearing in /data output --- Canopy[BP]/scripts/src/commands/data.js | 19 ++++++----- .../BP/scripts/src/commands/data.test.js | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 __tests__/BP/scripts/src/commands/data.test.js diff --git a/Canopy[BP]/scripts/src/commands/data.js b/Canopy[BP]/scripts/src/commands/data.js index d9c6b382..aad4b376 100644 --- a/Canopy[BP]/scripts/src/commands/data.js +++ b/Canopy[BP]/scripts/src/commands/data.js @@ -159,22 +159,25 @@ function formatComponent(target, component) { return `\n §7>§f ${component.typeId}§7 - {${output}}`; } -function formatObject(target, object, shouldColorTopLevel = false) { +export function formatObject(target, object, shouldColorTopLevel = false) { let output = ''; for (const key in object) { try { - if (typeof object[key] === 'function') + const value = object[key]; + if (typeof value === 'function') continue; - let value = object[key]; + let formatted; if (target === value) - value = 'this'; + formatted = JSON.stringify('this'); else if (typeof value === 'object') - value = formatObject(target, value, false); - + formatted = formatObject(target, value, false); + else + formatted = JSON.stringify(value); + if (shouldColorTopLevel) - output += `§7${key}=§b${JSON.stringify(value)}§7, `; + output += `§7${key}=§b${formatted}§7, `; else - output += `${key}=${JSON.stringify(value)}, `; + output += `${key}=${formatted}, `; } catch(error) { console.warn(error); } diff --git a/__tests__/BP/scripts/src/commands/data.test.js b/__tests__/BP/scripts/src/commands/data.test.js new file mode 100644 index 00000000..3fd94855 --- /dev/null +++ b/__tests__/BP/scripts/src/commands/data.test.js @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { formatObject } from "../../../../../Canopy[BP]/scripts/src/commands/data"; + +describe('formatObject', () => { + it('should format a flat object', () => { + expect(formatObject(null, { name: 'Steve', age: 3 })).toBe('{name="Steve", age=3}'); + }); + + it('should not escape nested objects', () => { + expect(formatObject(null, { owner: { name: 'Steve' }, age: 3 })).toBe('{owner={name="Steve"}, age=3}'); + }); + + it('should not escape deeply nested objects', () => { + expect(formatObject(null, { a: { b: { name: 'Steve' } } })).toBe('{a={b={name="Steve"}}}'); + }); + + it('should color nested objects when coloring the top level', () => { + expect(formatObject(null, { owner: { name: 'Steve' } }, true)).toBe('§7{§7owner=§b{name="Steve"}§7}§r'); + }); + + it('should skip functions', () => { + expect(formatObject(null, { name: 'Steve', greet: () => 'hi' })).toBe('{name="Steve"}'); + }); + + it('should replace self-references with this', () => { + const target = { id: 1 }; + expect(formatObject(target, { self: target, id: 1 })).toBe('{self="this", id=1}'); + }); + + it('should handle an empty object', () => { + expect(formatObject(null, {})).toBe('{}'); + }); +}); From 3a1cc78d90c4b12ef0f4f7d8a3eac00d6dbc678e Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 21:40:14 -0700 Subject: [PATCH 19/26] fix: blocks and entities show as this in their own /data output --- Canopy[BP]/scripts/src/commands/data.js | 38 ++++++++++++--- .../BP/scripts/src/commands/data.test.js | 48 +++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/Canopy[BP]/scripts/src/commands/data.js b/Canopy[BP]/scripts/src/commands/data.js index aad4b376..634dd63d 100644 --- a/Canopy[BP]/scripts/src/commands/data.js +++ b/Canopy[BP]/scripts/src/commands/data.js @@ -99,13 +99,13 @@ function formatProperties(target) { for (const key in target) { try { let value = target[key]; - if (typeof value === 'function') + if (typeof value === 'function') continue; - else if (target === value) + else if (isDataTarget(target, value)) value = 'this'; else if (typeof value === 'object') value = formatObject(target, value); - else + else value = JSON.stringify(value); output += `§7${key}=§b${value}§7, `; } catch(error) { @@ -144,11 +144,11 @@ function formatComponent(target, component) { let value = component[key]; if (typeof value === 'function') continue; - else if (target === value) + else if (isDataTarget(target, value)) value = 'this'; else if (typeof value === 'object') value = formatObject(target, value); - else + else value = JSON.stringify(value); output += `${key}=§b${value}§7, `; } catch(error) { @@ -159,6 +159,32 @@ function formatComponent(target, component) { return `\n §7>§f ${component.typeId}§7 - {${output}}`; } +export function isDataTarget(target, value) { + if (target === value) + return true; + if (!target || !value || typeof target !== 'object' || typeof value !== 'object') + return false; + try { + return isSameEntity(target, value) || isSameBlock(target, value); + } catch { + return false; + } +} + +function isSameEntity(target, value) { + return target.id !== undefined && target.id === value.id && target.typeId === value.typeId; +} + +function isSameBlock(target, value) { + if (target.id !== undefined || value.id !== undefined) + return false; + if (!target.location || !value.location || target.dimension?.id !== value.dimension?.id) + return false; + return target.location.x === value.location.x + && target.location.y === value.location.y + && target.location.z === value.location.z; +} + export function formatObject(target, object, shouldColorTopLevel = false) { let output = ''; for (const key in object) { @@ -167,7 +193,7 @@ export function formatObject(target, object, shouldColorTopLevel = false) { if (typeof value === 'function') continue; let formatted; - if (target === value) + if (isDataTarget(target, value)) formatted = JSON.stringify('this'); else if (typeof value === 'object') formatted = formatObject(target, value, false); diff --git a/__tests__/BP/scripts/src/commands/data.test.js b/__tests__/BP/scripts/src/commands/data.test.js index 3fd94855..c49f66c9 100644 --- a/__tests__/BP/scripts/src/commands/data.test.js +++ b/__tests__/BP/scripts/src/commands/data.test.js @@ -31,3 +31,51 @@ describe('formatObject', () => { expect(formatObject(null, {})).toBe('{}'); }); }); + +describe('formatObject target identity', () => { + const entity = { id: '-4294967295', typeId: 'minecraft:cow', location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + const block = { typeId: 'minecraft:chest', location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + + it('should recognize another wrapper of the same entity', () => { + const wrapper = { id: entity.id, typeId: entity.typeId, location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + expect(formatObject(entity, { entity: wrapper })).toBe('{entity="this"}'); + }); + + it('should expand a different entity', () => { + const other = { id: '-4294967296', typeId: 'minecraft:cow', location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + expect(formatObject(entity, { entity: other })).toContain('id="-4294967296"'); + }); + + it('should recognize another wrapper of the same block', () => { + const wrapper = { typeId: block.typeId, location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + expect(formatObject(block, { block: wrapper })).toBe('{block="this"}'); + }); + + it('should expand a block at a different location', () => { + const other = { typeId: block.typeId, location: { x: 9, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + expect(formatObject(block, { block: other })).toContain('x=9'); + }); + + it('should expand a block in a different dimension', () => { + const other = { typeId: block.typeId, location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:the_nether' } }; + expect(formatObject(block, { block: other })).toContain('the_nether'); + }); + + it('should not confuse an entity with a block at the same location', () => { + expect(formatObject(block, { entity: entity })).toContain('id="-4294967295"'); + }); + + it('should recognize the target nested several levels deep', () => { + const wrapper = { id: entity.id, typeId: entity.typeId, location: { x: 1, y: 2, z: 3 }, dimension: { id: 'minecraft:overworld' } }; + expect(formatObject(entity, { a: { b: { owner: wrapper } } })).toBe('{a={b={owner="this"}}}'); + }); + + it('should not treat the dimension as the target', () => { + expect(formatObject(entity, { dimension: entity.dimension })).toBe('{dimension={id="minecraft:overworld"}}'); + }); + + it('should survive a property access that throws', () => { + const hostile = { get id() { throw new Error('invalid entity'); } }; + expect(formatObject(entity, { hostile })).toBe('{hostile={}}'); + }); +}); From 56c72890f6f58aa27ee8fd72f6a674dedfdd173f Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 22:04:51 -0700 Subject: [PATCH 20/26] chore: add BuyMeACoffee support and remove Patreon --- .github/FUNDING.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f1b23c7f..1127a6bc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms github: ForestOfLight -patreon: ForestOfLight +patreon: open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel @@ -10,5 +10,5 @@ liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username -buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +buy_me_a_coffee: ForestOfLight custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From aaa7d7bfe574218aa49592f426a5d79c82a7f94d Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 22:09:37 -0700 Subject: [PATCH 21/26] feat: set proper bedrock-samples MC version --- Canopy[BP]/scripts/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Canopy[BP]/scripts/constants.js b/Canopy[BP]/scripts/constants.js index 55eda080..d9b1e357 100644 --- a/Canopy[BP]/scripts/constants.js +++ b/Canopy[BP]/scripts/constants.js @@ -1,4 +1,4 @@ const PACK_VERSION = '1.6.2'; -const MC_VERSION = '1.26.50.27'; +const MC_VERSION = '1.26.50.4'; export { PACK_VERSION, MC_VERSION }; \ No newline at end of file From 6acfe705d2670120a5342af13eaded39f4a8f448 Mon Sep 17 00:00:00 2001 From: Eric Blackman Date: Tue, 15 Sep 2026 14:23:20 -0500 Subject: [PATCH 22/26] fix: initialize light level renderers correctly RenderLightLevel initialized signalStrengthRenderers, but the class uses lightLevelRenderers throughout. This caused lightLevelRenderers to be undefined when stop() ran before start() initialized it. --- Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel.js b/Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel.js index 510bc984..71b6da82 100644 --- a/Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel.js +++ b/Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel.js @@ -11,7 +11,7 @@ class RenderLightLevel extends InfoDisplayShapeElement { player; playerId; static RENDER_DISTANCE = 4; - signalStrengthRenderers = {}; + lightLevelRenderers = {}; constructor(player) { const ruleData = { From 1cc3216dd101dfc5491c626af7e17c999da24791 Mon Sep 17 00:00:00 2001 From: Eric Blackman Date: Tue, 15 Sep 2026 17:22:26 -0500 Subject: [PATCH 23/26] test: cover render light level initialization Adds a regression test verifying that lightLevelRenderers is initialized when RenderLightLevel is constructed and that stop() can safely run before start(). --- .../infodisplay/RenderLightLevel.test.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 __tests__/BP/scripts/src/rules/infodisplay/RenderLightLevel.test.js diff --git a/__tests__/BP/scripts/src/rules/infodisplay/RenderLightLevel.test.js b/__tests__/BP/scripts/src/rules/infodisplay/RenderLightLevel.test.js new file mode 100644 index 00000000..f99a5265 --- /dev/null +++ b/__tests__/BP/scripts/src/rules/infodisplay/RenderLightLevel.test.js @@ -0,0 +1,29 @@ +import { describe, it, expect, vi } from 'vitest'; +import { RenderLightLevel } from '../../../../../../Canopy[BP]/scripts/src/rules/infodisplay/RenderLightLevel'; + +vi.mock('@minecraft/server', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + world: { + ...original.world, + afterEvents: { + ...original.world.afterEvents, + worldLoad: { subscribe: (callback) => callback() } + } + } + }; +}); + +const mockPlayer = { + id: 'render-light-level-test-player' +}; + +describe('RenderLightLevel', () => { + it('initializes lightLevelRenderers before start is called', () => { + const renderLightLevel = new RenderLightLevel(mockPlayer); + + expect(renderLightLevel.lightLevelRenderers).toEqual({}); + expect(() => renderLightLevel.stop()).not.toThrow(); + }); +}); From 2e27fdfb2ad0d3e1fc54582cee77cd6671bacad7 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 22:38:05 -0700 Subject: [PATCH 24/26] ci: add Github token to ci --- .github/workflows/ci.yml | 2 ++ __tests__/BP/scripts/include/data.test.js | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90e179a3..d9978337 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,8 @@ jobs: - name: Run tests with coverage run: npm test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload coverage report uses: actions/upload-artifact@v4 diff --git a/__tests__/BP/scripts/include/data.test.js b/__tests__/BP/scripts/include/data.test.js index da30daa5..3001b2ef 100644 --- a/__tests__/BP/scripts/include/data.test.js +++ b/__tests__/BP/scripts/include/data.test.js @@ -5,6 +5,11 @@ import { categoryToMobMap, meleeMobs } from '../../../../Canopy[BP]/scripts/incl import stripJsonComments from 'strip-json-comments'; const bedrockSamplesRawUrl = `https://raw.githubusercontent.com/Mojang/bedrock-samples/refs/tags/v${MC_VERSION}/`; +const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN; +const githubApiHeaders = { + Accept: 'application/vnd.github+json', + ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}) +}; describe.concurrent('categoryToMobMap', () => { for (const category in categoryToMobMap) { @@ -63,7 +68,7 @@ async function fetchAllFilesInRepoPath(owner, repo, path, ref = 'main') { async function walk(dirPath) { const url = `https://api.github.com/repos/${owner}/${repo}/contents/${dirPath}`; - const res = await axios.get(url, { params: { ref } }); + const res = await axios.get(url, { params: { ref }, headers: githubApiHeaders }); for (const item of res.data) { if (item.type === 'file') { const fileResponse = await axios.get(item.download_url); From 1fac5e1c5d0551c5cfaeefc18316762d08d3febc Mon Sep 17 00:00:00 2001 From: Eric Blackman Date: Tue, 15 Sep 2026 14:36:36 -0500 Subject: [PATCH 25/26] fix: update hud title text for 26.50 Updates the hud_title_text override to match the 26.50 vanilla control type. Bedrock 26.50 defines hud_title_text as a vertical stack_panel. Canopy overrides it as an image, causing the inherited orientation property to produce repeated UI errors. Changing the override to a vertical stack_panel resolves the errors while preserving Canopy's existing title display. --- Canopy[RP]/ui/hud_screen.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Canopy[RP]/ui/hud_screen.json b/Canopy[RP]/ui/hud_screen.json index 66200f29..72196d64 100644 --- a/Canopy[RP]/ui/hud_screen.json +++ b/Canopy[RP]/ui/hud_screen.json @@ -1,6 +1,7 @@ { "hud_title_text": { - "type": "image", + "type": "stack_panel", + "orientation": "vertical", "size": [ "100%c + 5px", "100%c + 4px"], "alpha": 0.7, "anchor_from": "top_right", @@ -29,4 +30,4 @@ } ] } -} \ No newline at end of file +} From b353c013a315db6fd62392a28a710cd5462e5b8e Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Tue, 15 Sep 2026 23:05:33 -0700 Subject: [PATCH 26/26] fix: offset away from the edge of the screen --- Canopy[RP]/ui/hud_screen.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Canopy[RP]/ui/hud_screen.json b/Canopy[RP]/ui/hud_screen.json index 72196d64..a2a9a7b4 100644 --- a/Canopy[RP]/ui/hud_screen.json +++ b/Canopy[RP]/ui/hud_screen.json @@ -2,11 +2,11 @@ "hud_title_text": { "type": "stack_panel", "orientation": "vertical", - "size": [ "100%c + 5px", "100%c + 4px"], + "size": [ "100%c", "100%c"], "alpha": 0.7, "anchor_from": "top_right", "anchor_to": "top_right", - "offset": [ 0, 0 ], + "offset": [ -5, 4 ], "layer": 30, "controls": [ {