Create Diesel Generator の Pumpjack の仕様

Create: Stranded At Sea という Modpack をやっていたら、Create Diesel Generator というModの理解が必要そうだった。Crude Oil をチャンクから引き出すのに Pumpjack が必要なんだけどこの contraption の仕様が ponder みてもよくわからなかったのでソースコードをみた。

自分が動作確認したのは 1.21.1-1.3.8 なんだけどこのプロジェクトのソースコードもタグ管理されてないのでマイクラ 1.21.1 向けブランチ最新を読んだ。

tl;dr

1台の Pumpjack で最大の抽出速度を出すには以下の構成になる。

H***************@***B
                *
                *
                *
O               *   C
-----------------------
  • H: Head
  • @: 軸、Bearing ブロックが横付けされた任意のブロック
  • B: Bearing B ブロック、Bearing ブロックを Wrench 右クリック
  • O: Hole
  • C: Crank

H-@距離は16(間のブロックは16),@-B距離は4(間のブロックは4)

一度の CrudeOil 抽出量(mB) = 1000 * H@距離 / @B距離 *  (Crank Large なら 2、Small なら 1)

アームの長さ(H@距離と@B距離)は最小4最大16なので上記構成の最大の抽出量は

1000 * 16/4 * 2 = 8000(mB)

Hole のタンク容量最大まで一度に引き出せるように作られているということになる。この場合、Mechanical Pump が一つでは足りなそう。

Contraption 動作条件

基本は Ponder の通りに構築すればよいのだが言及されていないところで気になる点があった。

Crank ブロック横の右クリック長押しインターフェース設定でContraptionの高さちょっとだけ違う。

ソースコード: https://github.com/george8188625/Create-Diesel-Generators/blob/a804b0f2ad2657748e3fd20d79203a175d23b28b/src/main/java/com/jesz/createdieselgenerators/contraption/PumpjackBearingBMovementBehaviour.java#L44-L64

  • Crank ブロックが small 設定のとき: contraption の Bearing B 部の地面からの高さ3(crank と bearing B の間は2ブロック)
  • large 設定のとき: 高さ4(crank と bearing の間は3ブロック)

Crank ブロックの small/large 設定は1イテレーション当たりの Crude Oil 抽出量にもかかわってくる。特に理由がなければ large でよいと思う。

Contraption 自体の構築条件は詳しく調査してない。Bearing に接触している軸ブロックと Bearing B の間に3ブロック以上が必要そうなのは確認した。ここが短いと警告が出て contraption 構築がされない。

1イテレーションの Crude Oil 抽出量

Pumpjack のヘッドが上に持ち上がったタイミングで、Crude Oil が抽出され、Pumpjack Hole に一定量の Crude Oil が追加される。このイテレーション1回あたりの追加量は Contraption の形状と Crank の設定次第。

ソースコード: https://github.com/george8188625/Create-Diesel-Generators/blob/94b3ff7c81771855c93709f3229ccc5b217e1bb1/src/main/java/com/jesz/createdieselgenerators/content/pumpjack/PumpjackHoleBlockEntity.java#L175

int subtractedAmount = Mth.clamp((int) (1000 * Math.abs((float) headPos / (float) bearingPos)) * (isCrankLarge ? 2 : 1), 0, oilAmount);

bearingPos を最小、headPos を最大、Crank を Large 設定にするとよさそうなのがわかる。

bearingPos/headPos が何なのかというとこれは Contraption のロジックを参照する必要がある。読むのが大変。

ソースコード: https://github.com/george8188625/Create-Diesel-Generators/blob/a804b0f2ad2657748e3fd20d79203a175d23b28b/src/main/java/com/jesz/createdieselgenerators/contraption/PumpjackHeadMovementBehaviour.java#L104-L105

  • bearingPos: 軸から Bearing B ブロックまでの距離
  • headPos: 軸から Head ブロックまでの距離

つまり bearingPos は上で言及した通り、間に3ブロック必要なので最小値は4となる。

headPos の最大値はソースコードを読む限り16ブロック。これは検証していない。

ソースコード: https://github.com/george8188625/Create-Diesel-Generators/blob/a804b0f2ad2657748e3fd20d79203a175d23b28b/src/main/java/com/jesz/createdieselgenerators/content/pumpjack/PumpjackBearingBlockEntity.java#L98-L101

