アドベントカレンダー 24日目
本記事はLabBaseテックカレンダー Advent Calendar 2025の 24日目になります!
はじめに
今年もやってきました、一年に一度の記事を書く日が...!
めちゃくちゃ寒いしインフルも流行ってるしで外に出たくない気持ちが本当に強い⛄
ところでみなさん、AI、活用してますか???
近年はAIの発展も著しく、性能がかなり上がってきましたよね
以前AIにアセンブリを食わせたことがあるのですが、あまりいい結果を得られなかったのを覚えています
最近プライベートのほうで、AIを活用しつつとある困った出来事を解決するために奮闘したときの出来事があったのですが、本記事ではそれについて話そうかと思います
ざっくり読める記事ではなく、結構読み物としての側面が強い記事になっているので、お酒とおつまみをお供にしてゆったりと読んでいただけたらなと思います🍻
それではきいてください...今回のテーマは...!
「AIと一緒に!リバースエンジニアリング!」
です!
ある日のこと
僕はプライベートでとあるFPSゲーム(以後cs2と呼びます)のコミュニティサーバーを運用しています
サーバーでは、Modding用のフレームワークを導入していて、開発者がプラグインという形でサーバー内の機能を改造したり追加したりすることができるようになっています
そんな中、いつものようにTodoリストから一つ取って、機能を実現するためにModdingをしていたある日のこと。
実現したい機能は次のようなものでした
任意のエンティティを貫通する弾丸処理の実装をしたい
正直取り組むまではそんなに難しくないだろう、と甘く考えていた自分がいましたとさ...
お仕事も終わって休日がきて、さくっと実装しちゃおう!そう思っていました
まず試したことが以下のようなものです
特定EntityのCollisionルールを変更する
※特定Entityは、ここではphysboxと呼ばれるEntityのことで、以後physboxと呼ぶことにします
まず前提として、physboxはサーバーから見るとただの Entity です
physboxは、マップ内に配置される「物理演算付きの箱系エンティティ」のようなもの
EntityにはCollision Ruleと呼ばれるものが存在していて、ようは衝突判定に関するプロパティのようなものですね
こいつはEnumの集合のようなフラグ値になっています
このEnumの一つに PassBullet と呼ばれるいかにもそれらしいフラグが存在しているのです
このフラグを立ててあげることで、弾を貫通させれるようになるので、これで実装完了...そう思っていました
さっそくコードを書いて仕上げて、ビルドしていざサーバーで実行してみると、
確かに弾は貫通するのです...が、一人でテストしていたためにその時はすぐに気づけませんでした
少し時間が経って、知人の一人に実装できたからテストしよう!と声をかけてサーバーへ呼びました
知人に敵役をしてもらい、僕はデバッグ用のコマンドで自分と知人の間にphysboxのEntityを設置しました
知人をデバッグコマンドで光らせて、Entity越しでも見えるようにし、いざ頭めがけて撃ってみると...!
知人: 「あれ、生きてる」
おかしい!!FPSにおいて頭に弾が当たるということは、ヘッドショットと呼ばれる判定になっていて、とっても痛いクリティカルなダメージを与えることができます
本来一発で知人は天国へ向かっているはずなのですが、彼は小石がこつんとぶつかったくらいのダメージしか受けておらず、まだしっかりと立っていました
なぜだ...弾はちゃんと貫通しているのに...と思考を巡らせていると、あることに気が付きました
弾は通るが、貫通したという事実は残ってしまう
そうです、弾は確かに貫通しているのですが、貫通したという事実を消したことにはならないため、サーバーではオブジェクトを貫通したから、その後のダメージ計算で減衰させたろ!
おそらくこんな感じでしょう
確かに弾を通すだけならこれで終わりでいいのですが、与えたダメージをベースに複数の機能が絡んでくるため、与えるダメージは貫通後の減衰したダメージではなく、そもそもそこに何もなかった時のダメージがいいのです
しょぼくれた顔で、フレームワークのドキュメントを読み漁ってみるも、もちろん貫通したという事実だけを魔法のように消すようなメソッドはフレームワークで用意されているはずもなく...
僕は狂ったようにネットの海や他のModdingをしているコミュニティの情報を漁ってみました
結果、今までに調べたりした中を含めても、そのようなことを誰かが実装したという形跡や情報は一切ありませんでした...
ところで、僕はこのModdingにおいて、唯一避けていたものが一つだけあります
それは、サーバーバイナリを解析して何かをフックしたりして、高度な機能を開発するということです
Moddingをしていると、どうしても普通には実装できないものは、サーバーバイナリをリバースエンジニアリングして特定の関数などを探し出してフックし、結果を編集したりする必要があります
そうすることで本来そうなるはずだった、という結果を自由に弄ることが出来るので、出来ることの幅が広がるのです
しかし、リバースエンジニアリングにはそれ相応の知識がないと非常に難しいことであり、怠惰な僕はそれをなるべくせずにたくさんの機能を頑張って実現してきました
でも、今回のケースは!!!どうしても!!!!これをしないと実現できない!!!!!しかもどうしても欲しい機能である!!!!!!
そして僕はバイナリ解析の沼へと踏み込んでいくのでした...ブラウザには、ググるためのGoogleと質問するためのAIを表示させながら...
事前準備
バイナリ解析するにあたって、解析するためのソフトウェアが必要になります
最近だとBinary Ninjaがいいとも聞いたりしますが、非常に高価なソフトウェアになります
有名なGhidraは無料ですが、結構癖があって慣れてないと扱いづらく、僕のような素人ではとても扱えたものではありません
あぁ、IDAの無料版が使えればな...と思ってホームページを見に行ってみると、なんと簡単に無料版が使用できるようになっていました
確か以前は、無料版を使うのに結構めんどくさい手順を踏まないといけないような記憶があったのですが、いつの間にかお手軽に使えるようになっていたようです
ということで、まずはIDAの無料版を入手するところから始めていきましょう!
hex-raysのida-freeページへ行ってみると、メールアドレスを入力できます
自分のメアドを入力し、"Request an IDA Free license key"をぽちっとしてみると、"Your IDA Free is ready!"と出てきます
こんな簡単に使わせてもらっていいんですか!?!?ありがとうございます!!!!
メールも飛んできますが、MyAccountボタンがあるので入力したメアドでLoginし、Licensesタブへ行くとFree Planのライセンスが見えます

