Technically Impossible

Lets look at the weak link in your statement. Anything "Technically Impossible" basically means we haven't figured out how yet.

Xanadu Clone - ザナドゥ クローン(ザナクロ)のソースを可視化する~Sourcetrail

f:id:espio999:20210718012127p:plain
ザナクロのソース全体を、Visual Studioのプロジェクトとして取り込むことで、ロジックやパラメータ設定の追跡が容易になった。細部を追いかけ、必要ならば変更を加え、さらにビルドまで対応できる環境として、Visual Studioは大変都合が良いのだが。ソースの全体像をつかむには、それに応じたツールを用いるのが良い。例えば、今回取り上げるSoucetrailだ。
www.sourcetrail.com

Sourcetrailを用いると、ソースコード中に定義された関数、パラメータなどの定義をクリック操作でたどることができる。そのため、ソース・ファイル、関数、パラメータ間の依存関係も把握しやすい。
コードを改変に興味はないが、ただコードを読んでみたいユーザーにも、馴染みやすい環境だ。

この投稿では、Visual StudioのプロジェクトからJSONファイルを出力し、それをSourcetrailに読み込ませて、コードを追跡するまでの手順を紹介する。

なお、この投稿での環境は、前回の投稿で紹介した手順を済ませた状態を引き継いでいる。手順を実行する前に、あらかじめ前回の手順を済ませておくのが理想的だが、Visual Studioの手順を飛ばして、直接Sourcetrailにソースコードを読み込ませることもできる。
とはいえ、ザナクロのコードはShift JIS、JISが混在している。初回投稿で紹介したように、ソースコードのUTF8変換は対応しておいた方が良い。
impsbl.hatenablog.jp
impsbl.hatenablog.jp

前提

フォルダ

この投稿では、ザナクロのソース・ファイルは、次のフォルダに収録されている。

D:\user temp\work\xanadu\src

フォルダ”xanadu”を、プロジェクト・フォルダとして登録する。つまり、次のフォルダだ。

D:\user temp\work\xanadu

Sourcetrail

この投稿で紹介する作業を始める前に、Sourcetrailのインストールを完了しておく必要がある。
Sourcetrail - The open-source cross-platform source explorer

Sourcetrail Extension

Visual StudioからSourcetrailへ取り込むためのJSONファイルを出力するため、拡張機能を導入する必要がある。

まずVisual Studioから次の操作を行う。いずれの作業も、ザナクロのプロジェクトを開いた状態で実施している。一連の作業についてのスクリーン・キャプチャは、下記「参照:一連の作業」を展開してほしい。

ファイルメニューから「拡張機能の管理」を選択し、

拡張機能 > 拡張機能の管理

”sourcetrail”を検索する。表示された”Sourcetrail Extension”をダウンロードする。自動的にインストールされるので、終了後にVisual Studioを再起動する。

再起動後に追加されたメニューから、”Create Compilation Database”を選択する。

拡張機能 > Sourcetrail > Create Compilation Database

表示された画面にてプロジェクトを選択し、次のように設定する。指示のない項目はデフォルト値を用いる。

Configuration Release
Platform x86
Directory d:\user temp\work\xanadu

”Create”ボタン押下で、JSONファイルが指定フォルダに出力される。Sourcetrailへの自動インポートが提案されるが、”Finish”ボタン押下で作業を終了する。

参照:一連の作業
f:id:espio999:20210717233232p:plain
f:id:espio999:20210717233246p:plain
f:id:espio999:20210717233301p:plain
f:id:espio999:20210717233311p:plain
f:id:espio999:20210717233323p:plain

Sourcetrail

プロジェクト

Sourcetrailを起動する。”New Project”ボタン押下で、新規プロジェクトを作成する。
”New Project”で指定する情報は任意で構わない。私は次のように設定した。

Sourcetrail Project Name xanadu_clone
Sourcetrail Project Location d:\user temp\work\xanadu\sourcetrail

"NEW SOURCE GROUP”では、次のように選択する。"Create Compilation Database”は作成済みなので無視する。

タブ”C” > C/C++ from Visual Studio > Next > Next

もしVisual Studioを利用しておらず、ザナクロのソースコードを直接読み込ませる場合は、”Empty C Source Group”を選択する。
”Edit Project”では、次の情報を追加する。指示のない項目は任意、あるいはデフォルトで構わない。

Compilation Database D:\user temp\work\xanadu\compile_commands.json
Additional Include Paths D:\user temp\work\xanadu\include
C:\Program Files (x86)\Embarcadero\Studio\20.0\include\windows\crtl

”Additional Include Paths”に指定したパスの一方は、RAD Studio (C++ Builder)が提供するWindows用ヘッダー・ファイルの在処だ。Visual Studio同様、Sourcetrailもコードを自動的に精査して、エラーや警告を出力する。その数を少しでも軽減するための配慮だ。
なお、ザナクロのソースにはWindowsだけでなく、LinuxFreeBSD向けに用いられるコードも含まれている。Windows環境には、これらのコンパイルに必要なリソースが含まれていないため、それに起因する警告、エラー表示は避けられない。