今回は Contraption をチャンクに収めるために 16-1-4 = 11 の構成とした。Contraption になったらエンティティ扱いだしチャンクに収める意識は不要そうなんだけど、あんまり大きくある必要がないっていうのもあってこれで済ませた。

マイクラ1.20.1における Thermal Arboreal Extractor の変更と CABIN 2 での実際の挙動

Create 6.0 & CABIN 2 がきて Thermal Series の Arboreal Extractor の挙動が変わってるっぽいのを察知して実装を確認した。

イクラ 1.20.1 向け Arboreal Extractor の仕様

Thermal の Changelog には特に言及が見当たらないがマイクラ 1.20.1 (Thermal 11) で rewrite された様子。主な変更内容は:

近くに Arboreal Extractor があっても抽出速度が落ちることがなくなった

Create: CAE をやったときは、くっつけた木ブロック周囲1ブロックに他の Arboreal Extractor があると抽出スパンが長くなる仕様があったが、それがなくなった。もう水平方向四方にくっつけても遅くならない。後述するけど縦はだめかも。

高くて葉が多いと一度の抽出で係数がかかってJEIに書かれた量よりも多く抽出できるようになった

レシピごとに最小・最大の木の幹の高さ、最小・最大の葉ブロック数が設定されている。たとえばオークの木は、最小幹高4, 最大幹高10, 最小葉ブロ16, 最大葉ブロ24に設定されている。

これは幹が4より低かったり葉ブロックが16より少ないと25mbより少ない抽出量になるし、それ以上なら25mbより多い抽出量になる。たとえば幹10ブロック、葉ブロック24個と両方最大までの大きさだと sqrt(10 * 24 / (4 * 16)) = 1.936... とおおよそ倍の量が抽出できるようになる。それ以上の高さと葉は無意味。

計算式は 該当実装を確認すること。

実装: https://github.com/CoFH/ThermalCore/blob/cc7c230146a71d177767a8f9d6b26f0a52e9aeed/src/main/java/cofh/thermal/core/common/block/entity/device/DeviceTreeExtractorBlockEntity.java#L165-L166

余談だけどレシピでなんで苗木の定義が必要なのか見てみたら、どうやら背後の幹が苗木が設置できるところなのかどうかも確認している様子。これによって妙な環境に自力で木を立てたり、Arboreal Extractor の多段設置を拒否している。

下方向に生える木にも対応してるっぽい

Arboreal Extractor の背後の幹を下方向にスキャンして幹と葉のカウントをするロジックもある。これは未検証の挙動。

実装: https://github.com/CoFH/ThermalCore/blob/cc7c230146a71d177767a8f9d6b26f0a52e9aeed/src/main/java/cofh/thermal/core/common/block/entity/device/DeviceTreeExtractorBlockEntity.java#L335

でも CABIN 2 ではちょっと違うレシピになっている

端的に言うと上述した後者の大きな木によるバフ効果を無効化している。すべて未定義にしている。

未定義時はすべて 3 になる。該当箇所。つまり 旧来の Arboreal Extractor の仕様とほぼ同等になっている。ということで幹3以上葉3個以上は適当で良い。ついでに苗木の定義まで外しているので、苗木設置可能かどうかのチェックをスキップしていることになり多段も可能になっている(はず、これは未検証)。もちろん設置したところから上(or下)に3以上の幹と3個以上の葉が必要になる。

IntegratedDynamicsで再利用性を意識したロジック構築

IntegratedDynamics でロジックを組むとき、関数呼び出しだけしてロジックを組むなら難しくないが、再利用するために関数を作ってから最後にこれを呼び出す、ということをしようとすると難しい。

関数型プログラミングというやつだと思うんだけどこのあたりの知見がなく丸一日かかってしまった。一晩寝て起きてようやく解決できた。

今回やりたかったこと:

  • Entity Reader が読み取っているアイテムエンティティに希望のアイテム一覧が揃っているか確認し、なかったら補充をする
  • Entity Reader の眼の前には常に定義したアイテムエンティが置かれているようにしたい
  • Create の Chute に Exporter を使って希望のアイテムを配置するようにする
    • Create Arcane Engineering では World Item Exporter が無効化されている
  • Exporter の Aspect "Export Items" に希望のアイテム一覧を渡すことで実現をする

