20年以上前の自作ゲームは再コンパイルしたら動くのか？

このエントリーはC++アドベントカレンダー23日目の記事です。

https://qiita.com/advent-calendar/2024/cxx

# 古（いにしえ）のゲームを発掘したけれど動かない
クラウドストレージに放り込んでいたデータの整理をしていたところ、20年以上前に自作したゲームが出てきました。
C++でWin32APIとDirectXを使って開発したWindowsゲームです。

確かWindows7までは動いた記憶があります。今の環境(Windows11)でも動くのかと試しに実行してみましたが、動きませんでした。（互換モードなら動くとの情報もあり、こちらも試しましたがダメでした。）

では、今の環境で再コンパイルしたら動くのだろうか？をふと試したくなりました。

## 動かなくなったゲームを再コンパイルして復活させよう！
という事で、この記事では発掘したソースコードを今の環境で再コンパイルして動くか？を試してみたいと思います。

:::note warn
勢いでC++のアドベントカレンダーに投稿してしまいましたが、ここ10年は触っていない「C++何もわからない」おじさんのネタ記事です。
ゲームを作った当時は「C++完全に理解した」と思っていた気もしますが🐶
:::

## 発掘したゲームのソースコード一式
とりあえず、GitHubに上げました。
（下記リンクは修正前のコードが入ったコミット）

https://github.com/pierre3/Tama-chan/tree/da8b2cd79aa75453b8ab9f22f288baab7a9c39fc/TamaChan

<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/e73b2571-bb0e-d3b5-b81e-9784f5be671a.png" />


- C++: ソースコード(.cpp、.h)
- mapData: ゲーム内の各ステージのマップ情報
- bitmap: タイトル画像、キャラクタなどの画像データ(.bmp)
- mds: BGM用MIDIストリームファイル(.mds)
- waveData: 効果音データ(.wav)
- ゲームの実行ファイル(.exe)

当時のメモ書きを見てみると、当時の開発環境とゲームの動作環境は下記とのこと。恐ろしいほど低スペックですね。

### 開発環境
- OS：windows98
- CPU：celeron 333MHz
- メモリ	：32MB
- ビデオメモリ：4MB
- directX：ver5.0
- コンパイラ：Microsoft visual C++5.0

### 動作環境
- OS：windows98以降
- CPU：Pentium133MHz以上
- メモリ：32MB以上
- VRAM：2MB以上
- DirectX：ver.5.0以上
- グラフィック：解像度640×480, 256色

# とりあえずビルドしてみよう
今の開発環境
- Windows11(64bit)
- VisualStudio 2022
- DirectX 12 (がインストールされていました)

## 1. Visual StudioでC++のプロジェクトを作る

### C++空のプロジェクトに.cppと.hを放り込む
- VisualStudio Installer でワークロード「C++によるデスクトップ開発」を追加インストール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/246c901f-988a-304d-63f1-1a071a554930.png)

- VisualStudioでプロジェクトをC++「空のプロジェクト」を新規作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/a96b1e85-09a8-7885-761f-dedac11f0acd.png)

- 発掘したソースコード一式をプロジェクトフォルダに入れ、ソースファイル(.cppと.h)をプロジェクトに登録

### プロジェクトに依存ライブラリの参照を追加する
プロジェクトのプロパティを開き、「リンカー」-「入力」‐「追加の依存ファイル」にDirectDrawなどの依存ライブラリを追加。とりあえず下記があれば良さそう。
- ddraw.lib
- dsound.lib
- Winmm.lib

ここまででとりあえずビルドしてみましょう。

## 2. ビルドエラーの対処
当然ながら大量のエラーと警告が出ました。このエラーを１つずつつぶしていきましょう。（警告は一旦無視します）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/ce6244d2-0936-563b-54f4-0699c0c56d63.png)

### ビルドエラー1: main関数が未解決

:::note alert
未解決の外部シンボル main が関数 "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) で参照されました
:::

