白銀のライねる

解析とかのメモとかをなんとか

【空の探検隊】markfont.dat・staffont.dat

markfont.datは、/FONT内にあるdatファイル。
ゲーム内の特殊文字(アイコン)を格納している。ABXYLRボタン、十字ボタン、メールアイコン等・・・。
markfont.palと紐づいており、こちらはパレットファイルとなっている。

これらのアイコンはゲーム内文字列の"「M」から始まるタグ文字"が変換されることで表示される。
例えば、メッセージ0x08E9の場合

[item:0]を とうろくした!
[M:B6]ボタンをおして[M:B7]ボタンで うてるぞ!

このうち「[item:0]」「[M:B6]」「[M:B7]」の3つがタグ文字。[item:0]はアイテム名が入るのでアイコンは使われず、Mから始まる残りの2つにアイコンが使われる。
[M:B6]にはLボタンのアイコン、[M:B7]にはRボタンのアイコンが表示される。ちなみに、Bは「Button」のB。

」「」「」と書かれているのは開発用に使われていたアイコン。ゲーム内では未使用部分含めて一切使用されていない、いわゆる没データ中の没データ。
それぞれ、は[M:T4]、は[M:T5]、は[M:T6]に割り当てられている。このTは「Text」のT。
はそのまんま「仮」の意味、は「未使用」となんとなくわかる。は分かりにくいがおそらく「デバッグ」のデだろう。
このようなアイコンが用意されているにもかかわらず、未使用・デバッグ系の文字には[M:D0]や[M:D1]という全く別のタグが付けられている・・・が、アイコンは割り当てられていないので何も表示されない。識別の用途?ちなみに、Dは「Debug」のDと思われる。
TCRFによると時闇のデバッグROMでは未使用の物にはちゃんとが使われているらしい。

更に雑学を挟むと、海外版では[M:T0]、[M:T1]のアイコンが日本版と異なる。日本版では[M:T0]が(鍵括弧)、[M:T1]が(アスタリスク)だが、海外版の[M:T0]は(コロン)、[M:T1]は💭のような吹き出しのアイコンが表示される。

staffont


staffont.datはスタッフクレジットに使われるフォントファイル。staffont.palがパレットデータ。
markfontと全く同じ構造で同じ方法で展開可能。
目立った没データは無いが、使われていないアルファベットぐらいは探せば見つかるかもしれない。(探していない)

名前が「stafffont」ではないのは、fが多すぎて冗長だから??

構造 (markfont.dat, staffont.dat)

datファイルの構造は以下の通り。

0x000~0x19F エントリ情報テーブル
0x200以降 アイコンデータ群(8bpp)

エントリ情報テーブルは4バイト毎に各アイコンの幅・高さ・開始アドレスが格納されている。
アイコンに関しては、このゲームでは珍しい8bppが使われている。他の画像データのほとんどは4bpp。
よってパレットは16色ではなく256色必要。後述のパレットデータ(pal)には128色分のデータしかないが、アイコンデータを覗くと0x81以上や0x00のバイトで敷き詰められている為、0x80=128色まで不要な色でパレットを埋めると数が合うようになる。

エントリ

エントリの構造は以下の通り。長さは4バイト。

0x00 byte アイコンの幅
0x01 byte アイコンの高さ
0x02 int16 アイコンデータ開始アドレス

幅・高さ共に0x00、アドレスが0xFFFFのエントリは予約領域として残っている無効なエントリ。プログラムで処理する場合はこの部分をスキップする必要がある。
開始アドレスはmarkfont.dat上のアドレスを示す。例えば、一番最初の「16 0B 00 02」は幅22px、高さ11pxの画像でアドレスは0x200から始まる。アドレス範囲の終端は次のエントリorファイルの終端。

構造 (markfont.pal, staffont.pal)

palファイル(パレットデータ)の構造は以下の通り。すべて4バイトのエントリで構成。

0x00 byte R値
0x01 byte G値
0x02 byte B値
0x03 byte 未使用

RGB値で構成される色データが格納されている。各エントリ内の最後のバイトは常に0x00か0xFFだが、無視して良い。
128エントリ=128色分の色が格納されているが、ここで使われるフォーマットは8bppなので256色必要。足りない分は先程の通り不要な色で128色分埋めて、以降にmarkfont.palの128色を足すことで256色のパレットが構成される。
なお、不要な色はゲーム内では「透明色」として表示される。

