Valheimの実績「調理師(The Cook)」には、日本語で「料理を100個作る。」と書かれている。

実績解除を狙って料理を作っていたが、どうもカウントが合わない。ロックス肉パイを焼いても、野ウサギの肉を焼いても、パンを焼いても実績が進まなかった。

肉を焼けば料理スキルの経験値は上がる。それなのに「調理師」のカウントは増えない。

ゲーム上では料理として扱われているはずなのに、実績では数えられていないように見える。何を基準にカウントしているのか気になり、Valheim本体の assembly_valheim.dll を調べた。

結論からいうと、「調理師」が見ているのは料理スキルでも、完成した料理の総数でもない。実績用の PlayerStatType.CraftFood という統計値だ。

通常クラフトでは、完成品の ItemTypeConsumable なら基本的に CraftFood へ加算される。一方、肉焼き器や石窯は CookingStation という別の処理を通るため、通常クラフトの CraftFood 判定には入らない。

さらに CraftFood の対象になる料理でも、完成した個数ではなくクラフトした回数が加算されていた。

コードにはクラフト方法やアイテム種別ごとの分岐が明示的に用意されている。私は、この挙動を単純な実績カウントのバグではなく、現在の仕様として実装されているものだと考えている。

「調理師」が進む料理と、進まない料理

先に実際の挙動をまとめると、違いはかなり大きい。

料理・工程作り方「調理師」
ニンジンのスープ大鍋などの通常クラフト+1
女王のジャム大鍋などの通常クラフト+1
ソーセージ大鍋などの通常クラフト+1
サラダ大鍋などの通常クラフト+1
未焼きロックスパイ通常クラフトで中間素材を作る+0
パン生地通常クラフトで中間素材を作る+0
ロックス肉パイ石窯で焼く+0
パン石窯で焼く+0
焼いた野ウサギの肉CookingStation 系で焼く+0

大鍋などの通常クラフトで、そのまま食べられる Consumable が完成する場合は CraftFood が増える。

一方で、石窯や肉焼き器で完成させる料理は別の処理を通る。未焼きロックスパイやパン生地のような中間素材も CraftFood には入らない。

石窯や肉焼き器で完成する料理が CraftFood に入らないのは、この処理経路の違いによる。

料理スキルが上がっても「調理師」が進むとは限らない

肉を焼くと料理スキルの経験値は入る。それでも「調理師」は進まない。

料理スキルの経験値と「調理師」の進行は、別の条件で加算されている。

「料理として経験値が入るか」と「CraftFood に加算されるか」は別の判定だ。肉を焼いて料理スキルの経験値が入っても、それだけでは CraftFood は増えない。

assembly_valheim.dll を調べた

今回解析したのは、Steam版Valheim本体に含まれる assembly_valheim.dll だ。

同じ処理を確認するなら、SteamからValheimのローカルファイルを開き、次のDLLをILSpyやdnSpyExでデコンパイルすればよい。

Valheim/
└─ valheim_Data/
   └─ Managed/
      └─ assembly_valheim.dll

解析したDLLは次のものだった。

項目
Valheim1.0.12
ファイルassembly_valheim.dll
サイズ2,568,192 bytes
SHA-25627a766a8d23a7bd8b6a54fb9ad0452a96c305fb3629b39c40527c09a1c393a84

通常クラフトの統計処理は InventoryGui.DoCrafting() にある。

DLLのメタデータとILから確認した位置は次のとおり。

対象
InventoryGuiTypeDef RID 164
InventoryGui.DoCraftingMethodDef RID 2584
RVA0x59B18
IL code size2520 bytes

ILSpyで確認するだけならRIDやRVAを意識する必要はない。InventoryGui を検索して DoCrafting を開き、その中から CraftFood を探す方が早い。

通常クラフトでは ItemType ごとに統計が分かれる

DoCrafting() ではレシピから完成品をたどり、m_itemType を取得している。

Recipe.m_item

ItemDrop.m_itemData

SharedData.m_itemType

ItemTypeごとに統計を振り分ける

実ILのうち ConsumableMaterial に関係する部分を、説明用のC#に置き換えると概ね次の処理になる。ゲームのソースコードをそのまま掲載したものではない。

var itemType = m_craftRecipe.m_item.m_itemData.m_shared.m_itemType;

switch (itemType)
{
    case ItemType.Material:
        IncrementStat(PlayerStatType.CraftMaterial, craftCount, cheated);
        break;

    case ItemType.Consumable:
        if (itemName.ToLower().Contains("bait"))
        {
            IncrementStat(PlayerStatType.CraftBait, craftCount, cheated);
        }
        else
        {
            IncrementStat(PlayerStatType.CraftFood, craftCount, cheated);
        }
        break;
}

ItemType.Consumable の完成品は基本的に CraftFood へ入る。ただし、内部名に bait を含むものは CraftBait へ分けられる。

ItemType.Material の完成品は CraftMaterial へ入る。未焼きロックスパイのように、あとで焼けば料理になるものでも、通常クラフトで作った時点では CraftFood に加算されない。

同じ処理では、料理以外のクラフト統計も振り分けられている。

PlayerStatType
CraftFood109
CraftFoodBonus110
CraftGrill111
CraftGrillBurnt112
CraftGrillBonus113
CraftWeapon114
CraftArmor115
CraftTrinket116
CraftAmmo117
CraftMaterial118
CraftTool119
CraftTorch120
CraftBait121
CraftOther122

CraftFood は、クラフトした完成品を種類別の統計へ振り分ける処理の一つとして用意されている。

未焼きロックスパイは CraftFood ではなく CraftMaterial

未焼きロックスパイは、この分類が分かりやすく表れる例だ。

料理工程の途中で作るものだが、通常クラフトで完成した時点の ItemTypeMaterial になっている。

未焼きロックスパイを作る

ItemType.Material

CraftMaterial +1

CraftFood +0

通常クラフトの分類では、あとで食べられる料理になるかどうかは見ていない。作ったアイテムが Material なので CraftMaterial に入る。

その未焼きロックスパイを石窯に入れてロックス肉パイへ完成させると、今度は InventoryGui.DoCrafting() ではなく CookingStation 側の処理へ移る。

肉焼き器や石窯は CookingStation の別処理

ロックス肉パイ、パン、焼いた肉などが「調理師」に入らない理由を考えるうえで重要なのが CookingStation だ。

肉焼き器や石窯は InventoryGui.DoCrafting() を通らない。通常クラフトの ItemType.Consumable → CraftFood という分岐そのものを使っていない。

CookingStation には、加算する統計を指定するフィールドがある。

PlayerStatType m_cookStat;
PlayerStatType m_burntStat;
PlayerStatType m_bonusStat;

統計を増やすタイミングも通常クラフトとは異なる。

CookingStation.UpdateCooking() は調理中・完成・焦げた状態を更新するが、料理が完成した瞬間には統計を増やさない。プレイヤーが完成品を取り出す CookingStation.OnInteract() 側で統計を加算している。

調理開始

CookingStation.UpdateCooking()

完成状態になる

この時点では統計を加算しない

プレイヤーが完成品を回収

CookingStation.OnInteract()

Prefabで指定された m_cookStat +1

料理スキルのボーナスによって完成品が2個になった場合も、m_cookStat は完成個数に合わせて +2 にはならない。m_cookStat+1、ボーナス用の m_bonusStat+1 される構造になっている。

PlayerStatType には CraftGrillCraftGrillBurntCraftGrillBonus が定義されている。通常クラフトの CraftFood とは別の統計を使える設計だ。

ただし CookingStation クラス上では、m_cookStat などの初期値は None になっている。肉焼き器や石窯のPrefabに実際に何が設定されているかは、Unity側のシリアライズデータを見ないと直接は確認できない。

今回のDLLだけで確認できたのは、CookingStation がPrefabで指定された統計を、完成品の回収時に加算するところまでとなる。

一方、実際のゲームではロックス肉パイ、パン、焼いた野ウサギの肉を完成させても「調理師」は進まなかった。少なくとも、これらの処理が通常クラフトの CraftFood と同じ扱いではないことは挙動とも一致している。

CraftFood は完成個数ではなくクラフト回数を数える

通常クラフト側の CraftFood では、完成個数とクラフト回数が別の値として扱われている。

DoCrafting() では、まずクラフト回数を表す craftCount を作っている。

var craftCount = m_multiCrafting
    ? m_multiCraftAmount
    : 1;

実際に生成するアイテム数は別に計算される。概念的には次の関係になる。

var outputAmount = recipe.m_amount * craftCount;

ところが CraftFood に渡している値は outputAmount ではない。統計には craftCount をそのまま渡している。