これは、空のプロジェクトでは、サブシステムが「コンソール」になっていることが原因でした。
プロジェクトのプロパティ「リンカー」-「システム」で「サブシステム」をWindowsにすることで解消。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/7193b695-d717-a586-3e2a-821152077ade.png)



### ビルドエラー2: 文字セットの問題

:::note alert
型 "const char *" の引数は型 "LPCWSTR" (aka "const WCHAR *") のパラメーターと互換性がありません
:::

これは文字セットの問題でした。既定値は「Unicode」になっていますが、当時は「マルチバイト文字」が前提でした。
プロジェクトの「構成プロパティ」-「詳細」で「文字セット」をUnicodeから「マルチバイト文字セットを使用する」に変更
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/58760854-755f-2a77-e5f1-14e4e0763f17.png)

これでかなりの量のエラーが取れました。

### ビルドエラー3: 文字列リテラルのConst

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/2d9e869b-91df-affc-60c7-1bb68bc2d4a1.png)

:::note alert
型 "const char *" の引数は型 "LPSTR" (aka "char *") のパラメーターと互換性がありません
:::

ファイルの読み込む関数の呼び出しでエラー。文字列リテラルがそのまま渡せなくなっている?

`const_cast`を使い文字列リテラルの型 `const char*` から`const`をはがしてやることで対応。
これが正しい対処かは不明ですが、単にファイルPathを渡しているだけなので問題ないはず。

```cpp
if(!DDLoadBmp(&lpDDOF[OFFSCRN_CHAR], const_cast<char*>("bitmap\\MyChar.bmp"),
	10,236,BMP_X,BMP_MC_Y,BMP_MC_W,BMP_MC_H)){
	return(FALSE);
}
```

### ビルドエラー4: 宣言時の型省略

:::note alert
型指定子がありません - int と仮定しました。メモ: C++ は int を既定値としてサポートしていません
:::

昔は、型を省略するとintとして扱ってくれて下記はOKでしたが、型を明示しないとコンパイルエラーになるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/077cd911-6201-d4ba-dc56-faf9e69f46b3.png)

```cpp
static int nYSpd =-0x98000;
```

## 3. 実行時エラー
これでコンパイルは通るようになりましたが、起動してみると下記のエラーが
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/fdb0cd87-a9d4-5754-a993-0d0a6086536b.png)

デバッグしてみると、ディスプレイのカラーモードを変更する箇所で失敗しているようです。
```cpp
//カラーモードを調べる-256色に切り替える_______________________________
dwColor=GetDeviceCaps(hDeviceDC,BITSPIXEL);
DeleteDC(hDeviceDC);
if(dwColor!=8){
	ZeroMemory(&dm,sizeof(dm));
	dm.dmSize=sizeof(dm);
	dm.dmBitsPerPel=8;	//256色モード
	dm.dmFields=DM_BITSPERPEL;
	if(SUCCEEDED(ChangeDisplaySettings(&dm,CDS_TEST)))//一度テストする
	{
		ChangeDisplaySettings(&dm,CDS_UPDATEREGISTRY);//切り替え用として保存
		fChgColor=TRUE;
	}else{
		MessageBox(gData.hwnd,"カラーモードの変更に失敗しました",
		"Direct Draw",MB_OK|MB_ICONSTOP);
		return(FALSE);
	}
}
```

戻り値を見てみると、-2(DISP_CHANGE_BADMODE)が返却されていました。対応していないカラーモードのようです。
どうやら256色(8bit)のカラーモードは対応していないよう。

24bitカラーモードに変更してみます。
```cpp
#define BPP 24　//24bitカラーモード

dm.dmBitsPerPel=BPP;
```

そうすると、今度はパレットデータを登録する`SetEntries`でエラーになりました。
パレットデータは8bitカラーモードのみで使用するものですので、パレット関連の処理は全て削除しました。
```cpp
//ビットマップからのパレットデータを登録
err = lpDDPL->SetEntries(
	0,
	dwStartingEntry,
	dwCount,
	&ape[dwStartingEntry]
);
```

