Skip to content

Repository files navigation

Spoiled Nested Containers

Spoiled の腐敗判定が届いていない2箇所を埋める。 Minecraft 1.20.1 / Forge。サーバ側専用side=SERVER)。Spoiled 本体は改変しない。

何を直すか

1. 入れ子の容器 — チェストに入れたシュルカーの中身が腐らない

Spoiled の走査経路は3つあるが、ブロック容器の経路だけが入れ子へ潜らない。 2026-08-05 に 2.2.1 の bytecode で位置関係を確認した:

経路 ITEM_HANDLER を取る位置 入れ子へ
updateInventory(プレイヤーの持ち物) offset 43 / 57 = スロット走査の 潜る
updateContainer(トロッコ等のエンティティ) offset 38 / 52 = 潜る
onWorldTick(ブロック容器) offset 151 / 344 = スロット走査(377-454)の 潜らない

3つのうち1つだけ挙動が違うので、設計ではなく書き漏らしに見える。実測でも:

SpoilTimer(同じ箱・同じ 75 秒)
チェスト直下のパン 5 → 7
同じチェストに入れたシュルカーの中のパン 5 のまま

⇒ 「拠点ではシュルカーをチェストに入れておけば永久保存」という抜け道になっていた。

2. エンダーチェスト

中身はブロックエンティティではなく Player#getEnderChestInventory() に紐づくため、 Spoiled のどの経路からも見えない。config に minecraft:ender_chest を書いても効かない。

どう直すか

Spoiled の public な入口を呼ぶだけ。mixin は使わない。

  • ChunkHelper.getBlockEntityPositions() で本体と同じ範囲を走査する
  • 倍率は SpoiledConfigCache.containerModifier / spoilItemInHandleritemContainerModifier をそのまま使う ⇒ config の1行で調整できる
  • 腐敗の進行と置換は SpoilHelper / SpoilHandler に任せる

⚠ 気をつけたこと

  • 本体と同じ tick でしか動かないgameTime % spoilRate == 0)。 独自の間隔で回すと同じアイテムが1回の更新で二重に進む
  • 入れ物であるスロットだけを見る。中身が食料そのものの場合は本体が処理済みなので、 触ると二重に進む
  • 未開封のルートチェストに触らない。中身を読むと戦利品がその場で生成されてしまう (本体も同じ理由で飛ばしている)
  • オーバーワールド限定を本体にそろえる。片方だけ広げると 「ネザーのチェストの中のシュルカーだけ腐る」ことになる
  • 外側の器の倍率(冷蔵庫 0.01 等)と内側の倍率(シュルカー 0.5)が両方掛かる

確認

dev/verify/scenarios/spoiled-newfoods.jsonnewfoods-nested-shulker。 導入後、入れ子のパンが 5 → 7 に進むことを確認した(本体と同じ進み)。

エンダーチェスト側は dev/verify/scenarios/spoiled-enderchest.jsonクライアント台本(--server)でしか測れない——検証サーバは無人で PlayerTickEvent が飛ばないため。

実測(2026-08-06・台本 spoiled-enderchest: 同じ8回分の更新(240秒)で

置き場所 倍率 開始 8回後
エンダーチェスト 0.5 SpoilTimer:5 9(8回中 4回進んだ)
持ち物(陽性対照 等速 SpoilTimer:5 13(8回全部進んだ)

倍率 0.5 の期待値ちょうど。判定は probe mark で機械化してあり OK / NG 0。 ⚠ survival で行うこと——Spoiled はクリエイティブのプレイヤーを丸ごと飛ばす。 ⚠ 腐敗が進んだ状態から始めること——まっさらだと tag.isEmpty() 分岐でタグが付かず区別できない。

3. 塩漬けを「完全停止」から「倍率」へ

Spoiled の saltCompat は boolean で、有効にすると塩漬けの食料は canSpoil が false を返して一切腐らなくなる。当部の方針は 「絶対に腐らない置き場は作らない」(冷蔵庫ですら 0.01)なのに、 塩は海水から無限に採れるうえ元の食料のまま残るので、これだけ例外的に最強だった。

作り: saltCompat = true のままにして Spoiled には塩漬けを完全に無視させる。 そのうえで本MODが、無視された分を低い倍率で進める。二重に進まないのはこのため。

  • 倍率は containerModifier の擬似キー spoiled:salted(既定 0.01)。 ⚠ ブロックエンティティのIDではないが、つまみを1か所にまとめるため同じ表を使っている
  • 塩の印を外した複製で腐敗レシピを引く。本物のまま getSpoilRecipe を呼ぶと canSpoil が false を返して null になる(それが saltCompat の仕組みそのもの)
  • 腐敗タグは自分で撒く。塩漬けは最初から NBT を持つので、本体の updateSpoilingStacktag.isEmpty() の分岐に入れず永久に何もしない

実測(倍率を一時的に 1.0 にした陽性対照): まっさらな塩漬けパンに SpoilTimer:1 が撒かれ、SpoilTimer:5 から始めた塩漬けは 7 まで進んだ (改定前は 5 のまま止まっていた)。本番の 0.01 では 5 のまま留まる。

上流

入れ子の件は Spoiled 側の不整合なので、上流が3経路をそろえたら本MODの1番目は不要になる。 外しても害はない。

走査の代償(2026-08-07 実測)

本MODは Spoiled 本体と同じブロックエンティティ全走査をもう1回歩く(入れ子を見るため)。 30 秒に1回とはいえ大きな拠点ではバーストになるので、2つの入口から測った。

1. MOD 自身の計測(本体に仕込んである)

onLevelTick の所要を毎回測り、最初の5回は必ず/以後は 50ms を超えたときだけ ログに出す。配布物に入るのでログは汚さない。

ワールド 所要 ブロックエンティティ 入れ子
probeworld(台本用) 0〜2ms 24 5 1
半径384を焼いた新規 0ms 13 1 0

2. MOD の有無での A/B(py verify/measure_scan_cpu.py --preset spoiled_nested

同じシードで半径384を焼き、再起動後の定常状態を180秒サンプリングした。

条件 CPU中央値 CPUピーク
入れた(on) 121.27 606.52
抜いた(off) 128.43 624.81

抜いた側のほうが高く出た差はノイズに埋もれている。 ⚠ 陽性対照つき: on 側のログに走査の行が10件、off 側は0件かつ MOD の語も0件で、 jar の除去が実際に効いている(同じ条件を2回測ったのではない)。

⚠ ここから言えること・言えないこと

  • 言える: この規模では代償が測れない。1回の A/B で符号が逆に出る程度の差しかない
  • ⚠ 言えない: 大きな拠点での話は未確認。上の2つの世界はどちらも 器が1〜5個しか無い新規生成で、心配していた「箱がたくさん並ぶ拠点」を再現していない
  • どう埋めるか: MOD 自身の計測を残してあるので、本番で 50ms を超えたらログに出る。 出たら見直す

About

Extends Spoiled's food decay to nested containers and ender chests (Forge 1.20.1, server-side)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages