2019年9月27日

【Papyrus.psc】Int型やString型のRefIDを取得・表示する

- Papyrusスクリプトの記憶を記録に残すシリーズその17 -

スカイリムの RefID や BaseID は16進数で表されています。今回はこの16進数を扱ってみたいと思います。


目次

  1. Papyrus における Int型 について
  2. Int型 を String型の16進数 に変換するスクリプト
  3. String型の16進数 を Int型 に変換するスクリプト
  4. RefID や BaseID からesp名を取得するスクリプト

1. Papyrus における Int型 について

1-1. Int型 の範囲

IDは8桁の16進数で表されますが、中身は Int型 の数値です。

Papyrus における Int型 は、他のプログラミング言語と同じく「32ビットの符号付き整数型」です。表せる範囲は以下の整数です。

  • -2147483648 ~ 2147483647 (10進数)
  • 0x00000000 ~ 0xFFFFFFFF (16進数)

一般に16進数を表す場合は、先頭に「0x」を付けます。これは Papyrus でも同様です。
Int型16進数はその内部に符号情報を持っていますので、10進数と照らし合わせると以下のような対応関係にあります。

10進数 -2147483648 -1 0 2147483647
16進数 0x80000000 0xFFFFFFFF 0x0 0x7FFFFFFF
1-2. Int型 の計算

Int型 を内部処理に用いる際は、それが10進数なのか16進数なのかということを考える必要はありません。数値的には同一の値だからです。

数式に Int型 の変数名を入れればそのまま計算できますし、10進数として直接「255」と書いても、16進数として「0xFF」と書いても同じように動作します。10進数と16進数との混合計算や比較も可能です。

ちなみに、String型 (文字列型) の10進数表現 を Int型 や Float型 として取得するなら、String型 の後ろに「as Int」や「as Float」を付けるだけで変換できます。

Int intValue = "1234" as Int ; 10進数文字列を数値に変換
Debug.MessageBox(intValue + 1) ; 「1235」を表示
1-3. IDと16進数の関係

RefID や BaseID も16進数であり、上位2桁が「00 ~ FF」としてロードオーダーに使われています。

この2桁だけを10進数で考えると「0 ~ 255」に相当します。ロードオーダーの「FF」はセーブファイルに保存されるデータになりますので、esm/esp でロードオーダーとして扱えるのは16進数だと「00 ~ FE」まで、10進数だと「0 ~ 254」までの255個となります。

バニラの「Skyrim.esm」「Update.esm」「Dawnguard.esm」「HearthFires.esm」「Dragonborn.esm」の5つは外せませんので、残り250個がMODで使える数となっています。

下6桁は、そのMOD内で使えるオブジェクトの数になります。
作成したベースオブジェクトや、レンダーウィンドウに配置した ObjectReference 等すべてにIDが割り振られます。

理論的には 0x0~0xFFFFFF までなので、1677万7216個登録することができます。0番付近をシステム管理で使うとしても、1つのMODで1600万以上のオブジェクトを扱えるというのはすごいですね。普通のMODならその0.1%も使っていないと思います。

1-4. IDを扱う関数

IDを扱う関数には、以下のようなものがあります。

Form.GetFormID() Form や ObjectReference のIDを Int型 で取得する。
Game.GetForm(int aiFormID) aiFormID (Int型) から Form を取得する。
Game.GetFormFromFile(int aiFormID, string asFilename) asFilename (String型) のesp名のロードオーダーを aiFormID に付与した上で Form を取得する。
Game.GetModName(int modIndex) modIndex (Int型) をロードオーダーとして読み、そのesp名を String型 で取得する。
Game.GetModByName(string name) name (String型) のesp名のロードオーダーを Int型 で取得する。espが無ければ 255 (0xFF) を返す。

GetFormFromFile() は面白い関数で、esm/esp名とIDから Form を取得できてしまいます。ロードオーダーは不問です。
下の例では、セラーナさんをプレイヤーの所に呼び寄せます。このとき "Dawnguard.esm" をマスター指定する必要はありません。

Actor actSerana = Game.GetFormFromFile(0x00002B74, "Dawnguard.esm") as Actor
actSerana.MoveTo(Game.GetPlayer())

目次に戻る


2. Int型 を String型の16進数 に変換するスクリプト

2-1. 10進数を16進数に変換する必要性

上述のように、内部処理で Int型 を扱う場合には、10進数と16進数を変換する必要はありません。
それが必要になるのは、「GetFormID() などで取得した Int型 を、IDとしてプレイヤーに表示したい」というときです。

Debug.MessageBox() 関数で Int型 を表示すると10進数になってしまうので、8桁の RefID として表示したいときに困ります。
プレイヤーに向けてIDを表示するには、Int型 を String型 に変換することになります。

以下がそのスクリプトです。内容を説明するとビット演算の領域になってしまうので割愛しますが、DecimalToHex という Function(関数)を作って、必要な所で呼び出して使います。

ビット演算はSKSEの関数を使いますので、今回載せたスクリプトはすべてSKSEが必要です。

2-2. DecimalToHex
;--------------------------------------------------何らかのイベントの中で関数を呼び出す。
Event OnActivate(ObjectReference akActionRef)
    Int intDecimal = 1234 ; 変換したい Int型
    Debug.MessageBox(intDecimal + " は、16進数で表すと " + DecimalToHex(intDecimal) + " です。")