この動画の実装の話:

今回はこの「Exporter の Aspect "Export Items"」にわたすカードを作る。

結論から書くと以下のような疑似コードになる:

Missing1: ItemList
=Apply2(ListMissing, DetectedEntityList1, DesiredItemList1)

DesiredItemList1: ItemList
=List(SulfurDust)

DetectedEntityList1: EntityList
=EntityReaderから取得したエンティティリスト

// 揃えるべきアイテムリストと、検知したアイテムリストを比較して、欠けているアイテムリストを返す
// DetectedEntityList: Entity Reader が検知したエンティティリスト 
// DesiredItemList: 欲しいアイテムリスト
ListMissing(DetectedEntityList, DesiredItemList): ItemList
=Pipe(EntitiesToItem, FlippedRemoveAll)

FlippedRemoveAll(DenyList, BaseList): ItemList
=Pipe(NotContainsRaw, Filter)

// ItemList に Item が一切含まれていなかったら true を返す
NotContainsRaw(ItemList, Item): Boolean
=Pipe(ContainsRaw, InvertApply)

// ItemList に Item が含まれていたら true を返す
// ID に組み込まれている Contains の代わり
// Contains はアイテムエンティティに2個以上のアイテムが含まれていると別のアイテム扱いになってしまうため
ContainsRaw(ItemList, Item): Boolean
=Flip(FlippedContainsRaw)

FlippedContainsRaw(Item, ItemList): Boolean
=Pipe(RawEquals, FlippedContainsPredicate)

FlippedContainsPredicate(Operator[(Item): Boolean], ItemList): Boolean
=Flip(ContainsPredicate)

// 第一引数のOperatorの実行結果を否定する
InvertApply(Operator[(Item): Boolean], Item): Boolean
=Pipe(Negation, Apply)

// エンティティリストをアイテムリストに変換する
EntitiesToItems(DetectedEntityList): ItemList
=Apply(EntityToItem, Map)

/////////////////////////////////////////////////
// Integrated Dynamics があらかじめ用意している関数
/////////////////////////////////////////////////

Filter(Operator[(Item): Boolean], ItemList): ItemList
Map(Operator[(Item): Boolean], ItemList): BooleanList
ContainsPredicate(ItemList, Operator[(Item):Boolean]): Boolean
Apply(
  Operator[(Item):Boolean],
  Item
  ): Boolean
RawEquals(Item, Item): Boolean
Pipe(
  Operator[(A): B],
  Operator[(B): C]
  ): Operator[(A): C]
Pipe(
  Operator[(A): B],
  Operator[(B, B2): C]
  ): Operator[(A, B2): C]
Pipe(
  Operator[(A1, A2): B],
  Operator[(Operator[(A2): B], B2): C]
  ): Operator[(A1, B2): C]
EntityToItem(Entity): Item
Negation(Operator[(Item): Boolean]): Operator[(Item): Boolean]

この Missing1 を Exporter の Export Items に挿せば目的の仕組みができる。

Missing1をEntityReaderに挿してる様子

苦労したところ: Pipe の挙動

関数型プログラミングでは常識なのかもしれないけれど、IDのマニュアルが簡潔すぎて Pipe の挙動がピンとこなかった。特にこれ

Pipe(
  Operator[(A1, A2): B],
  Operator[(Operator[(A2): B], B2): C]
  ): Operator[(A1, B2): C]

第1引数の Operator の引数が2個以上だと、第2引数の Operator には Operator が渡されてしまう。

以下のような挙動だと思ってたが違った:

// これが勘違いだった
Pipe(
  Operator[(A1, A2): B1],
  Operator[(B1,B2): C]
  ): Operator[(A1, A2, B2): C]

このため ContainsRaw(ItemList, Item): Boolean を否定するにはどうしたらいいのか、というのがわからなくて一晩明かしてしまう羽目になった。

無理やり Pipe に与えたときに必要となるシグネチャはこれになる:

NotContainsRaw(ItemList, Item): Boolean
=Pipe(ContainsRaw, なにか)

なにか(Operator[(Item):Boolean], Item): Boolean

この「なにか」はシグネチャだけ見ると Apply っぽいんだけど否定する必要があるので Apply ではない、第一引数のOperatorの結果を否定する必要がある、というところまでは思いつき InvertedApply という名前をつけたがそこで深夜になって諦めて寝てしまった。