クリックしてライセンスをダウンロードしておき、次にDownload centerタブから9.2のIDAをダウンロードしてInstallします
起動時に、ライセンスファイルを求められるので、ダウンロードしておいたhexlicファイルを選択し、無事認証されれば準備完了です!
あとは自分のサーバーから、cs2のサーバーバイナリである "libserver.so" を持ってきて、これで探索に出かける下準備が整いました
IDAと一緒にバイナリ探索
準備もできたとこで、いよいよIDAを起動して本腰を入れてバイナリを解析していきます
バイナリロード
IDAを起動すると、バイナリを選択してね的なダイアログが出てくるので、取ってきたバイナリを選択します

ELFであっているので、このままOKで進みましょう
なんか警告みたいなのが一瞬出ますが無視して進むと、

こんな感じで読み込んだ状態になります
ただ、解析はまだ完全に終わってないため、このまましばらく待たなければいけません

画面上側のバーっぽいところをよく見るとですね、このちっこい矢印が右にどんどん進んでいくのですが、何度か左から進みなおします
解析が進んでいる証拠になるので、このちっこい矢印がいなくなるのをしばらく待ちます
...
...
2000年ほど待ってみると、IDAがピロンと音を出してくれます、音が鳴ったら準備できたよ!の合図です!
ではさっそく解析していきましょう
解析解析~!
とはいえ、どこから進めればいいのでしょうか、という感じですね
色々やり方はあると思うのですが、今回は文字列をベースにフックしたい関数を探してみましょう
初期状態だと、Stringsタブがないので、まずはそれを表示するところから始めていきます
一番上のタブにViewがあるので、ここからsubviewと辿って、Stringsを選択します

するとこのように、バイナリにある文字列がリストとなっているのが見えます

ここからCtrl+Fで文字列を探していきますが、まずはやりたいことをおさらいしてみると、弾丸に関することでしたよね?
ということは、"Bullet"というワードで検索すればある程度絞れるのではないか、と推測しました
ヒット数は106個!!ちょっと多いですね😢
ですが仕方ありません、FPSゲームですから、弾に関する処理が多くてもなんら不思議ではありません
しょうがないので、全部舐めまわすように見てみて、ある程度目星をつけていきましょう...とやってると何日かかるかわからないので、ここで少しずるをしましょう
あまり大きい声で言えませんが、csには以前のバージョンのエンジンコードが実はleakされてしまっています
数年前の、しかも今のナンバリングより一個前なので現行のものとは大きく異なるでしょうが、内部で使用されているゲームエンジンと似通っているところがいくつもあります
そこで、leakコードを調べてみて、銃の弾丸処理に関するものを根気よく探してみると"GunFire"というクラスがエンジンに存在しているようで、その中の"FireBullet"というメソッドが弾丸を発射したときの処理をすべて担っているようです
ということで、"FireBullet"で改めて調べてみると、