EndEvent

;--------------------------------------------------Int型 を String型16進数 に変換する関数。
String Function DecimalToHex(Int aiDecimal)
    String strHex

    Int i = 0
    While i < 8
        strHex = StringUtil.GetNthChar("0123456789ABCDEF", Math.LogicalAnd(0xF, aiDecimal)) + strHex
        aiDecimal = Math.RightShift(aiDecimal, 4)
        i += 1
    EndWhile

    Return strHex
EndFunction

目次に戻る


3. String型の16進数 を Int型 に変換するスクリプト

3-1. 16進数を10進数に変換する必要性

String型の16進数を Int型 として取得したい場合ですが、これはかなり状況が限られます。
UIExtensions のようにプレイヤーからの入力を受け取れるMODを用いて、入力された RefID から Form を取得する場合に必要となります。

以下のスクリプトでは、文字を数値に変換するために文字コードを取得しています。
プレイヤーの入力を受け取るということから、エラー処理をしっかり組み込む必要があります。大文字と小文字の両方に対応しておく必要もあります。

3-2. HexToDecimal
;--------------------------------------------------何らかのイベントの中で関数を呼び出す。
Event OnActivate(ObjectReference akActionRef)
    String strHex = "FFFFFFFF" ; 変換したいString型の16進数
    Int intHex = HexToDecimal(strHex)
    If intHex == 0
        Debug.MessageBox(strHex + " は 0 であるか、または有効な16進数ではありません。")
    Else
        Debug.MessageBox(strHex + " の 10進数は " + intHex + " です。")
    EndIf
EndEvent

;--------------------------------------------------String型16進数 (最大8桁) を Int型 に変換する関数。
Int Function HexToDecimal(String asHex) ; Return 0 on error
    Int intLength = StringUtil.GetLength(asHex) - 1
    If intLength < 0 || intLength > 7
        Return 0
    EndIf

    Int intDecimal = 0
    Int intChar ; Character code
    Int i = 0
    While i <= intLength && i < 8
        intChar = StringUtil.AsOrd(StringUtil.GetNthChar(asHex, intLength - i))
        If intChar >= 48 && intChar <= 57 ; 0 to 9
            intChar -= 48
        ElseIf intChar >= 65 && intChar <= 70 ; A to F
            intChar -= 55
        ElseIf intChar >= 97 && intChar <= 102 ; a to f
            intChar -= 87
        Else
            Return 0
        EndIf
        intDecimal += Math.LeftShift(intChar, 4 * i)
        i += 1
    EndWhile

    Return intDecimal
EndFunction

目次に戻る


4. RefID や BaseID からesp名を取得するスクリプト

4-1. esp名の取得

最後におまけとして、Int型 のIDからesm/esp名を取得する関数も載せておきます。処理自体は短いので、Function にせずに書き込んでしまっても良いと思います。
ロードオーダーがFFの場合は「Save file」という文字列を返しています。

4-2. DecimalToModName
;--------------------------------------------------何らかのイベントの中で関数を呼び出す。
Event OnActivate(ObjectReference akActionRef)
    ActorBase acbPlayer = Game.GetPlayer().GetActorBase()

    Int intFormID = acbPlayer.GetFormID() ; Int型 のFormID
    Debug.MessageBox(acbPlayer.GetName() + " は、" + DecimalToModName(intFormID) + " で管理されています。")

    acbPlayer = None
EndEvent

;--------------------------------------------------Int型 のIDから String型のesp名 を取得する関数。
String Function DecimalToModName(Int aiFormID)
    Int intOrder = Math.RightShift(aiFormID, 24)
    If intOrder == 0xFF
        Return "Save file"
    Else
        Return Game.GetModName(intOrder)
    EndIf
EndFunction

Int型 のIDから上位2桁のロードオーダーを取得するとき、RightShift() を使っています。これを「0x1000000 で割る」という方法でやることもできそうに思えますが、実際にはうまくいきません。

ロードオーダーが 80~FF のIDは、内部的には 負数のInt型 になります。最初の10進数と照らし合わせた表を見るとわかりやすいかもしれません。

負数の割り算というのは鬼門でして、プログラミング言語によってその商の求め方が異なります。Papyrus の場合、次のようになります。

  • 0x80000000/0x1000000=-128
  • 0x80000001/0x1000000=-127

結果が負数な上に割り切れる場合と余りが出る場合とで1つずれてしまい、ロードオーダーとして取得できません。もちろん「結果が負数だったら255を足す」というような処理をしても構いませんが、RightShift を使う方が綺麗でしょう。

ビット演算に詳しい方は、「負数を RightShift したら算術シフトされてしまうんじゃないの?」と思われるかもしれません。算術シフトされると結果は「-128」になってしまいます。
でもそこはSKSEの秀逸なところで、負数でも論理シフトを行ってくれて結果は「128」となり、ロードオーダーの取得に使えるようになっています。

目次に戻る


【Papyrus.psc】シリーズリスト

補足:私はスクリプトを書くのは好きですが、専門家ではありません。内容は creationkit.com の情報と個人的な経験を基にして書いています。どうかご参考程度にご覧ください。

トップに戻る

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。