Unformed Building

Project ZomboidのModを作った話と重複アイテムの並べ替え処理

公開:

パーマリンク

昨年後半あたりから「Project Zomboid」というゲームをやっていました。ゲーム自体は2014年に購入して数百時間はプレイしていたのですが、ひさびさに再開したという感じです。

それはいいのですが、アイテム収集癖のあるわたしはアイテムの管理に苦労していました。
このゲームのアイテムはカテゴリーや名前でソートできるのですが、同じ名前のアイテムはスタックされて、その中身はソートできません。
耐久値などのパラメーターが存在するアイテムの場合、同じ名前でも状態が異なるのです。
拾ったアイテムなどは最初から状態がバラバラなこともあって、雑に突っ込んでいるとアイテムを取り出すときに状態を確認して選び出すという行動を取らなくてはなりません。
それが面倒くさくて、重複アイテムを出し入れして並べ替えるというModを作ることにしました。

アイテムを収納している箱やバッグから、一度プレイヤーキャラクターにアイテムを移し替える必要がある関係上、キャラクターが持っているアイテムは並べ替えられないなどの制限が発生してしまいましたが、どうにかModは完成し、2月27日に公開できました。
それから何度かアップデートして、3月12日版でひとまずやりたかったことは終わり、現在に至ります。

Steam Workshop::Reorder Duplicates by Condition

ありがたいことに2000人以上にも使っていただいており、作ってよかったと思っています。

開発中の話

初めてLuaを書きましたが、とりあえずエラーなく動いているので大丈夫だと思います……というか、基本的にifforくらいしか使っていません。
どちらかというとゲームのAPIのほうが苦戦しました。
APIの非公式Javadocはあるのですが、説明とかがあるわけではないので、それっぽいものを探して試すを繰り返しました。

並べ替え処理の話

たとえば「包丁」という武器カテゴリーアイテムが複数ある場合は次のような表示になります。

▶ 包丁 (5)

これをクリックするとスタックが展開されます。

▶ 包丁 (5)
包丁
包丁
包丁
包丁
包丁

その際に、耐久値などのバーが表示されますが、パラメーターによってはマウスオーバーでツールチップを表示させなければなりません。

このゲームの重複アイテムスタックは任意のアイテムを取り出せますが、スタックに突っ込むときは最後にしか入れられません。
例えば上記の包丁の場合、3番めのアイテムを取り出すのは問題ありませんが、戻したときは5番目になります。

Mod作成の場合、収納の中身を並べ替えてデータを書き換えるという処理も可能ですが、若干チート気味なので個人的には避けたく、バニラで可能な動作のみで完結させたかったのです。
チートっぽいと感じるのは、アイテムの取り出しや収納にはゲーム内時間が経過しますが、一気に書き換えるとノータイムで並べ替えが完了するからです。

この最後にしか追加できないという状況で、最少の手順をどうやって考えればいいのかというのが課題でした。
一番最初に出したバージョンでは、たとえば耐久値が「低 → 高」の順で並べ替える際に、最低値以外はすべて並べ替えるという処理をしていました。目的は達成できますが、とても無駄が多かったです。

ちなみにアイテムスタックの中身を並べ替える場合、キャラクターに次の動作を行わせます。
これはバニラの状態で手作業でやるものですが、これをModで自動化したいというわけです。

  1. スタックから任意のアイテムを取り出す(ゲーム内時間が経過)
  2. 取り出したアイテムを元のスタックがある収納に入れる(ゲーム内時間が経過)

これを耐久値などの、アイテムカテゴリーごとの重要パラメーターを見ながら行います。
アイテム数が10未満だと手作業でも大変ではないですが、そもそもアイテムの種類が多いのと、わたしのような収集癖があるプレイヤーは50個以上のスタックがあってもおかしくありません。
また、マルチプレイの場合は複数人分のアイテムスタックも存在するでしょう。
(Modの説明にはマルチプレイでテストしていないと書いていますが、サーバー機能は使っていないので普通に動くはずです)

最少手順を考えなければ、先に書いたように最低または最高の状態以外のアイテムを全部並べ替えれば完了です。
しかし実際に使ってみると「このキャラクター手際が悪いな」と感じます。

たとえば次のようなアイテムスタックがあるとします。
展開したアイテムに付いている角括弧内の数値は耐久値だと考えてください。

▶ 包丁 (5)
包丁 [1]
包丁 [3]
包丁 [10]
包丁 [10]
包丁 [5]

このとき、耐久値が低い順に並べ替えるなら、耐久値が10のアイテム2つを取り出して戻せばいいだけです。
ところが何も考えていない場合、耐久値が1以外のアイテムを3 5 10 10 の順で取り出して戻すことになります。
手際が悪いです。

結局どうしたとかというと、並べ替えるアイテムを選択するためのデータを作成して、それで必要分を選び出すという処理をしました。

まず、元の並びのアイテムスタックのデータを取り出し、次のように変換します。
(アイテムスタックのデータを取り出すのもちょっとややこしいのですがスキップします)。

{
  {
    condition = 耐久値,
    index = 元の並びのインデックス,
    item = {アイテムのデータテーブル}
  },
  ...
}

ここでは耐久値ですが、他にも色々なパラメーターを利用しています。それらの値はメソッドで取得するので、この時点で取得して適当なキーに値を入れておきます。
次は値ごとにアイテムをまとめます。

{
  [耐久値] = {
    value = 耐久値,
    last = 該当耐久値が一致する最後のアイテムのインデックス,
    items = {各アイテムのデータテーブル}
  },
  ...
}

ここまで終わったら、昇順または降順に応じて、上記のテーブルを耐久値で並べ替えます。これはsortを使うだけなので難しくありません。

問題の並べ替えるべきアイテムのフィルタリングは次のようにしています。

local function createTransferItems(data, itemsCount)
    local transferItems = {}
    local previousLastIndex = 0
    for index, baseDataItems in ipairs(data) do
        for _, itemData in pairs(baseDataItems.items) do
            if itemData.index < previousLastIndex + 1 then
                if index > 1 then
                    table.insert(transferItems, itemData.item)
                end
            end
        end
        if #transferItems > 1 then
            previousLastIndex = itemsCount
        elseif previousLastIndex < baseDataItems.last then
            previousLastIndex = baseDataItems.last
        end
    end
    return transferItems
end

引数dataには、前述の値ごとにまとめたアイテムのテーブルを渡します。itemsCountはそのままアイテム数です。
やっていることですが、たとえば低い順に並べ替える場合、現在の耐久値より低い耐久値のアイテムテーブルの最後のインデックスより前にある現在の耐久値のアイテムは並べ替えるべき、という感じです。

あとはこのフィルタリングしたデータにそって取り出して戻すという処理をキューに追加するだけです。

もしかするともっといい方法があるのかもしれませんが、わたしが思いついた方法は以上です。

ModのソースはGitHubにあるので、興味のある方は見てみてください

matori/pzmod-reorder-duplicates-by-condition