'static + Life

運良く生きています

楽しいHypixel: PaintBall編

この記事はKaigen Discord Advent Calendar 5日目の記事です。 PaintBall編とか言ってるけど続くかはわかりません。

前書き

この記事はHypixelのPaintBallの魅力と楽しみ方の一つを自分なりに伝えるための記事なのでそんな本格的なことは書かないつもりです。
あと(こんな紹介記事書いてるけど)別にPaintBallが上手いわけではないのでKillstreakの選び方やPerkの選び方などは参考程度に留めておいてください。

PaintBallってなんぞや

(知ってる人は飛ばして構いません)
ゲームルールは

"2チームに分かれて雪玉投げあって相手のチームの残機を先に0にしたチームの勝利"

…これだけ見るとただの雪合戦と何が違うのか, そしてなんの面白さがあるのかわかりにくいかもしれません。
普通のPaintBallならつまらないでしょうけどここはあの"Hypixel", 普通ではないんです。
じゃあ何が違うのか?説明していきましょう。

豊富なShop

まずはHypixel恒例のShopを見ていきましょう。
Classic Games Lobbyへ行きPaintBallのShopを覗いてみます。 f:id:KashEight:20181201020354p:plain
5つの項目がありますが特に重要なのは上の3つです。それぞれ見ていきましょう。

Hat

f:id:KashEight:20181201020642p:plain
(テクスチャバグっててごめんなさい)
見る限りたくさんあって頭(Hatだけに)が混乱するかと思いますがこれらは大きく2種類にわけることができます。
一つは安価で購入できるBlockHat, もう一つは高価なSkullHatです。(BlockHat, SkullHatはそれぞれわかりやすくするために自分がつけた名称なので正しい名称ではないのでご注意を)
この二つの説明の前にまずはHatの説明をしましょう。

Hatとは

Hatは購入して装着することで試合にてHatに応じた効果を得ることができます。ただエフェクトを出す(意味のないネタ)Hatやゲームに大きな影響をもたらすHatまで多種多様なHatがこのPaintBallでは存在しており, ゲームをプレイする上では必要不可欠な存在です。
Hatを装着するにはShopで, 購入したHatをクリックするだけで装着, 次の試合から効果を得られます。逆に言えば装着するまでその効果は得られないということです。
勿論装着できるHatは一人一つまで, でないとゲームバランスは崩壊するからね。(元から壊れてるとか言わない)

このHatの量を見て大抵の方は「けどこんなたくさんの中から一つとか選べないよぉ~><」ってなると思います。
そこでBlockHat, SkullHatと分け, それぞれの中で必要なものをピックアップしましょう。まずはこの二種類の説明から。

BlockHatはブロック型のHatで安価(4,200コイン)で購入できるものです。コインがない序盤に購入しやすいです。
一方, SkullHatはプレイヤーの頭の形をしたHatでかなり高価(50,000/75,000コイン)です。クールタイムがありますがその分価値にあった性能を出してくれるのでコインが溜まったら買うのが良いと思います。

次にこの二種類の中から必要なものを紹介します。

BlockHat

  • Speed Hat
    -> 常時Speed2が付与, PaintBallのマップは広いので速く移動できるのは試合の勝敗を大きく分けます。

BlockHatはこの一つだけで十分です。というかこれを選ばないでどう試合すればいいかレベルなので必ず買いましょう。

SkullHat

  • Rezzus Hat
    -> スニークすると5秒間雪玉にホーミング性能がつく, クールタイムは70秒
  • Paintballkitty Hat
    -> 常時Speed2が付与, スニークすることで5秒間Speed3が付与される, クールタイムは70秒

SkullHatは基本的に高いのもあって最初のうちは手を出すことは難しいかもしれません。最初のうちはSpeed Hatでプレイしてコインに余裕が出てきて買おうと思ったらこの二つのどちらかを買えばいいと思います。
私のオススメは二番目のPaintballkitty Hatです。Speed Hatの上位互換版なのでHatを変えても違和感は全く無いです。
Rezzus HatはSpeed2は付与されませんが雪玉のホーミング性能が最強です。狭いマップではこれに敵うHatはないでしょう。