結局翌朝 Negation, Pipe, Apply を組み合わせることでこの InvertedApply が実現できた:

// 第一引数のOperatorの実行結果を否定する
InvertApply(Operator[(Item): Boolean], Item): Boolean
=Pipe(Negation, Apply)

これで Sulfur Dust 常時配置する以外にも再利用できて Variable Storage の省スペース化ができる。

Sulfer Dust を水に放り込んで別の液体を作る
Chrome (CAEのアイテム)を水に放り込んで別の液体を作る

また、Create の Funnel と Smart Fluid Pipe を使っても実現できそうなんだけど、IDでロジックを組むとこれもまた省スペース化になる。

Ars Nouveau の Source Link のイベント検知距離は 15 ブロック

ユークリッド距離で。

ソースコード: https://github.com/baileyholl/Ars-Nouveau/blob/dd005b4ff100fb70a660124e2484ff103013a405/src/main/java/com/hollingsworth/arsnouveau/common/block/tile/SourcelinkTile.java#L82-L84

これは記述時点マイクラ 1.19.2 向けバージョン 3.23.0 の値。1.18.2 向け最終バージョン 2.9.0 でも同じ

背景

Create Arcane Engineering をやっていて「そういや Source Link の検知距離はいくつなんだろ」と実装を確認した。以下で示すソースコードのリンクは 3.23.0 のものだけど、CAE が使ってるのは 2.9.0 なのでちょっと紛らわしいかも。

イベント検知系 Source Link

植物の成長イベントがソースになっている Agronomic Source Link や動物の生死イベントがソースになっている Volcanic Source Link は冒頭に書いた実装によってイベント検知している。

つまり、Agronomic Source Link と Volcanic Source Link のイベント検知範囲はユークリッド距離で 15 ブロック。

アイテムエンティティ消費系 Source Link

食物を消費する Mycelial Source Link と可燃物を消費する Volcanic Source Link のこと。これは上のイベント検知系 Source Link と違う実装になっている。

Mycelial Source Link: https://github.com/baileyholl/Ars-Nouveau/blob/dd005b4ff100fb70a660124e2484ff103013a405/src/main/java/com/hollingsworth/arsnouveau/common/block/tile/MycelialSourcelinkTile.java#L38

Volcanic Source Link: https://github.com/baileyholl/Ars-Nouveau/blob/dd005b4ff100fb70a660124e2484ff103013a405/src/main/java/com/hollingsworth/arsnouveau/common/block/tile/VolcanicSourcelinkTile.java#L42

両方とも同じ new AABB(worldPosition).inflate(1.0)。つまりアイテムエンティティ検知範囲は1ブロック(マンハッタン距離)。

CAE の Arboreal Extractor で期待した量の抽出ができない

Create: Arcane Engineering を触ってるが、Thermal Expansion の Arboreal Extractor で JEI に記載されているような量の抽出ができない。

原因を調べたところ Arboreal Extractor の実装上の問題で、追加レシピ次第では JEI に記載されている情報とズレが出ることがわかった。

現象

Arboreal Extractor の JEI で表示されるレシピをみると次のようになっている:

Spruce の Arboreal Extractor のレシピ

Spruce Log, Spruce Leaves でオペレーションごとに 50mB の Resin が抽出されると記載されているが、実際には 25mB しか抽出されない。

原因

Arboreal Extractor の実装の影響で Create: Arcane Engineering が追加している別の Spruce レシピを引いてしまっている。 Arboreal Extractor の実装が悪いのか Create: Arcane Engineering の設定が悪いのかは判断が難しいところ。

幹で抽出物・量が決まるというのは割と違和感は無いので Arboreal Extractor の実装はこれでいいかもしれない。

Arboreal Extractor の実装

ここを見れば分かる通り、幹(trunk)だけを見て Fluid の導出をしている。葉(leaves)は少し上で稼働条件を満たしているかという確認にしか使っていない。

この実装により、幹に対する抽出物・量はあとから追加されるレシピによって上書きされてしまう。

Arboreal Extractor のレシピ

Arboreal Extractor でバンドルされている Spruce Tree に対するレシピは次のようになっている:

https://github.com/CoFH/ThermalCore/blob/b55443b053c0991a36fd36d3e192d8e825703f10/src/main/resources/data/thermal/recipes/devices/tree_extractor/tree_extractor_spruce.json#L7

{
  "type": "thermal:tree_extractor",
  "trunk": "minecraft:spruce_log",
  "leaves": "minecraft:spruce_leaves",
  "result": {
    "fluid": "thermal:resin",
    "amount": 50
  }
}

一方 Create: Arcane Engineering では BYG で追加される葉の色違い Spruce Tree に対するレシピを追加している。

https://github.com/CoolerGangster/Create-Arcane-Engineering/blob/27b9ac6feba82aa584e6cc8821089aebf09c978c/overrides/kubejs/server_scripts/treeExtractor.js#L314-L319

{
  trunk: 'minecraft:spruce_log',
  leaf: 'byg:yellow_spruce_leaves',
  sap: 'thermal:resin',
  rate: { living: 25, dead: 4 }
},

JEI のレシピはこれ:

BYG の色違い Spruce に対する Arboreal Extractor のレシピ

kubejs で追加されるのでこちらがあとに読み込まれ Spruce Log に対する抽出量 25mB が上書きされてしまう。

これはあくまで Thermal Expansion が内部で持っている対応付の上書きなので、JEI には反映されない。

回避策

オペレーションあたり 50 mB ほしいなら Fir 使う。ただしこちらにも罠があって、Fir にも Biomes O' Plenty で追加される Fir と Biome You Go で追加される Fir がある。

愚痴

BOP や BYG といった大量にブロックが追加される MOD を採用して一貫性のあるクラフト体験というのはもう無理なのではないかと思う。 自分で Modpack 作るときは Modpack としての一定の完成度を求めるなら手を付けないのが良さそう。

追記: 既存のレシピの倣う形で PR を送った。https://github.com/CoolerGangster/Create-Arcane-Engineering/pull/80 バランスのためにひょっとしたら既存レシピの 50mB を 25mB に変更したいって言われる可能性はあるが。

トライアングルストラテジーメモ

結構テキストあるので楽しく読んでる。エスフロストへ外遊し現在第5章までやったところ。キャラ強化の進捗から判断するに序盤佳境ってところかな?という判断で一度情報まとめる。

爆発

劇中テキストで爆発とういう単語が普通に使われている。黒色火薬(爆裂丸)出たてという文化レベルで爆発って現象は一般的ではないはず。

火魔法あたりにそれに準じる魔法があるのかもしれない。だとしたら爆裂丸の新規性は何?

それともそんな魔法は存在せず、採掘などで発生しやすい粉塵爆発あたりから割りと爆発は身近?

新鉱山で見つかった物

引っ張ってるけどどうせ岩塩。

公国は新鉱山を占領すればいいだけとも思うが・・・後述するそもそも一夜王国包囲をどうやったんだ問題が謎すぎるけど、たしかにそれができるくらい戦力仕込んでおけるなら妥当かも?

海はないけど3国より以前の歴史は?

この舞台での人類史はどうなってるんだろう。海の向こうから人類がきたような設定ではない?

「塩がもっとあれば保存食を用意できそうなれば教国の砂漠の更に先や大瀑布の先の開拓ができる」といった旨の話が出ているので話の展開次第では海にたどり着く?

とはいえ海からの塩の調達は気候に恵まれるか大量の燃料が必要だとは聞く。

塩鉄大戦におけるウォルホード家の立ち位置

公国と王国が手を結んで教国と対峙してるさなかに王国臣下筆頭が敵である教国と手を結ぶってどういう判断?裏切り者扱いされてもおかしくなさそう。とはいえ劇開始時みても相変わらず王国臣下筆頭扱いなのはなぜ?

当時はウォルホード領が王国配下でなく独立した国くらいでないと説明が想像できない。でもそこから劇開始時のような臣下筆頭として扱われるのはかなり厳しそうなので、この仮定はかなり薄い線だと思ってる。

エスフロストによる一夜で王都包囲どうやったの?

  • 包囲できるほどの伏兵を仕込んでた?
    • 無茶すぎるけどリアリティライン下げたならそれっぽい説明になる?
  • 川下りによる急襲?
    • 相当の船数になりそうだしそれなら先に水門戦では?一晩とはいかなそう
  • 陸地からの進行はもっと無理そう