展開するプログラム (C#)

MarkFont.cs

using System;
using System.Collections.Generic;
using System.Drawing;

public class MarkFont
{
    /// <summary>エントリのバイト長</summary>
    public const int ENTRY_BYTE = 4;

    /// <summary>エントリデータ</summary>
    public List<MarkFontEntry> Fonts = new List<MarkFontEntry>();

    /// <summary>パレット</summary>
    public List<Color> Palette = new List<Color>();

    /// <summary>
    /// MarkFont
    /// </summary>
    /// <param name="dat">markfont.dat</param>
    /// <param name="pal">markfont.pal</param>
    public MarkFont(byte[] dat, byte[] pal)
    {
        // パレット取得
        for (int i = 0; i < 0x80; i++)
            Palette.Add(Color.Transparent);
        for (int i = 0; i < pal.Length; i += 4)
            Palette.Add(Color.FromArgb(pal[i + 0], pal[i + 1], pal[i + 2]));

        // フォントデータ取得
        for (int i = 0; i < BitConverter.ToInt16(dat, 0x2); i += ENTRY_BYTE)
        {
            int width = dat[i + 0];
            int height = dat[i + 1];
            int offset = BitConverter.ToInt16(dat, i + 2);

            if (width > 0 && height > 0)
            {
                byte[] data = dat[offset..(offset + width * height)];
                Fonts.Add(new MarkFontEntry(width, height, data));
            }
        }
    }
}

MarkFontEntry.cs

using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

public class MarkFontEntry
{
    /// <summary></summary>
    public int Width { get; set; }

    /// <summary>高さ</summary>
    public int Height { get; set; }

    /// <summary>アイコンデータ</summary>
    public byte[] Data { get; set; }

    public MarkFontEntry(int width, int height, byte[] data)
    {
        Width = width;
        Height = height;
        Data = data;
    }

    /// <summary>
    /// Bitmapとして生成
    /// </summary>
    /// <param name="palette">パレットデータ</param>
    /// <returns></returns>
    public Bitmap GenerateBitmap(List<Color> palette)
    {
        Bitmap res = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);

        BitmapData bmp = res.LockBits(
            new Rectangle(0, 0, res.Width, res.Height),
            ImageLockMode.WriteOnly,
            PixelFormat.Format32bppArgb);

        for (int i = 0; i < Data.Length; i++)
            Marshal.WriteInt32(bmp.Scan0, i * 4, palette[Data[i]].ToArgb());
        res.UnlockBits(bmp);

        return res;
    }
}

MarkFontクラスにmarkfont.datとmarkfont.palをbyte配列で持たせて、生成されたList内のMarkFontEntryクラスのGenerateBitmapにパレット持たせて実行すればBitmapに変換されます。

Marshal.Copyでささっと8bppのまま出力したかったのに何故か座標がずれてうまくいかないので、32bppにしてMarshal.WriteInt32で色をポチポチ置いていくことでなんとかしました。
もっと良い方法がありそうだけど技術不足なので今はこれでいいかな…。

参考

skytemple-files (graphics/fonts/graphic_font)

markfont.datのファイル構造が全く分からなかったので超参考にした。
コードはpythonで書かれており、私自身もpythonは少しわかるのでC#化は簡単だったが、画像フォーマットをずっと4bppだと思い込んでいて一度詰んだ。
ソースコードとにらめっこし続けていると画像生成処理にImage.frombytesを使っているっぽいのがわかったのでググって出てきたPILのリファレンスを参照したところ、modeを"P"に指定していると8ビット(8bpp)で生成すると書かれていたのでここでようやく問題解決。
空の探検隊の画像は全部4bpp」という固定観念に囚われてはいけない。
github.com

Pillow (PIL Fork) 10.0.0 documentation

さっき出てきたPILのリファレンス。pythonひよっこなので世界が広がった。
pillow.readthedocs.io

ExplorerScript Boundary Test

海外製の空の探検隊ROMハックツール「SkyTemple」と一緒に開発されたテキストテストツール。
文字タグに対応するアイコンについて参考にした。
textbox.skytemple.org