Perk

f:id:KashEight:20181201030640p:plain
Hatとは違って一気に数が少なくなりました。
けれどもPaintBallの中で一番重要な部分であり一番コインを必要とするのがこのPerkです。
ではこのPerkとはなにか, そしてどれを選べばいいのかこれから説明します。

Perkとは

Perkはレベル制となっておりHatとは違って買い切りではありません。
レベルの上限はそれぞれのPerkによって違い, 短いもので5レベル, 長いもので50レベルまであります。
全部上限まで開放するのはかなりの時間をPaintBallに捧げる必要はあるので現実的ではありません, そこでこの中からHatのように必要なものをピックアップしましょう。

  • Fortune
    -> 敵をKillした際にKillstreakをさらに1個貰える確率が増える。一レベル上げるごとに5%増加。上限は20レベル(100%)
  • Superluck
    -> 敵をKillした際に雪玉を10個貰える確率が増加する。一レベル上げるごとに5%増加。上限は20レベル(100%)
  • Headstart
    -> 初めから持っているKillCoinを増やすことができる。一レベル上げるごとに1KillCoinを得る。上限は5レベル(5KillCoins)

他にもGodfatherやEnduranceがありますがとりあえずこの三つを上限にすることを目標にすればいいかと思います。
どれを先に上げるかですが自分はFortune, Headstart, Superluckの順にあげるのがおすすめです。どのPerkにも言えることですが買うごとに値段は上昇していくため最適解としては

Fortune(途中まで) -> Headstart(3か4レベルまで) -> Superluck -> Fortune(最後まで)

が良いかもしれませんね。

Killstreak

f:id:KashEight:20181201034039p:plain
数が多くなりましたが, これも同じように説明していきましょう。

Killstreakとは

敵をKillしたときにもらえるKillCoinを使って使用できるスキルです。雪玉を遠くに速く投げることができるKillstreakから敵を全滅させるKillstreakまで色々なKillstreakが存在します。
ちなみに, 先程の画像以外にも初めから所持しているKillstreakもあるので画像が全てのKillstreakというわけではないです。
試合で使えるKillstreakをピックアップしていきましょう。

  • Strong Arm
    -> 30秒間雪玉の飛ぶ速度が早くなり, 遠くに飛ぶようになる。消費KillCoinは4, しかも初めから所持している。
  • Triple Shot
    -> 30秒間投げた雪玉が3方向に飛ぶようになる。Strong Armと併用可。消費KillCoinは3
  • Nuke
    -> カウントダウンが始まり5秒後に「リスポーン時の無敵時間中以外」の敵を全員Killする。Killした敵一人ごとに1KillCoin手に入る。クールタイムは60秒, 消費KillCoinは25
  • +10
    -> 味方の残機を10増やす。初めから使える+3(消費KillCoinは5)の上位互換, これで勝敗が大きく分かれることがある。消費KillCoinは15
  • Force Field
    -> 15秒間無敵になる。コストが高いがその分敵をKillすれば元は取れる。ただしKillstreakを使った攻撃(TNT Rain, Revengeなど)には無敵はきかない。クールタイムは60秒?(情報がないのでわからない), 消費KillCoinは50

マップにもよるけどほとんどのマップで使えるKillstreakを列挙しました。これ以外にもLightningやEnder Pearlと言ったマップによってはかなり強いKillstreakもあるので適宜使い分けよう。特にNukeは狭いマップやプレイヤーが少ない試合では効果を発揮できないことが多いです。

豊富なマップ

HypixelのPaintBallのマップは少人数用から大人数用まで色々なマップが存在しています。
マップは人の好みが分かれますがとりあえずの参考として自分の好きなマップを上げたいと思います。

  • Mansion
    -> 少人数マップ, かなり狭いのもあってLightningが大いに活躍できる。
  • Herobrine
    -> 大人数マップ, リスキル等でここで100Killを叩き出すことはよくある。自分も何回か出した。
  • Juice
    -> 少人数マップ, Lightningが活躍できるので楽しい。
  • Victorian
    -> 大人数マップ, Herobrineと同じくリスキルができる。