IncrementStat(
    PlayerStatType.CraftFood,
    (float)craftCount,
    cheated
);

つまり、CraftFood の対象になる料理でも「何個完成したか」ではなく「何回クラフトしたか」を数えている。

1回のクラフトで複数個完成しても、実績は+1

女王のジャムは1回のクラフトで4個完成する。それでも CraftFood+1 だ。

女王のジャムを1回クラフト

料理が4個完成

CraftFood +1

5回まとめてクラフトすれば、完成するジャムは20個で CraftFood+5 になる。

1回の完成数: 4個
クラフト回数: 5回

完成する料理: 20個
CraftFood: +5

ここで数えているのは20個の完成品ではなく、5回のクラフトだ。

料理スキルのボーナスで完成個数が増えても加算量は変わらない

料理スキルのボーナスで完成個数が増えた場合も CraftFood の加算量は変わらない。

1回のクラフトで本来4個できる料理がボーナスによって5個になっても、craftCount は1なので CraftFood+1 のままだ。

1回クラフト

通常の完成数 4個
    +
料理スキルのボーナス 1個

料理を5個入手

CraftFood +1

この話は、石窯や肉焼き器が実績に入らない問題とは別だ。

石窯や肉焼き器は、そもそも通常クラフトの CraftFood 判定を通らない。一方、大鍋などで CraftFood の対象になる料理は、完成個数ではなくクラフト回数で数えられる。

「調理師」の解除条件は CraftFood >= 100

実績側も確認すると、「調理師」が参照している統計は CraftFood だった。

解除条件は CraftFood が100以上になることだ。

通常クラフト

完成品が Consumable

CraftFood += craftCount

CraftFood >= 100

「調理師(The Cook)」の条件を満たす

英語版でも「100回料理する」とは書かれていない

日本語の説明は「料理を100個作る。」、英語では Cook 100 dishes. となっている。

英語の dishes も料理そのものを数える表現なので、少なくとも「100回料理する」と明記しているわけではない。日本語だけが誤って「100個」と訳した、という話ではなかった。

さらに Cook という表現からは、肉焼き器で肉を焼いたり石窯でパンを焼いたりする行為が対象外になることも読み取れない。

つまり、表示言語の問題というより、実績の説明文から想像する条件と、実際に CraftFood へ加算される条件が一致していない

条件分岐を見る限り、カウントバグではなく仕様だと考えている

最初は、料理をしているのに実績が進まないためカウントバグも考えた。

しかしコードを見ると、通常クラフトでは完成品の ItemType に応じて CraftFoodCraftMaterialCraftBait などへ明示的に振り分けている。

肉焼き器や石窯は通常クラフトとは別の CookingStation を通り、専用の統計フィールドまで用意されている。さらに CraftFood の加算値も、完成個数ではなく craftCount を明示的に渡している。

ここまで条件ごとに処理が分かれているため、私は現在の「調理師」を**CraftFood に分類される通常クラフトを100回行う実績として実装されている**ものだと考えている。

日本語だけでなく英語版の説明からも、この範囲を読み取るのは難しい。特に、料理スキルの経験値まで入る肉焼きが実績では対象外なのは、実際に数字を見ないとなかなか気づけない。

まとめ

「調理師(The Cook)」が見ているのは、料理スキルの経験値でも、完成した料理の総数でもない。実績用の CraftFood だ。

料理スキルの経験値が入る料理
        = 「調理師」の対象
        ではない

すべての完成した料理の個数
        = 「調理師」のカウント
        でもない

「調理師」が見ているもの
        = CraftFood

石窯や肉焼き器は通常クラフトとは別の処理を通り、CraftFood の対象になる通常クラフトでも完成個数ではなくクラフト回数が加算される。該当する料理を100回作る必要があり、料理スキルのボーナスや複数個完成するレシピで必要回数を短縮することはできない。

日本語の「料理を100個作る。」だけが原因ではない。英語版も Cook 100 dishes. なので、実装上の限定されたカウント条件は説明文からは分からない。

参考

Steam上の英語表記は The Cook、説明は Cook 100 dishes. になっている。

Jotunnがゲームデータから抽出した英語ローカライズでも、$ach_grindcook_descCook 100 dishes. になっている。

日本語ローカライズでは、同じキーが「料理を100個作る。」になっている。

アイテムのPrefab名や分類を確認するときは、JotunnのObject DB一覧も参照できる。