これでひとまず、ディスプレイモードの問題は解消しました。

### ポインタの64bit問題
今度は下記メソッドの実行時にアクセス違反のエラーとなりました。

```cpp
//------------------------------------------------------------
//デバイスオープン
//------------------------------------------------------------
mmResult = midiStreamOpen(
	&midiStream.hMidiStream,//デバイスハンドルの取得
	&uiID,				//デバイスIDの指定
	1,					//１に予約
	(DWORD)StreamProc,//ストリーム用プロシージャ
	0,
	CALLBACK_FUNCTION);//コールバック関数としてストリーム用プロシージャを使用
```

:::note alert
0x00000000E58E11EA で例外がスローされました (TamaChan.exe 内): 0xC0000005: 場所 0x00000000E58E11EA の実行中にアクセス違反が発生しました
:::

これは、コールバック関数`StreamProc`のポインタを`DWORD`で渡していることが原因でした。

DWORDは`unsigind long`(32bit)ですが、ポインタはプラットフォームによって精度が変わり、64bitプラットフォームでは64bitになるとの事です。

:::note info
[ポインター精度整数型](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/kernel/the-new-data-types#pointer-precision-integer-types)
ポインターの精度が変化すると (つまり、32 ビット プラットフォーム用にコンパイルされると 32 ビットになり、64 ビット プラットフォーム用にコンパイルされた場合は 64 ビット)、これらのデータ型はそれに応じて精度を反映します。 したがって、ポインターの算術演算を実行するときに、これらの型のいずれかにポインターをキャストすることは安全です。ポインター精度が 64 ビットの場合、型は 64 ビットです。 
:::

ここでは、ポインタ精度の`DWORD_PTR`でキャストして渡すのが正解のようです。

```cpp
mmResult = midiStreamOpen(
	&midiStream.hMidiStream,//デバイスハンドルの取得
	&uiID,				//デバイスIDの指定
	1,					//１に予約
	(DWORD_PTR)StreamProc,//ストリーム用プロシージャ
	0,
	CALLBACK_FUNCTION);//コールバック関数としてストリーム用プロシージャを使用
```

また、コールバック関数の引数wParamもポインタを受け取るため`DWORD`から`DWORD_PTR`に修正する必要がありました。
```cpp
void CALLBACK StreamProc(HMIDIIN hMidiIn, UINT msg,
	DWORD hInst, DWORD_PTR wParam, DWORD lParam)
{

}
```

### 起動した！
ここまででようやく、ゲーム画面が起動しました。操作も問題ありません。BGMも軽快に鳴っております。
ただ、まだ１つ問題がありました。

## 4. キャラクタの背景が透過されない問題
キャラクタ周辺の背景色が透過されていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/cefdfcfb-0a49-6d04-41a0-847f870f619d.png)

背景を透過させるには、キャラクタなどの画像を描画するSurfaceオブジェクトに透過させたい色を登録する必要があるのですが、カラーモード8bit時の設定が残っていました。これが原因かもしれません。

```cpp
#define COLOR_KEY 15 //透過色を指定（8bit,パレット番号）
```
```cpp
DDSURFACEDESC	ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));

ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = dwWidth;
ddsd.dwHeight = dwHeight;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN　| dwMemoryFlag;
//...
//透過色を指定
ddck.dwColorSpaceLowValue = COLOR_KEY;
ddck.dwColorSpaceHighValue = COLOR_KEY;
```

Bitmap画像から透過色（薄水色の部分）のRGBを調べて、下記の様に設定してみましたがうまくいきません。

```cpp
#define COLOR_KEY RGB(153,217,234)//カラーキー(透過色)
```

RGBマクロの定義を見てみると、B|G|R のbit構成で32bit整数を組み立てているようです。
```cpp
#define RGB(r,g,b)  ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))
// 32bitの構成
// 0000 0000 bbbb bbbb gggg gggg rrrr rrrr
```