初心者のうちはマップに慣れないだろうけど慣れてきたらどのマップに自分は合ってるかわかるので是非ともいろんなマップに挑戦していただきたい。
まずは試合でKill数1位を目指して頑張ろう!

まとめ

PaintBallは初心者には取っ付きにくいですがコツさえ掴めば誰でもできるものです。
自分も最初はKillされてばかりでしたがやっていくうちに逆にKillをする方になっていたのでどんなことにも当てはまりますが徐々に慣れていくことが大切だと思います。
もしPaintBallに興味を持ったならば是非とも参加してみてください!というかしてほしい, ただでさえ人が少ないから。

kivyでシステムフォントを使用できるようにする

デフォルトでシステムフォントを使用したい場合

from kivy.resources import resource_add_path
from kivy.core.text import LabelBase, DEFAULT_FONT

resource_add_path("<システムフォントのパス>")
LabelBase.register(DEFAULT_FONT, "<使用したいフォントのファイル名>")

適宜システムフォントを使用したい場合

今回はLabelのみで行う。

kvlangなし:

from kivy.app import App
from kivy.uix.label import Label

class MainApp(App):
    def build(self):
        return Label(text="hoge", font_name="<使用したいフォントのファイル名>")

kvlangあり:
(main.py)

from kivy.app import App

class MainApp(App):
    pass

(main.kv)

Label
    text: "hoge"
    font_name: "<使用したいフォントのファイル名>"

KivyでPythonのみ使用して色んなものを実装する

Kv LanguageをPython側でどう処理されてるか見るためにPythonのみで色々実装しようとしたら失敗続きで2日程悩んだので自戒をこめて書いとく。

Button実装

main.py:

from kivy.app import App
from kivy.uix.button import Button


class MainApp(App)
    def build(self):
        pass

main.kv:

Button:
    text: "hoge"

from kivy.app import App


class MainApp(App):
    def build(self):
        return Button(text="hoge")

と書き換えられる。

Labelも同じ感じなので割愛

Buttonを押したときのEvent処理

Python Kivyの使い方① ~Kv Languageの基本~の10項を例にする。
今回はクラスを新たに作らず全てMainAppクラス内で済ませるようにした。(正直、新しくクラス作ったほうが可読性は上がる)

main.py:

from kivy.app import App
from kivy.properties import StringProperty


class MainApp(App):
    text = StringProperty()
    btn_text = StringProperty()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = ""
        self.btn_text = "Button"

    def build(self):
        pass

    def pressed(self):
        if self.text != "":
            self.text = ""
            self.btn_text = "Blank"
        else:
            self.text = "Hello World"
            self.btn_text = "Changed"

main.kv:

BoxLayout:
    orientation: 'vertical'
    size: root.size

    Label:
        id: label1
        font_size: 68
        text: app.text

    Button:
        id: button1
        text: app.btn_text
        font_size: 68
        on_press: app.pressed()

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button

class MainApp(App):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.label = Label(text="", font_size=48)
        self.button = Button(text="Button", font_size=48, on_press=self.pressed)

    def pressed(self, *args):
        if self.label.text != "":
            self.label.text = ""
            self.button.text = "Blank"
        else:
            self.label.text = "Hello World"
            self.button.text = "Changed"

    def build(self):
        boxlay = BoxLayout(orientation="vertical")
        boxlay.add_widget(self.label)
        boxlay.add_widget(self.button)
        return boxlay

と書き換えられる。

ここで注意が2つある。

一つは

self.button = Button(text="Button", font_size=48, on_press=self.pressed)

on_press=self.pressed()とはしないこと。
この場合、Event: on_pressの値をcallback関数に指定する必要があり、self.pressed()だと返された(returnで返ってきた)値を代入してしまうこととなる。

もう一つは

def pressed(self, *args):

で必ず引数を(使わずとも)追加する必要がある。理由としては、pressed関数はcallback関数なので引数に自身のインスタンス(self)ともう一つ、呼んだ関数のインスタンス(この場合MainApp())を必要とする。そのため、pressed関数を利用するためにはこの場合2つの引数が必要となる。 呼んだ関数のインスタンスを利用したい場合は*argsinstanceとかに変えれば良い。使わないなら基本的に引数は*argsで十分。