ローゼル

ローゼル族が迫害される理由がある?象徴的な身体的特徴を持つ少数民族が迫害されやすいとはいえなにかきっかけは?

塩湖に住んでいたと書かれているだけで教国より先住民ってわけではない?教国との歴史は?

製塩技術は教国で秘匿されてるって話だしローゼル族はそれは知らなそう?知ってたら公国王国で厚遇されそう。

今マイクラMODのマネタイズで起きている問題

MinecraftのModをホスティングしているサービスCurseForgeがまた炎上しているけどちょっと複雑そうだし面白そうなのでメモ。

背景

CurseForgeはModのホスティングだけではなくMod/Modpackの管理ソフトウェアも配布している。このソフトウェア上の広告で収入を得ている。とは言え、管理ソフトとして機能が足りなかったり、余計なソフトウェア(OverWolf)が付いてくるので、サードパーティ管理ソフトも人気がある。

またCurseForgeはMod製作者へModダウンロード数に応じてこの先述の収益の一部を還元している。今回はこのあたりが話の発端となっている。

問題のきっかけ

CurseForgeは新しいAPIを公開と同時にModの配布設定としてサードパーティー管理ソフトからのダウンロードを拒否できる設定ができるようになった。

https://github.com/PolyMC/PolyMC/issues/593#issuecomment-1130564447

Allow the distribution of this project outside the CurseForge-Overwolf ecosystem.

Note: download outside the CurseForge-Overwolf ecosystem do not count toward the reward program.

「公式クライアント以外からのダウンロードを許可する?そのダウンロードは報奨金の対象にならんけど」というチェックボックスができた。これをチェックしない限りサードパーティー管理ソフトからのダウンロードができないようになった。

この管理画面を見たことがないが、デフォルトオンのオプトアウト方式らしくMod製作者が明示的に操作しない限りはサードパーティー管理ソフトからのダウンロードはできるはずである。が、数十数百のModで構成されるModpackは高確率でサードパーティークライアントを拒否するModを含んでいるため、サードパーティー管理ソフトではModpackで遊べなくなってしまっている。

サードパーティークライアントの反応

APIへの対応に合わせてこのダウンロード制限により、サードパーティー管理ソフトの対応は様々だった。

MultiMC

https://github.com/MultiMC/Launcher/commit/0a827ba70e6ef20187f8507a536d54a8441020dc

This has been requested by Slowpoke, on behalf of both FTB and OverWolf.

Import from locally installed packs from the official clients will be the replacement, but for now, you will have to do that manually.

It was nice while it lasted.

CurseForgeの新API対応にいち早く対応したがサードパーティー制限の問題をうけてかCurseForge対応自体を消してしまった。

ここに出てくるSlowpokeとは多分FTB創始者?のSlowpoke101氏かな?

https://twitter.com/Slowpoke101

PolyMC

APIに対応したあとサードパーティー制限問題への対応は2つ

参考: https://github.com/PolyMC/PolyMC/issues/593

GDLauncher

APIに対応したあとこのサードパーティー制限に関しては特に対応してなさそう?

https://github.com/gorilla-devs/GDLauncher/issues/1342

ちょっとエラーがわかりにくい。

面白いなと思った点

twitterの公式クライアントは広告収入などのためにサードパーティークライアントへの制限を強いてる印象があるけれどこれはやはりサービスホストへの批判がたくさん起きている。

CurseForgeの件では意図してるかわからないが、「Mod製作者」と「プレイヤー」という同じサービスユーザーの分断をし、槍玉に上がりがちなホスティングサービスへの批判をいくらか逸らす役目をしている。

実際、現状でサードパーティー管理ソフトを使って困っているプレイヤーが取れる行動としては、1) 諦めてCurseForge公式管理ソフトをつかう 2) Mod製作者にサードパーティーダウンロードを許可するようお願いして回る、くらいかな?

Mod製作者の中にはこれらの動きを受けてCurseForge以外のプラットフォームにも配布を始めているケースがある。

https://www.reddit.com/r/feedthebeast/comments/utnrlu/cofh_mods_are_available_on_modrinth/

しかしCurseForgeと収益モデルを変えていかない限りは、今回の問題再生産をしているだけなようにも思える。