このBit構成は、環境によって異なっていた気もします。調べてみるとどうやらSurfaceオブジェクトのPixelFormatにその情報があるようです。

### RGBのbit構成を調べてカラー値を組み立てる
Surfaceオブジェクトの詳細情報が格納されている SurfaceDescからRGB各色のBitマスクが取れました。
```cpp
//dwRBitMask: 0000 0000 1111 1111 0000 0000 0000 0000
//dwGBitMask: 0000 0000 0000 0000 1111 1111 0000 0000
//dwBBitMask: 0000 0000 0000 0000 0000 0000 1111 1111
```
RGBマクロとはRとBが逆ですね。これを元にColor値を組み立てる関数を作りました。

```cpp
//DDSurfaceのPixelFormatのBitMask値を見てRGB値からDWORD値を組み立てる
DWORD tagDDRAW::RgbToDword(LPDIRECTDRAWSURFACE lpDDPR, BYTE r, BYTE g, BYTE b)
{
	DWORD
		dwRBits = 0, dwGBits = 0, dwBBits = 0,
		dwR = 0, dwG = 0, dwB = 0,
		dwRMask = 0, dwGMask = 0, dwBMask = 0;
	int
		nRStart = -1, nGStart = -1, nBStart = -1,
		bRBitOn = FALSE, bGBitOn = FALSE, bBBitOn = FALSE;
    //SurfaceDescからbitマスクを取得
	DDSURFACEDESC ddsdPR;
	ZeroMemory(&ddsdPR, sizeof(ddsdPR));
	ddsdPR.dwSize = sizeof(ddsdPR);
	lpDDPR->GetSurfaceDesc(&ddsdPR);
	dwRMask = ddsdPR.ddpfPixelFormat.dwRBitMask;
	dwGMask = ddsdPR.ddpfPixelFormat.dwGBitMask;
	dwBMask = ddsdPR.ddpfPixelFormat.dwBBitMask;
    //各マスクについてONビットの開始位置とONビット数を調べる
	for (int i = 0; i < 32; i++)
	{
		bRBitOn = (dwRMask >> i) & 1;
		bGBitOn = (dwGMask >> i) & 1;
		bBBitOn = (dwBMask >> i) & 1;
		if (bRBitOn) {
			if (nRStart == -1) {
				nRStart = i;
			}
			dwRBits++;
		}
		if (bGBitOn) {
			if (nGStart == -1) {
				nGStart = i;
			}
			dwGBits++;
		}
		if (bBBitOn) {
			if (nBStart == -1) {
				nBStart = i;
			}
			dwBBits++;
		}
	}
    //8ビット未満ならその差を右シフト
	r = r >> (8 - dwRBits);
	g = g >> (8 - dwGBits);
	b = b >> (8 - dwBBits);
    //マスクの開始位置まで左シフト
	dwR = ((DWORD)r) << nRStart;
	dwG = ((DWORD)g) << nGStart;
	dwB = ((DWORD)b) << nBStart;
    //ORでつなげて返却
	return(dwR | dwG | dwB);
}
```
これを使って透過色を指定することで、無事にキャラクタの余白部分を透過することができました。


## まとめ
無事に古（いにしえ）のゲームを復活させることができました。

動かなかった原因としては下記２点になると思います。

- ２５６色カラーモードが使えなくなっていた
- そもそも32bitプラットフォームを前提にコーディング＆ビルドしていたものを64bit PCで動かそうとしていた

何はともあれ無事動くようになって良かったです。正直、これだけの修正であっさり動いてしまうとは思っていませんでした。
DirectDrawはDirectX8.0で廃止されたとの事ですが、DirectX12となった今でも当時のコードがそのまま動くのには驚きです。

### ソースコード
最後に修正したソースコードをあげておきます。
プレイ動画のリンクも付けておきましたのでどんなゲームか気になりましたら見てやってください。

https://github.com/pierre3/Tama-chan