参考:
公式リファレンス - Button (英語)

また、pressed関数に他の引数を追加したい場合はfunctoolsモジュールからpartialをimportして

self.button = Button(text="Button", font_size=48, on_press=partial(self.pressed, <arg1>, <arg2>...)

とする。(self.pressed(<arg1>, <arg2>...)は先述したとおりreturnされた値を代入してしまう。)
そしてpressed関数を

def pressed(self, instance, <arg1>, <arg2>, ...):

とする。

LabelまたはButtonのtextを変えたい場合は

self.label = Label(text="", font_size=48)
self.button = Button(text="Button", font_size=48, on_press=self.pressed)

のように変数を作り、

self.label.text = ""
self.button.text = "Blank"

のようにクラス変数の値を変えれば良い。

今回示した例とけっこう違う方法で実装することも可能なので、随時更新していきたい。

Pythonで指定されたディレクトリ内のファイルを条件付きで表記する

ちょっと考えてしまったのでメモ用に。
実行環境はPython 3.6.3

ファイル構成(rootは省く)

hoge ┳ fuwa.txt
     ┣ foo
     ┃ ┣ spam.txt 
     ┃ ┗ ham.txt 
     ┗ bar 
        ┗ test.txt

ジェネレータの作成

import os

def getTree(root: dir=None):
    for r, _, f in os.walk(root):  #os.walkで親ディレクトリ下のディレクトリ(今回は使わない), ファイルの参照
        if r == root:  #パスが親ディレクトリのパスと同値か判定("\"が含まれないようにする)
            for files in f:
                yield os.path.join(r.replace(root, ""), files)  #親ディレクトリのパスを削除
        else:
            for files in f:
                 yield os.path.join(r.replace(root + "\\", ""), files)

一応動作を確認してみる。(今回はDesktopにhogeフォルダを作成したと仮定)

p = "C:\\Users\\<User>\\Desktop\\hoge"
gen = getTree(p)

for path in gen:
    print(path)

実行結果:

fuwa.txt
bar\test.txt
foo\ham.txt
foo\spam.txt

取り出すファイルに条件をつける

今回はfoo\以外を取り出す。

p = "C:\\Users\\<User>\\Desktop\\hoge"
gen = getTree(p)

for path in gen:
    parsed_path = path.split("\\")  # pathを"\"で区切る
    parsed_path[:] = [p for p in parsed_path if "foo" not in parsed_path[0]]  #parseしたpathの最初にfooがあった場合リストを空にする
    if parsed_path:  # 存在するリストか確認
        print(path)

実行結果:

fuwa.txt
bar\test.txt

数個ぐらいのファイルだったらリストを上手く使ってできるけども大量のファイルが存在する場合大量のメモリを使用するので、ジェネレータを使用したものを利用した。除くファイルや入れたいファイルがある場合、 parsed_path[:] = [p for p in parsed_path if <条件式>] を上手に利用してください。
あと、これ以上にいいやり方あったら教えてください。

PAYDAY2 Moddingから学ぶゲーム解析 #2

この記事はLava Bucket Advent Calendar 2017 10日目の記事です。
9日目の記事を先に見たことを前提として書いていきます。

Luaスクリプトを覗いてみる

Luaスクリプト群はcore/liblib/に存在しています。前者はこのゲームの基礎の基礎となる部分、つまりDiesel Engine*1の中心スクリプトとなる部分です。後者はこのゲーム自体のスクリプトとなる部分です。
さて、この中にLuaスクリプトが入ってるわけなのでそれを見ようと思いますが…。

f:id:KashEight:20171203192208p:plain (core/lib/utils/coreapp.luaの例)

あれ?
運の悪いことに.luaテキストエディタで開けません、どういうことでしょうか?
実はこのluaファイルはコンパイル済みであり、通常のテキストエディタで開くことはできません。なので、これらのLuaファイルをデコンパイルして読まなければなりません。
これを全てデコンパイルするのはかなり時間かかるのと手間がかかりますので有志の方が公開しているリポジトリをダウンロードかクローンして覗いてみましょう。
これらからお好きな方を選んでください。

  • bitbucket (アップデートの反映は早い、ただしgitでのクローンは不可)

  • github (gitでのクローンは可能、ただしアップデートの反映は遅い)

これでもう一回同じファイルを見てみましょう。

f:id:KashEight:20171203194248p:plain

これにて正常に開けました。

Luaスクリプトから推察する

さて、先程見れるようになったLuaスクリプトですが、これからpayday2のシステムを(一部ですが)推察してみましょう。

とりあえずデータを見てみる

今回はpayday2自体を見るのでlib/内のフォルダを見ることにしましょう。

f:id:KashEight:20171203195400p:plain
(lib/の内部)
この中には膨大というわけでもありませんが多くの量のスクリプトがあるので今回は"ゲームデータ"関連のスクリプトに焦点を当てて見てみましょう。
lib/内部をすべて解説するほどの労力がない(ごめんなさい)ので目的のフォルダだけを挙げます。今回の目的のフォルダはtweak_dataフォルダです。
f:id:KashEight:20171208231735p:plain
(lib/tweak_dataの内部)
内部だけでも大量のluaファイルが存在するのでさらに焦点を当ててみましょう。"キャラクター関連のゲームデータ"を探そうと思います。

f:id:KashEight:20171208232328p:plain
(lib/tweak_data/charactertweakdata.lua)の内部
まず、1行目から見てましょう。

CharacterTweakData = CharacterTweakData or class()

ここの定義からCharacterTweakDataはクラスとして定義されていることがわかります。*2
次に4行目を見てみましょう。

function CharacterTweakData:init(tweak_data)
…

initの詳しい説明は省きますがこの関数(メソッド)はオブジェクト指向型プログラミングでいうコンストラクタみたいなものです。ここでは、CharacterTweakDataクラスが呼び出されたときの初期化処理を行っています。
余談ですが、今回initの詳しい説明を省いたのはinit自体は別のファイル(クラス)で定義はされており、それを解説するとキリがないことや今回の目的とは少し遠くなるので説明を省きました。また今度違う記事で説明するかもしれません。

さて、このクラスについて基本部分はある程度の説明できました。あとは、各種のメソッドの処理がどうなっているか調べるだけです。
この部分が理解できればあなたも立派なHeisterです。いえい!(あいにく私は理解できていませんですが。)
もうほとんど書ききってしまったのとこれ以上書くと記事が長くなるのでここでお終いとします。
ここまで見てくださりありがとうございました。
明日はNagisberryさんの"ラズライト ~天藍石と青金石の関係性~"です。一体どういう記事なんでしょうか、楽しみです。

注釈

*1:OVERKILL Softwareの前身となるGRIN(現在は閉鎖済み)が開発したゲームエンジン

*2:補足: 今回は省略していますが、Luaは最初からクラスが提供されているわけではないため、クラスとして扱うには連想配列を使って定義する必要があります。1行目のclass()関数はPAYDAY 2のLuaスクリプト側で定義されたグローバル関数であり、Luaが直接持っている関数ではないので注意が必要です。class()関数の詳細は都合により省きます。

PAYDAY2 Moddingから学ぶゲーム解析 #1

この記事はLava Bucket Advent Calendar 2017 9日目の記事です。

まず最初に

PAYDAY 2とは?Moddingとは?

他のプレイヤーやNPCと最大4人のチームを組み、銀行強盗などのお仕事をこなすFPSです。
以下のキーワードに反応した方は是非チェックを。
 強盗
 CO-OP
 オブジェクティブベース
 エレクトロニック・ダンス・ミュージック
Payday2 日本語 WikiFAQ - どんなゲームより

そういうことですので、気になった方は購入を。

・・・

冗談は置いといて、少し真面目な解説をしましょう。

PAYDAY 2とは、 OVERKILL Softwareが制作、Starbreeze Studioがパブリッシャーの最大4人で遊ぶことができるCO-OP*1ゲームです。引用に書いてあるように内容は銀行強盗などのお仕事を受けてオブジェクティブ(目標)を達成しながらクリアを目指すゲームです。特に音楽に関しては評価がかなり高く、有名アーティストがPAYDAY 2用の曲を作成するほどの人気があります。ゲーム自体はC/C++*2で書かれており内部スクリプトLua*3で書かれています。そのため、内部スクリプトLuaをいじることでゲームの挙動を変えることができます。これがModdingです。これからこのModdingという行為からゲームの挙動の予測、つまり、解析を学ぼうと思います。

ゲームデータを展開する

まずゲーム解析を行うためにはゲームデータを展開させないと意味ありません。
PAYDAY 2はC:\Program Files (x86)\Steam\steamapps\common\PAYDAY 2\assetsにゲームデータを.bundle拡張子に変換して配置し、本体に読み込ませられるようにしています。
f:id:KashEight:20171124214722p:plain
(assetsの中身、およそ40GB以上ある。)

通常、.bundle拡張子の中身は見ることや展開することは不可能ですが、有志の方が作成したツールを使用することでそれらをすることができます。それで使うツールはこれです。

f:id:KashEight:20171124220015p:plain

使い方

前述してあるURLをクリックし、ダウンロードページへ移動、下画像の赤丸で囲んである"Download"をクリックしダウンロード、そして好きな場所へ解凍します。フォルダ作ってそこに格納したほうがいいかもしれません。

f:id:KashEight:20171124220550p:plain

解凍したファイルの中からPDBundleModPatcher.exeを実行、ダイアログが出ると思いますが迷わず[OK]と押しましょう。
そうすると紹介時の画像ような画面が映るかと思います。
そこからGame File Extractionタブをクリックしましょう。するとこういう画面に移ると思います。

f:id:KashEight:20171124221302p:plain

ここでのCustom Extract Folderは、展開先をどこにするかの指定です。中央左のBrowseをクリックして指定できます。ゲームデータは40GBを余裕で超えるので別ドライブに展開したら良いでしょう。
また、Extention Replacementは特定のワードを置換する機能です。PAYDAY 2はデータを読み込ませるために独特の拡張子を使用しています。一部のデータは拡張子を変えるだけで見れるのでこの機能を使い拡張子を一気に変更します。今回は.unitファイルを.xmlに、.textureファイルを.ddsにします。追加方法はExtentionに置換元のワード、Replacementに置換後のワードを入力してAdd/Updateをクリックするだけで追加できます。削除は削除したいものをクリックし、Removeを押せば削除できます。

これで、展開する準備が完了しました。

…と思いきや、まだやることがあります。

理由

まず、先程の状態から展開をスタートすると、「ちゃんと展開されるし問題ないのでは?」と思うかもしれませんが、実は一部のものしか展開できていません。
では一体どのファイルができていないのでしょうか?

ここで、この記事を見てみましょう。
この記事はall_<number>_h.bundleを作成するPython*4スクリプトといういうことがわかります。
ここからはBundle Modderの内部の話になりますが、Bundle Modderは.bundleをhashlist*5を元にhashを作成、それを参照し展開するような形をとっています。
そのため、もしhashが上手く作成されていない場合、.bundleを正常に展開することはできません。
ということは、このスクリプトはall_<number>_h.bundleを正常に読み込ませるためのスクリプトっていうことがわかります。
このため、展開する前にファイルを修正する必要があります。

.bundleファイルの修正, 及び更新

Python 2.7またはPython 3.x環境が必要です。

はじめに、hashlistのリポジトリダウンロードします。

Python 2.7を使用している場合

  1. hashlist, bundle fixer.pyを解凍する。
  2. hashlistをBundle Modderにあるフォルダに移動する。
  3. bundle fixer.pyを実行、all_h.bundleが作成されていることを確認する。
  4. Bundle Modderを起動、ファイルを展開する。

Python 3.xを使用している場合

  1. hashlistを解凍する。
  2. hashlistをBundle Modderにあるフォルダに移動する。
  3. ここからコードをペーストし、.py拡張子にして実行、all_h.bundleが作成されていることを確認する。
  4. Bundle Modderを起動、ファイルを展開する。

以上でファイルの展開が完了します、正常に展開されれば下画像のように展開されているはずです。

f:id:KashEight:20171125203535p:plain

アップデートで追加されたファイルを展開したい場合、先程のリポジトリを再度ダウンロード、hashlistをBundle Modderが存在するフォルダに移動させ、bundle fixer.pyを実行すればall_h.bundleが更新されます。
そして、Bundle Modderを起動して展開を実行すれば新しいファイルが追加されます。(この場合Ignore existing filesにチェックを入れることをオススメします。展開するファイルが既に存在している場合、展開しない処理をするので早く終わらせることができます。)

#1はここまでです。payday2の内部データの説明、解析は明日の#2で紹介いたします。
ここまで見てくださりありがとうございました。

注釈

Payday2のunitデータを解析する

経緯

日本語化MODをBLT2.x系に対応する際に「pager応答の字幕表示」という機能があり、それを対応、更新するのにLuaコードだけでは不十分で、他のところから参照する必要があった。Luaコード内には探しても探しても見つからないメソッドやクラス等が何個もあり、更新する際にそこがネックとなった。 また、従来の方法だとゲームを起動して探すという方法しかないためそれをどうにかしたいと思ったのもある。

元のコード

f:id:KashEight:20170919180719p:plain
上の画像はnbmd氏が作成した日本語MODの元のjp_p.luaのコード、ここでは

self._unit:sound_source(source):post_event(sound_name, callback_function, self._unit, "marker", "duration", "end_of_event")

上のsound_souce(), post_event()の2つのメソッドがLuaコード内で見つからなかった。

unitデータを探す

hook元となるlib\units\beings\player\playersound.luaからself._unitを探したところ序盤に

function PlayerSound:init(unit)
    self._unit = unit

    unit:base():post_init()

    local ss = unit:sound_source()

    ss:set_switch("robber", "rb3")

    if unit:base().is_local_player then
        ss:set_switch("int_ext", "first")
    else
        ss:set_switch("int_ext", "third")
    end
end

とコンストラクタがあったのでそれから探し出せないかと考えた。
とりあえずunitを定義しているものを探そうと思いpayday2\unitsをcmd開いて
for /r /d %i in (*) do ren %i\*.unit *.xml
を実行し、.unitファイルを全て.xmlファイルに置換した。(Bundle Modderにも置換機能あるんでそれを利用しても良い。)
それで実際に置換したものを開いてみる。
f:id:KashEight:20171118235309p:plain
(units\payday2\characters\civ_male_worker_1\civ_male_worker_1_husk.xmlの例)
見ても普通にわからないのでmodworkshopのwikiページを参考にしながら色々探ってみる。(詳しいことはwikiページにて、英語なので苦労するかもしれない。)
流石に全部説明するのは酷(自分も完全に理解してるわけではないのもある)なので説明は省略するが、重要と思えるところだけ取り上げてみる。

<extensions>
        <extension name="unit_data" class="ScriptUnitData" />
        <extension name="base" class="HuskCivilianBase" >
...

<extentions>タグで囲まれてる部分がunitデータの部分と属するクラスである。
おそらく、呼び出されたunitデータがどのクラスで処理しているかを示していると思われる。
ここから、目的のものを探すことはできるのではないかと思ったが事はうまく進まない。まあ、当たり前だろう。
目的のsound_sourceがどう処理されているかがわからないのである。そのため、pager応答がどう行われているかわからない。

振り出しに戻ってしまった。けっこうだるい。まあ、少しは進んだか。
一応、この記事を書いてる現在(17/11/19)はある程度進んだもののまだ先は長そうだ。
これ以上書くと長くなるので、続きは次回にしよう、userdata型とかIdstring型とかめんどいのもあるし。

今回の話とあんまり関係ないけれども

f:id:KashEight:20171119004111p:plain
どうやら音は「ケツ」から出ているらしい。面白い。

参考にしたサイト

modworkshop - Wiki - unitデータの詳しい解説が載っています。