ここまで絞ることが出来ました
ぱっと見どうみてもFX_FireBulletsに挟まれている
FireBullets @ %10f [ %s ]: inaccuracy=...
するとこのように、この文字列を参照している関数が DATA XREF:として出てきます
これをクリックすると、

このように、その関数の解析されたグラフに飛ぶことが出来ます
この画面でF5を押すと、

なんと!このようにCコードにすることができます!!!これなら機械語よりはある程度読みやすくなりましたね
でもこんなの見ても僕にはどんなコードかわかるわけがありません
ここで、AIくんの登場です
この関数は全部で712行もありますが、この程度ならAI君はすぐに解析してくれるでしょう
AIくんに適当に解析して!と投げると、どうやらleakコードの知識もあるようで、すぐにこれが弾丸処理だと見抜いてくれます
細かくコメントで解説してもらったのですが、処理のまとまりをコメントで置き換えてみると、
// sub_A0A260: 弾丸の発射と管理
__int64 __fastcall FireBullets(unsigned int a1, _QWORD *a2, /* ... 引数省略 ... */)
{
// ... (初期化、アイテム定義の取得など) ...
// 武器データの取得 (GetItemDefinition, GetWeaponEconDataFromItem)
v115.m128i_i64[0] = sub_15E84D0(v21);
if ( !v115.m128i_i64[0] )
return sub_8FFAD0("FX_FireBullets: GetItemDefinition failed\n");
// ... (デバッグ出力: "FireBullets @ %10f ... inaccuracy=%f spread=%f" など) ...
// ---------------------------------------------------------
// 弾丸のループ処理
// ショットガンのように複数の弾が出る場合や、貫通処理のためにループする
// ---------------------------------------------------------
if ( (int)v44 <= 0 )
{
// ...
}
else
{
v49 = 0; // ループカウンタ
do
{
// ... (弾丸ごとのランダムシード計算など) ...
// ---------------------------------------------------------
// 重要な呼び出し: TraceBullets (sub_10AE4C0)
// 弾丸1つ分の軌跡を計算し、衝突判定を行う
// ---------------------------------------------------------
if ( (unsigned int)sub_10AE4C0( /* たくさんの引数 */ )
{
// トレース結果に応じた処理 (着弾エフェクトなど)
}
++v49;
}
while ( *(_DWORD *)(v29 + 1848) > (int)v49 ); // 発射弾数分ループ
}
// ... (自分自身へのダメージ処理や後片付け) ...
return sub_1346E20(*(_QWORD *)&v57);
}
ざっとこんな感じになるようです
ここですでにお気づきかと思いますが、"重要な呼び出し"となっている関数があると思います
こいつは弾丸が何にヒットするのか、等を司っている部分で、僕が今回やりたいこともおそらくここをいい感じに弄ることで実現できると考えました
ではこのsub_10AE4C0を解析してみましょう
IDAの画面左のほうにFunctionsのViewがあるので、

これで直接関数へ行くことが出来ます
グラフへ飛んでF5を押して、それもそのままAIくんに投げてみましょう
// sub_10AE4C0: 弾道トレースの実行
__int64 __fastcall TraceBullets(__int64 a1, __int64 a2, /* ... 引数省略 ... */)
{
// ... (ベクトル計算、フィルタリング設定など) ...
// トレースループ
// 弾丸が進むにつれて、障害物に当たるたびに処理を行う
while ( 1 )
{
++v53;
v54 = &v250[v52];
// ---------------------------------------------------------
// 重要な呼び出し: BulletHandler (sub_10AC940)
// 衝突したオブジェクトに対する処理を行う。
// ダメージ計算、貫通力の減衰、貫通可能かの判定などがここで行われる。
// ---------------------------------------------------------
v55 = sub_10AC940(v190.m128i_i64[0], v191.m128i_i64[0], &v250[v52], v50, v46);
// 戻り値 v55 が非ゼロの場合、弾丸は停止する (貫通失敗、またはエネルギー切れ)
if ( v55 )
break;
// 次のトレースへ進む
v51 = v249;
v52 += 24;
if ( v249 <= v53 )
goto LABEL_119;
}
// ... (着弾後の処理、デカール貼り付け、イベント発行など) ...
}
おおー!!!さらに重要な呼び出しが見つかりましたね!!!
かなり省略してしまっていますが、sub_10AC940の呼び出しの前後の解析結果を見た感じ、弾が当たったのかどうかと貫通処理を含むダメージ計算をこいつが行っているようです
先ほどと同様にこの関数を検索し、CコードにしてからAIにさらに解析を進めさせます
// sub_10AC940: 弾丸の貫通・ダメージ処理
// a1: なんらかのポインタ, a2: BulletState構造体(ダメージ情報等), a3: ヒット情報
__int64 __fastcall BulletHandler(__int64 a1, float *a2, float *a3, unsigned int a4, int *a5)
{
// a2[0] = Damage?
// a2[4] = PenetrationCount?
// ... (距離によるダメージ減衰計算など) ...
// ---------------------------------------------------------
// 重要な呼び出し: HandleBulletHit (sub_108F220)
// ここでヒットしたエンティティの情報を取得・整理していると思われる
// ---------------------------------------------------------
sub_108F220(a1, v88, *(_QWORD *)(a1 + 8) + 48LL * *((unsigned __int16 *)a3 + 9), *a3);
// ... (貫通シミュレーションの計算) ...
// ---------------------------------------------------------
// 貫通判定ロジック
// ---------------------------------------------------------
// 貫通カウントの確認
v40 = *((_DWORD *)a2 + 4); // a2[4]: PenetrationCount
if ( !v40 )
goto LABEL_14; // 貫通残り回数が0なら停止
// 材質や貫通係数(Penetration Modifier)に基づく計算
// 弾丸のダメージ(v68)を減衰させる
v68 = v58 - _mm_blendv_ps((__m128)0LL, v62, _mm_cmpge_ss(v62, (__m128)0LL)).m128_f32[0];
*v6 = v68; // ダメージ更新
// ダメージが小さすぎたら停止
if ( v68 < 1.0 )
goto LABEL_20;
// 貫通カウントを減らす
--*((_DWORD *)v6 + 4); // PenetrationCount--
// 正常に貫通成功 (return 0)
*((_BYTE *)v6 + 20) = 0;
return 0;
LABEL_20:
// 貫通失敗・弾丸停止 (return 1)
*((_BYTE *)v6 + 20) = 1;
return 1;
}
もはやフックするべき関数はこれでしょう!!ビンゴです!!
ほんとにこの関数こういう処理してるんか!?という疑問はありつつも、現代のAIがそう判断したのだから試しに信じてみましょう
信じるとなれば、当初の目的である弾丸の貫通したという事実を捻じ曲げるためにはこの関数をフックすればよさそうです
また、この関数は戻り値もとても重要なようで、0/1で次のTraceをする・しないを制御できるようです
つまり、常に0を返しておけばどこまでも弾丸が飛んでいくことになります、世界を貫通する弾...!!!
関数の引数も重要で、a2はある程度推測して構造体を作れるものの、a3が何かがこの段階ではよくわかっていません
じっくりコードを解析してはみたものの、a3のフィールドがフラグのようなものも多く、さらにビット演算をたくさん駆使して何かを判定していたため、ここからHitしたであろうEntityの情報を取得するのは難しそうです
そこで、AIくんが判断してくれたもう一つの重要な関数について注目してみます
sub_108F220はHitした後のEntityに対して何らかの処理及び整理のようなものをしているような関数なんだそうです(ほんまか?)
ここは一旦信用してみて、この関数も解析させてみましょう(どうせ僕が読んでもわからんのだし、ゎら)
すると、
// sub_108F220: ヒット情報の処理
// 引数 a2 が TraceResult 構造体へのポインタに近い役割を果たす
__int64 __fastcall HandleBulletHit(__int64 a1, __int64 a2, __int64 a3, __m128i a4)
{
// ... (ヒット情報の展開) ...
// ヒットしたエンティティのインデックスやハンドルを取得する処理
v10 = *(unsigned int *)(a3 + 36);
// エンティティ判定ロジック
if ( (_DWORD)v10 == -1 || ... )
{
// ワールド(背景)ヒット時の処理など
}
else
{
// エンティティヒット時の処理
// ここで取得される情報が TraceResult に格納される
// traceResultPtr->HitEntity を参照できる?
}
// ... (衝突点の法線ベクトル計算、材質プロパティの取得など) ...
return result;
}
このような小さな関数のようです
ここでのポイントはa2で、こいつはTraceした結果、つまりHitしたEntityの結果情報が含まれていると推測されています
TraceFilterという構造体はすでに先人たちの解析班およびleakコードからある程度組み立てることが出来るのですが、ここではTraceの結果云々よりもHitしたEntityさえ取得できればOKです
つまり、最小限で実装するならば、
struct TraceResult
{
uint8_t m_pad0[0x08]; // 0x00 surface?
void* m_pHitEntity; // 0x08
uint8_t m_pad1[0xB8 - 0x10]; // padding
};
逆アセンブルしたコードがあっているならば、これだけでいいはずで、これにキャスト後 traceResult->m_pHitEntity のようにして取得できるはずです
HitしたEntityはこれで取得できるようになりましたが、問題は貫通後のダメージをどうするか、です
一個前の sub_10AC940 のコードへ戻って重要そうなものを一旦整理してみましょう
- ダメージ
- 距離減衰
- 貫通した回数
- 材質や貫通係数(Penetration Modifier)に基づく計算
- このHit後、さらにTraceを続けるかどうか
とりあえず重要そうなのはこの辺でしょう
当初の目的は、
プレイヤーすべて、または任意のエンティティを完全に貫通する弾丸処理の実装をしたい
でしたね
これはざっくり僕がTodoに書いたものであって、より細かく言うと、
- 貫通後はあたかも貫通してないかのように振舞ってほしい
- でも距離減衰によるダメージはそのまま適用させたい
- 特定のEntity以外のダメージ計算はそのままにしたい
というようなわがまま仕様になっております
これより、わかっていることと僕がやるべきことを整理してみると、
おおよそこのような流れでやりたいことが実現できると予想しました
sub_10AC940で必要な情報が入っているのは同じくa2なので、AIくんの解析結果を信用し、必要な構造体を構築してみると、
struct BulletState
{
float m_flDamage; // 0x00 これ
float m_pad0; // 0x04
float m_pad1; // 0x08
float m_pad2; // 0x0C
int m_nPenetrationCount; // 0x10 これ
uint8_t m_pad3; // 0x14
uint8_t m_pad4[3]; // 0x15
};
こんな感じでいけるはずです
ここまでで必要なフック箇所3つと必要な構造体の定義はそろいました、いよいよ実現させるために実装していきます
プラグインを実装する
さて、ここからは実際に動くプラグインを組んでいきます
いくつかフレームワークに選択肢はあるのですが、ちょうど最近リリースされた新しいフレームワークがあるので、ミーハーな僕は"新しい"というだけで惹かれてしまうものがあります
今回使用するフレームワークはこちら!じゃじゃーん: swiftlys2
こちらはcs2用のC#フレームワークになります
ここからはドキュメントとにらめっこしつつさくっと実装していきますよ~!みなさんお手元にVisual Studioを準備しなさい!!!!
早速ドキュメントを読んでみると、dotnet用のテンプレートを用意してくれてるようです、ありがたや

適当に入力しつつ、テンプレートをジェネレートしましょう
といいつつ、どうせならかっこいい名前にしたいので BulletMagic... とでもしましょうか!かっこよすぎだろ!!!!
dotnet new swplugin -n "BulletMagic" --PluginName "BulletMagic" --PluginAuthor "Anonymous" --PluginVersion "1.0.0"
これを実行すると、C#プロジェクトが生成されるので、Visual Studioを開いてコードを書いていきます
namespace BulletMagic;
[PluginMetadata(Id = "BulletMagic", Version = "1.0.0", Name = "BulletMagic", Author = "Anonymous", Description = "No description.")]
public partial class BulletMagic(ISwiftlyCore core) : BasePlugin(core)
{
public override void Load(bool hotReload)
{
}
public override void Unload()
{
}
}
初期コードはすっきりしていますね、何をすればいいかわからん...
とりあえず、さっき定義した構造体を定義するところから始めていきましょうか
先ほどはCコードの流れでCコードの構造体として定義してしまいました
まずはこれをC#へ書き直すところからです
C#には便利なアトリビュートがいっぱいあるので、活用していきますよ~~!!
[StructLayout(LayoutKind.Explicit, Size = 0xB8)]
public struct TraceResult
{
[FieldOffset(0x00)] public nint Padding;
[FieldOffset(0x08)] public IntPtr HitEntity;
}
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public struct BulletState
{
[FieldOffset(0x00)] public float Damage;
[FieldOffset(0x10)] public int PenetrationCount;
}
直接サイズやオフセットを指定できるので、めちゃくちゃすっきりしました!やはりC#は素晴らしい
そして、ダメージを記憶して巻き戻したりするための構造体も用意しておきたいですね
public class BulletContext
{
public BulletState Before10AC940;
public bool HasBefore;
public bool StepHasPhysbox;
public bool ShouldForceReturnZero;
public IntPtr LastHitEntityHandle;
public string? LastHitEntityDesignerName;
public uint LastHitEntityIndex;
}
これはそれぞれ後で使います
構造体の定義はこれで完成なので、次は関数フックの準備をしなければなりません
フックするためには、指定関数のシグネチャが必要になります
シグネチャの作り方はいくつかありますが、IDAにプラグインがあるのでそちらを利用しましょう
今回利用するのはこちら: IDA-Fusion
Releaseからダウンロードして、IDAのpluginsフォルダに直置きし、再起動します
再起動後、プラグインが読み込まれていると思うので、まずは3つの関数のグラフへ飛びます
そしたら関数の先頭へ頑張って移動して、Ctrl+Alt+Sを押すと、

こんなのが出てきます
IDA StyleのGenerateを選択すると、

このように、下のOutputのViewにこの関数のシグネチャが生成されます
これをフレームワークで使用するので、A0A260/10AC940/108F220でそれぞれシグネチャを生成してメモっておきましょう
Visual Studioへ戻り、フックするための準備をしましょう
フックについてのドキュメントを見ると、

となっているので、Delegateも必要ですね、3つともささっと準備しちゃいます
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate nint FireBulletDelegate(
nint a1,
// 引数多すぎて長いから省略...
);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate long Sub10AC940Delegate(
nint a1,
float* a2,
float* a3,
uint a4,
int* a5
);
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct M128i
{
[FieldOffset(0)]
public long Lo;
[FieldOffset(8)]
public long Hi;
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void* Sub108F220Delegate(
void* a1,
void* a2,
void* a3,
M128i a4
);
一部端折りました、FireBulletの引数多すぎるだろ!!25個もあるんですケド!!!!
また、108F220用にM128iを作ってるのもポイントです(もしかしたらほかにやり方あるかもだけど)
m128iはC#で対応する型が見つからなかったので、自作しました
delegateを定義したら、後はシグネチャを読みこみつつ、callする準備をしましょう
var FireBulletAddr = Core.Memory.GetAddressBySignature(Library.Server, "55 48 89 E5 41 57 4D 89 C7 41 56 49 89 D6 41 55 41 54 49 89 CC");
if (FireBulletAddr is { } fbp)
{
var FireBulletFunc = Core.Memory.GetUnmanagedFunctionByAddress<FireBulletDelegate>(fbp);
FireBulletFunc.AddHook((next) =>
{
return (a1 // ...) =>
{
_isBulletTrace = true;
var r = next()(a1 // ...);
_isBulletTrace = false;
return r;
};
});
}
var sub_108F220 = Core.Memory.GetAddressBySignature(Library.Server, "55 48 89 E5 41 57 66 41 0F 7E C7 41 56 41 55 49 89 FD 48 89 F7");
if (sub_108F220 is { } sub_108F220_ptr)
{
var sub_108F220_func = Core.Memory.GetUnmanagedFunctionByAddress<Sub108F220Delegate>(sub_108F220_ptr);
sub_108F220_func.AddHook((next) =>
{
return (a1, a2, a3, a4) =>
{
var r = next()(a1, a2, a3, a4);
if (a2 is not null)
{
var tr = (TraceResult*)a2;
// TODO
}
return r;
};
});
}
var sub_10AC940 = Core.Memory.GetAddressBySignature(Library.Server, "55 66 0F EF D2 48 89 E5 41 57 41 56 41 55 41 54 49 89 F4 53 48 81 EC");
if (sub_10AC940 is { } sub_10AC940_ptr)
{
var sub_10AC940_func = Core.Memory.GetUnmanagedFunctionByAddress<Sub10AC940Delegate>(sub_10AC940_ptr);
sub_10AC940_func.AddHook((next) =>
{
return (a1, a2, a3, a4, a5) =>
{
if (!_isBulletTrace)
{
return next()(a1, a2, a3, a4, a5);
}
var bulletInfo = (BulletState*)a2;
if (a2 != null)
{
// TODO
}
var r = next()(a1, a2, a3, a4, a5);
if (a2 != null)
{
// TODO
}
return r;
};
});
}
こんな感じで出来上がりました、後はダメージの巻き戻しさえ実装できればよさそうです
cs2になってからはゲームループはシングルだが、ネットワークスレッドがマルチスレッドになっていると聞きます
なので、弾丸処理もEntityを扱うことから別スレッドで送信される可能性もあるため、念のためAsyncLocalにコンテキストを保存するようにしておきましょう
後はこれをFireBulletのPre/Postで生成と破棄をするだけです
public static class BulletDamageManager
{
internal static AsyncLocal<BulletContext?> CurrentBullet = new();
public static void OnFireBulletsPre()
{
CurrentBullet.Value = new BulletContext();
}
public static void OnFireBulletsPost()
{
CurrentBullet.Value = null;
}
// ...
といういことで、フックのFireBulletFuncを編集して、
_isBulletTrace = true;
BulletDamageManager.OnFireBulletsPre();
var r = next()(a1 //...);
_isBulletTrace = false;
BulletDamageManager.OnFireBulletsPost();
return r;
こんな感じにしておきましょうか
フラグはコンテキストの有り無しでもよさそうですが、そこまでコストが高いわけでもないので一旦このままにしておきます
次に、HitしたEntityに関しての処理を実装していきましょう
これはSub108F220のフックにて実現することができるはずです
public static class BulletDamageManager
{
// ...
public static void OnSub108F220Hit(TraceResult* traceResultPtr)
{
var bullet = CurrentBullet.Value;
if (bullet == null || traceResultPtr == null)
return;
var ent = traceResultPtr->HitEntity != 0
? Helper.AsSchema<CBaseEntity>(traceResultPtr->HitEntity)
: null;
if (ent == null)
return;
bullet.LastHitEntityHandle = traceResultPtr->HitEntity;
bullet.LastHitEntityDesignerName = ent.DesignerName;
bullet.LastHitEntityIndex = ent.Index;
if (ent.DesignerName == "func_physbox")
{
bullet.StepHasPhysbox = true;
bullet.ShouldForceReturnZero = true;
}
}
.DesignerNameはEntityの種類としての名前です
現在の弾丸コンテキストに、HitしたEntityの情報を格納していき、physboxの時だけフラグを立てるといった感じです
Entityを扱う際はtryで囲ったりしたほうがいいですが、Nullチェックもしっかり行ってるのでまぁいいでしょう...たぶん...
これを、sub_108F220_funcでTODOだった部分へ追加します
var r = next()(a1, a2, a3, a4);
if (a2 is not null)
{
var tr = (TraceResult*)a2;
BulletDamageManager.OnSub108F220Hit(tr);
}
return r;
これで弾丸処理中にHitしたEntityを取得することができました!もう少しで完成です!
最後に、10AC940用のダメージ巻き戻し処理を実装すればOKです
ここはちょっとめんどくさいですが、Pre/Postそれぞれが必要になります
というのも、
- 貫通処理をする前に弾丸の状態を覚えておきたい
- 処理した後に、前回がphysboxであるというフラグが立っているかつ前回状態があれば巻き戻す
という流れだからですね
public static void OnSub10AC940Pre(BulletState* a2)
{
var bullet = CurrentBullet.Value;
if (bullet != null && a2 != null)
{
bullet.Before10AC940 = *a2;
bullet.HasBefore = true;
}
}
public static void OnSub10AC940Post(BulletState* a2)
{
var bullet = CurrentBullet.Value;
if (bullet == null || a2 == null)
return;
if (bullet.StepHasPhysbox && bullet.HasBefore)
{
var cur = a2;
var before = bullet.Before10AC940;
if (cur->Damage < before.Damage)
{
cur->Damage = before.Damage;
}
if (cur->PenetrationCount < before.PenetrationCount)
{
cur->PenetrationCount = before.PenetrationCount;
}
bullet.StepHasPhysbox = false;
bullet.HasBefore = false;
}
}
ざっくりこんな感じでしょう
もっといい書き方ややり方はあると思いますがうごけりゃいいのですよ~!!!!!
あとはこれをsub_10AC940_funcのTODO部分へ組み込むだけです
if (!_isBulletTrace)
{
return next()(a1, a2, a3, a4, a5);
}
if (a2 != null)
{
BulletDamageManager.OnSub10AC940Pre((BulletState*)a2);
}
var r = next()(a1, a2, a3, a4, a5);
if (a2 != null)
{
BulletDamageManager.OnSub10AC940Post((BulletState*)a2);
var bullet = BulletDamageManager.CurrentBullet.Value;
if (bullet != null && bullet.ShouldForceReturnZero)
{
bullet.ShouldForceReturnZero = false;
// Force return 0 to skip physbox bullet blocking
return 0;
}
}
return r;
できました!理論上はこれで完成のはずです!!理論上はね!!!
AIの解析結果を全信頼しているのでちょっと疑わしいけど!!!!!
動作ちぇ~~~~っく
さて、最もフィジカルで、最もプリミティブで、そして最もフェティッシュなやり方で実装した素晴らしいぼくの "BulletMagic..." プラグインが完成したので、実際にサーバーを起動して動作を確認してみましょう
敵プレイヤーはGlowさせてphysbox越しで見えるようにし、ダメージに応じてノックバック発生と撃った人に与えたダメージ分のお金が入るようにして軽くチェックしてみます
まずは通常の挙動から
そしてプラグイン導入後
比較してみると一目瞭然ですね!!!
Beforeではそもそも全く弾が通らないのに対し、プラグイン導入後のAfterではちゃんとダメージが通っています!!お金も入ってきてる!!!
ダメージ数値は、この武器ですと胴体がだいたい25-27くらいなので、gifだと見づらいですがちゃんとそれくらいのお金が入ってきているのがわかります
ヘッドショット計算もそのまましっかり反映されてるし、ノックバックで距離があいた分でダメージが少し減ってることも確認できました
Trace対象はそのままなので、見た目のエフェクト的にはphysboxにそもそも当たってしまっているのですが、動作自体は期待動作なのでこれでいいとしましょう
見た目も完ぺきにするなら記事冒頭で話したCollision Ruleを適用すればいいですが、Entityの特性を変えると悪い影響があることもあるので、副作用も少なく実装できたことになります
技術ってなんでもできるんだ...素晴らしいですね
まとめ
無事にAIと二人三脚しながら自分のやりたいことを実現することが出来ました!
AIの進化は本当に目まぐるしく、僕一人ではそもそも解析時点で挫折していたことでしょう
リークコードの存在も大きいとは言え、AIと一緒に高度な部分をこんなにもあっさりと解決できてしまったことに正直とてもびっくりしてしまいました
これからエンジニアに求められるのは単なる技術力や実装力ではなく、解決力、なのでしょうか...
そういえば、うちのCTOもこんなことを記事で言っていました
AIによって、研究がより多くの人にとって “楽しめるコンテンツ” になる。
そして研究の面白さそのものが、もっと広く届くようになる。
今年は、そんな未来が一歩近づいたと感じた一年でした。
これは本当にそうで、今まで人の手では難しかった・面倒だったことなどがAIをうまく活用することでぐっとやりやすくなってきています
趣味では頭で思い描いている実装を、AIにコードを起こしてもらって、自分で手直ししつつ完成させるということをよくするようにしていますが、明らかに以前よりも作業効率が上がっています
まぁ、結構バグを埋め込んだり想定外の挙動もすることがありますが...総合的には活用してよかったと思うことのほうが多いです
みなさんも、AIをうまくツールとして活用していき、今まで出来なかったようないろいろなことに挑戦してみればなと思います!!
今年もありがとうございました!!よいお年を~~~~~🎄
~番外編~
記事が終わったと思った???残念!!
本編と関係こそありますが裏話的なトピックをここで消化させていただきます
※実は、もっと楽そうなアプローチも最初に試していて、それについての話を少し。
見たい人はクリック!
記事をちゃんと読んで勘のいいひとなら気づいたと思うのですが、
そもそもphysboxのEntity自体がTrace対象として除外するような処理を挟めばこんなめんどくさいことをしなくてもいいのでは?
大正解です、どう考えてもそうですよね
実は僕も当初はそれに挑戦していたのですが...どうして出来なかったのか語っていきます
まず、10AE4C0を少しおさらいすると
// sub_10AE4C0: 弾道トレースの実行
__int64 __fastcall TraceBullets(__int64 a1, __int64 a2, /* ... 引数省略 ... */)
{
// ... (ベクトル計算、フィルタリング設定など) ...
これが最初のほうですね、ここに
ベクトル計算、フィルタリング設定など
おっと!いかにもそれらしい処理が紛れ込んでそうですよね!?
はい、ビンゴでして、ここで弾丸が何に当たるのか、の準備をしています
リークコードより、従来のcsのエンジンではTrace時、ShouldHitEntityのようなメソッドが存在していました
こいつはVTableのメソッドで、TraceのFilter構造体の一つ目のメソッドとして存在していたことが知られています、こんな感じで↓

つまりここをフックすればいいのでは?当初は僕もそう思ったのですが、
10AE4C0にはそれらしい仮想テーブルメソッドの呼び出しがないことに気づきました
一応物は試しということで、関数内にFilter構造体を組み立てている場所があることはわかっているので、そちらを直接フックし、FilterのVTableを差し替えるフックをしてみましたが、デバッグ用のPrintは一切呼ばれなかったのです...
という経緯もあり、僕はTrace周りを深堀することをやめましたとさ...
おしまいです!ここまで読んでくれてありがとう!!!