参照:一連の作業
f:id:espio999:20210717233358p:plain
f:id:espio999:20210717233410p:plain
f:id:espio999:20210717233422p:plain
f:id:espio999:20210717233432p:plain
f:id:espio999:20210717233448p:plain
f:id:espio999:20210717233459p:plain
f:id:espio999:20210717233510p:plain
f:id:espio999:20210717233521p:plain

コードの追跡

得られた出力結果は、次の項目でまとめられている。

関心のある所から、クリックしていく。プログラムは”main.c”から始まる。特にWindowsの場合は、次のパスに格納されているものだ。

D:\user temp\work\xanadu\src\win32

該当ファイル中に定義された関数”WinMain”が実行され、関数”start”が関数”init”を呼び…まずザナクロの画面領域が定義され、出力されていく。

次のようにクリックしていくと、「一連の作業」にまとめたように画面遷移していく。その操作に合わせてエディタ画面には、ファイルや関数のコードが連動して出力される。

Files > main.c > WinMain > start

参照:一連の作業
f:id:espio999:20210717233535p:plain
f:id:espio999:20210717233553p:plain
f:id:espio999:20210717233603p:plain
f:id:espio999:20210717233617p:plain

逆さツララのダメージ

XANADU scenario 2』には「逆さツララ」と呼ばれる地形が存在する。その地形に触れるのは、言うなればスパイクを踏みつけることとなり、そのときの装備、アイテム数に応じたダメージを負うことになる。
バグ、あるいは仕様かは分からないが、ある条件を満たすと、このダメージが200万を超えるのだという。ザナクロは、この現象を実装しているか追跡してみよう。

まずVisual Studioで「逆さツララ」を検索すると、”field.c”で次のように定義されているのが分かる。ここをスタート地点として、Sourcetrailで追跡する。

  • field.c
/* 逆さツララ */
static int field_user_trapped; /* ダメージフラグ */
static void user_fall_hazard(void);

f:id:espio999:20210717233736p:plain
関数”user_fall_hazard”にてダメージが計算され、HPからダメージが差し引かれているのが分かる。

void user_fall_hazard(void)
{
  int i, damage;

  /* ダメージの計算 */
  damage = 1;
  for (i = 0; i < MAX_GOODS; i++) {
    damage += user.inventory[GOODS_WEAPON][i].stock;
    damage += user.inventory[GOODS_SCROLL][i].stock;
    damage += user.inventory[GOODS_ARMOUR][i].stock;
    damage += user.inventory[GOODS_SHIELD][i].stock;
    damage += user.inventory[GOODS_MAGIC_ITEM][i].stock;
  }
  damage *= 100;
  user.status.HP -= damage;
    
  format_message("DMG-%d", damage);
  status_update_HP(red_pixel);
  
  field_user_trapped = 1;
  se_play(SE_TRAPPED);
}

ダメージは、int型の変数”damage”として定義されており、その最大値を考慮すれば200万を超える場合はあり得る。
実際のダメージは所持品数合計の100倍だ。”goods.c”に定義されている所持品全てを255ずつ所持していたとすると、ダメージは2,448,000になるのだが、オリジナルのプレイ中に、この状況が成立することはあり得ないだろう。

考えられるとすれば、ダメージ計算処理が繰り返し呼び出された結果として、合計ダメージ数が200万を超えた場合だが、それならば、その繰り返し回数だけ被ダメージ・メッセージが表示される。一度に200万超のダメージを被るわけではない。

ザナクロのソースを見るたびに思うのは、オブジェクト指向でもないのに、かなり整然かつ明瞭に構造化されていることだ。ツールを用いなくとも、とても読みやすい。
おそらくオリジナルでは、そのような構造化ができておらず(きっと、そのようなことを考慮する時代ではなかったように思う)、想定していなかった条件が重なった結果として、200万超のダメージが出力されたのではないか。

その200万超ダメージはカルマ、毒の取得に関連するらしいのだが、ザナクロは見事に構造化されているお陰で、それらが相互に影響することがない。カルマ、毒に関連する要素は、関数”fall_hazard”に含まれていないし、関連も無いのは、コード、並びにSourcetailの出力から明らかだ。
少なくとも、ザナクロではパラメータを改変して、意図的に所持品数を増やさない限り、「逆さツララ」200万超のダメージを被ることはないだろう。

ちなみに、

該当箇所の処理を消してしまう、あるいはロジックは変えず、100倍するところを0倍とすれば、「逆さツララ」のダメージは無効化できる。

毒の取得によって、カルマが-5されるのは、次の箇所で定義されている。

  • battle.c
    case OTHER_POTION:
      user.status.KRM = max(0, user.status.KRM - 5);
      goto its_poison;