# ATSプラグイン 制作メモ ###### tags:`BveTs` ATSプラグインを初めて作る方に少しでも参考になればと思います。 ->[DXDynamicTexture(C#)の使い方](https://hackmd.io/@Nepia/HkcnsZ4yq) ->[C#で作るATSプラグイン](https://hackmd.io/@Nepia/H1uLvcIs5) ->[AtsEXを使ったプラグインを作成する方法](https://hackmd.io/@Nepia/H1kgSaVCs) new!(23/02/23) ## 目次 [TOC] ::: info 解説してほしい所があればDM等でご連絡ください 随時更新していきます twitter:@Tn_E235 ::: --- ## 準備 0. [Visual Studio 2019](https://visualstudio.microsoft.com/ja/vs/)をインストールします 1. [公式サイト](https://bvets.net/jp/edit/formats/)からプラグインのソースをダウンロードします 2. ats1-2.zipを解凍して、Ats.vcprojをダブルクリックして起動します 3. そのままビルドして(ctl+B)成功すれば完了です --- ## プロジェクトの設定 [こちらのサイト](http://ct-813.hatenablog.jp/)にて画像付きで解説されています。 以下、自分用メモです。 ### モジュール定義ファイルの作成と参照の設定 :::spoiler 1. 上部メニューバー「プロジェクト」から「プロパティ」を選択します。 2. ページ左上、構成(C):[すべての構成]にします。 3. 左サイドメニュー「構成プロパティ」->「リンカー」->「入力」まで展開します。 4. メインメニュー内の「モジュール定義ファイル」を「Ats.def」に設定します。 5. 設定後、画面右下の「適用」をクリックします。 ### dllexportの設定 :::spoiler 1. 上部メニューバー「プロジェクト」から「プロパティ」を選択します。 2. ページ左上、構成(C):[すべての構成]にします。 3. 左サイドメニュー「構成プロパティ」->「C/C++」->「プリプロセッサ」まで展開します。 4. メインメニュー内の「プリプロセッサの定義」を「ATS_EXPORTS」に設定します。 5. 設定後、画面右下の「適用」をクリックします。 ::: ### デバッガーの設定 :::spoiler 1. 上部メニューバー「プロジェクト」から「プロパティ」を選択します。 2. ページ左上、構成(C):[すべての構成]にします。 3. 左サイドメニュー「構成プロパティ」->「デバッグ」まで展開します。 4. メインメニュー内の「コマンド」を”BveTs.exeまでのパス”に設定します。 入力例:E:\Users\Tn-E235\Games\mackoy\BveTs6.0\BveTs.exe 5. 設定後、画面右下の「適用」をクリックします。 ::: ### Release用の設定 :::spoiler 1. 上部メニューバー「プロジェクト」から「プロパティ」を選択します。 2. ページ左上、構成(C):[Release]にします。 3. 左サイドメニュー「構成プロパティ」->「リンカー」->「デバッグ」まで展開します。 4. メインメニュー内の「デバッグ情報の生成」を「いいえ」に設定します。 5. 設定後、画面右下の「適用」をクリックします。 ::: ### 64bitでビルドする設定 :::spoiler 1. 上部メニューバー「プロジェクト」から「プロパティ」を選択します。 2. ページ右上、構成マネージャーをクリックします。 3. ページ右上、「アクティブ ソリューション プラットフォーム」の下のプルダウンメニューを展開し、新規作成をクリックします。 4. 「新しいプラットフォームを入力または選択してください」の下のプルダウンメニューを展開し、「x64」を選択します 5. 設定元のコピー元を「x86」にします。 6. 画面下部の「OK」をクリックします。 7. 「構成マネージャー」ウィンドウのプロジェクトのコンテキスト表内の、プラットフォームを「Win32」から「x64」に変更します。 8. 画面右下「閉じる」、「OK」をクリックします。 ::: --- ## ほぼ空のプログラムにする 以下のファイルを削除します。 - AtsP.h - AtsSn.h - Spp.h - AtsP.cpp - AtsSn.cpp - Spp.cpp ファイルを削除した状態でビルドするとエラーが出るので、エラーを解消していきます。 基本的に赤線が表示されている箇所を削除すればOKです。 1. Ats.hファイル内、32~34行目を削除します。 :::spoiler ```cpp=32 CAtssn g_atssn; // ATS-SN CAtsp g_atsp; // ATS-P CSpp g_spp; // 停車駅通過防止装置 ``` ::: 2. Ats.cppファイル内、6~8行目を削除します。 :::spoiler ```cpp=6 #include "Atssn.h" #include "Atsp.h" #include "Spp.h" ``` ::: 3. Ats.cppファイル内、13~33行目を削除します。 :::spoiler ```cpp=13 switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: g_atssn.Time = &g_time; g_atssn.Notch = &g_brakeNotch; g_atsp.TrainSpeed = &g_speed; g_atsp.DeltaT = &g_deltaT; g_atsp.BrakeNotch = &g_brakeNotch; g_atsp.Reverser = &g_reverser; g_spp.TrainSpeed = &g_speed; g_spp.DeltaT = &g_deltaT; g_spp.BrakeNotch = &g_brakeNotch; break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } ``` ::: 4. Ats.cppファイル内、25~29行目を削除します。 :::spoiler ```cpp=25 g_atssn.EmgNotch = g_emgBrake; g_atssn.CancelNotch = vehicleSpec.AtsNotch; g_atsp.EmergencyNotch = g_emgBrake; g_atsp.ServiceNotch = vehicleSpec.AtsNotch; g_spp.ServiceNotch = vehicleSpec.AtsNotch; ``` ::: 5. Ats.cppファイル内、29~31行目を削除します。 :::spoiler ```cpp=29 g_atssn.InitSn(); g_atsp.InitP(); g_spp.InitSpp(); ``` ::: 6. Ats.cppファイル内、38~40行目を削除します。 :::spoiler ```cpp=38 g_atssn.RunSn(); // ATS-SN g_atsp.RunP(); // ATS-P g_spp.RunSpp(); // 停車駅通過防止装置 ``` ::: 7. Ats.cppファイル内、38~58行目を***次のように変更***します :::spoiler ```cpp=38 /* ハンドル出力 たぶんここ要らないです。 if(g_brakeNotch != g_emgBrake) { g_output.Brake = g_svcBrake; } else { g_output.Brake = g_brakeNotch; } */ if(g_pilotlamp) { g_output.Reverser = g_reverser; } else { g_output.Reverser = 0; } ``` :::danger ここの部分を削除するとマスコンの状態が反映されません。 ::: 8. Ats.cppファイル内、58~73行目を***次のように変更***します :::spoiler ```cpp=58 // パネル出力 panel[0] = 0; // サウンド出力 sound[0] = 0; ``` ::: 9. Ats.cppファイル内、75~76行目を削除します。 :::spoiler ```cpp=75 g_atssn.CheckAts(); g_spp.NotchChanged(); ``` ::: 10. Ats.cppファイル内、84~96行目を削除します。 :::spoiler ```cpp=84 switch(atsKeyCode) { case ATS_KEY_S: // ATS 確認 g_atssn.DownButtom(); break; case ATS_KEY_A1: // 警報持続 g_atssn.CancelChime(); break; case ATS_KEY_B1: // 復帰 g_atssn.Reset(); g_atsp.Reset(); break; } ``` ::: 11. Ats.cppファイル内、88行目を削除します。 :::spoiler ```cpp=88 if(hornType == ATS_KEY_S){g_atssn.UpButton();} ``` ::: 12. Ats.cppファイル内、97行目を削除します。 :::spoiler ```cpp=97 g_spp.StopChime(); // 停通のチャイムを止める ``` ::: 13. Ats.cppファイル内、110~142行目を削除します。 :::spoiler ```cpp=110 switch(beaconData.Type) { case ATS_BEACON_S: // Sロング g_atsp.Exit(); if(g_speed != 0){g_atssn.PassedLong(beaconData.Signal);} // 駅ジャンプを除外する break; case ATS_BEACON_SN: // SN直下 g_atsp.Exit(); g_atssn.PassedShort(beaconData.Signal); break; case ATS_BEACON_P: // 停止信号 g_atssn.TurnOff(); g_atsp.PassedSig(beaconData.Distance); break; case ATS_BEACON_EMG: // 即停(非常) g_atssn.TurnOff(); g_atsp.PassedStopEmg(beaconData.Distance); break; case ATS_BEACON_SVC: // 即停(常用) g_atssn.TurnOff(); g_atsp.PassedStopSvc(beaconData.Distance); break; case ATS_BEACON_SPDLIM: // 分岐器速度制限 g_atssn.TurnOff(); g_atsp.PassedSpeedLim(beaconData.Optional % 1000, beaconData.Optional / 1000); break; case ATS_BEACON_SPDMAX: // 最高速度 g_atssn.TurnOff(); g_atsp.PassedSpeedMax(beaconData.Optional); break; case ATS_BEACON_SPP: // 停車駅通過防止装置 g_spp.Receive(beaconData.Optional); } ``` :::success お疲れ様です。これでビルドして成功すれば完了です。 ::: ### 不足している関数を追加する 64bitでビルドしたプラグインを単体で使用する際に、Bve本体が落ちる現象が発生します。 今回使用しているテンプレートは、とてもふるーいので、以下の関数を追加します。 - Load - Dispose atsplugin.hの最下部に追加 ```cpp=144 // このプラグインがBVEによって読み込まれた時に呼び出される。 ATS_API void WINAPI Load(); // このプラグインがBVEから解放された時に呼び出される。 ATS_API void WINAPI Dispose(); ``` Ats.cppの最下部に追加 ```cpp=143 ATS_API void WINAPI Load() {} ATS_API void WINAPI Dispose() {} ``` Ats.defの最下部に追加 ```cpp=18 Load Dispose ``` <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">あれ、DetailManagerってLoad/Dispose無くても動くんだっけ</p>&mdash; あすくとついでに846人のきつね (@AskED757001) <a href="https://twitter.com/AskED757001/status/1282671306593366017?ref_src=twsrc%5Etfw">July 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> --- ## パネル出力をしてみる `Elaps関数`内の`panel配列`に値(整数型)を入れることでパネル出力されます。 `panel`配列の要素数は$256(0~255)$になっていて、要素番号がパネルインデックスに対応しています。 例えば、ats0の画像を表示させたい場合、 ```cpp=1 panel[0] = 1; ``` とすることで、出力$1$に対応した画像が表示されます。 これができれば何でもできます(何でもできるとは言っていない) --- ## 音声出力をしてみる `Elaps関数`内の`sound`配列に値(整数型)を入れることでパネル出力されます。 `sound`配列の要素数は$256(0~255)$になっていて、要素番号がサウンドインデックスに対応しています。 例えば、ats0の音声を再生させたい場合、 ```cpp=1 sound[0] = ATS_SOUND_PLAY; ``` とします。再生開始の次フレーム以降は、 ```cpp=1 sound[0] = ATS_SOUND_CONTINUE; ``` とします。わからんと思うけど、がんばれ。イメージとしてはこんな感じ。 ```cpp=1 sound[0] = ATS_SOUND_CONTINUE if (再生しますか()) Sound[0] = ATS_SOUND_PLAY; ``` --- ## 地上子から情報を受け取る 地上子(Beacon)を通過すると、`SetBeaconData`関数が呼ばれます。 ATS_BEACONDATAから以下の情報がとれます。 | 取れる情報 | 型 | 説明 | | -------- | -------- | -------- | | Type | int | 地上子番号 | | Signal | int | セクションに対応する信号番号 | | Distance | float | 対応閉塞までの閉塞距離 | | Optional | int | 地上子の第3引数(sendData)| 地上子構文:`distance;Beacon.put(type,section,sendData);` Typeで条件分岐させて処理すればOKです。かんたんですね。 地上子で小数の値を入れるとintになる?(切り捨て?) :::info ATS_BEACONDATAは構造体なので ```cpp int type = beaconData.type; ``` な感じで情報がとれます ※beaconDataはsetBeaconData関数の引数 ::: :::warning 地上子を踏んだ順番(記述順)に読み込まれます ::: --- ## キー入力を受け取る 保安装置キーで設定されたキーが押下されたとき`KeyUp`関数、離されたとき`KeyDown`関数が呼ばれます。引数で押されたキー番号が入ってくるので、条件分岐させて処理してください。 一定時間押下すると長押しとして処理されます。(毎フレーム呼ばれる、だぶん) | 種類 | 番号 | 保安装置キー | キーボード | | :-: | :-: | :-: | :-: | | ATS_KEY_S | 0 | S Key | Space | | ATS_KEY_A1 | 1 | A1 Key | Insert | | ATS_KEY_A2 | 2 | A2 Key | Delete | | ATS_KEY_B1 | 3 | B1 Key | Home | | ATS_KEY_B2 | 4 | B2 Key | End | | ATS_KEY_C1 | 5 | C1 Key | PageUp | | ATS_KEY_C2 | 6 | C2 Key | PageDown | | ATS_KEY_D | 7 | D Key | 2 | | ATS_KEY_E | 8 | E Key | 3 | | ATS_KEY_F | 9 | F Key | 4 | | ATS_KEY_G | 10 | G Key | 5 | | ATS_KEY_H | 11 | H Key | 6 | | ATS_KEY_I | 12 | I Key | 7 | | ATS_KEY_J | 13 | J Key | 8 | | ATS_KEY_K | 14 | K Key | 9 | | ATS_KEY_L | 15 | L Key | 0 | --- ## 外部ファイルを読み込む ### 1. 手っ取り早くiniファイルを読み込ませたい まだini使ってんのかって誰かに言われた記憶 #### 事前準備 1. [inimoni](https://www.vector.co.jp/soft/dl/winnt/prog/se401738.html)をダウンロードする 2. 取説道理にソースを生成する ※ .iniのファイル名はプラグインと同じ名前にしておきましょう ※ デフォルト値が.iniで設定した値になります #### テンプレに組み込む インポートかなんか生成された.hファイルを取り込む ソリューションなんたらで新しいファイル作ってコピペしても良い(ボクはこっち派) ##### Ats.hに変数定義 inimoniで生成されたヘッダファイルがiniRead.hの場合 ```h= iniRead ini; ``` ##### Ats.cppに組み込む ###### 1. ヘッダファイルをinclude ```cpp= #include "iniRead.h" // #include "Ats.h"の上に書く ``` ###### 2. DllMainにおまじないを書く Ats.dllまでのパスを取得してゴニョゴニョ... ```cpp=1 //ファイルパス格納 char filePath[_MAX_PATH + 1] = _T(""); //検索文字列へのポインタ char* posIni; //Ats.dllのファイルパスを取得 ::GetModuleFileName((HMODULE)hModule, filePath, _MAX_PATH); //パスから.dllの位置を検索 posIni = strstr(filePath, ".dll"); //.dllを.iniに置換 memmove(posIni, ".ini", 4); ``` #### セクションに対する各項目の値を取得する iniファイルの設定例 ```ini=1 [PANEL] index0 = 200 index1 = 201 [SOUND] index0 = 1 index1 = 2 ``` PANELセクションのindex0の値を取得する ```cpp= int index0 = ini.PANEL.index0; ``` #### double型の扱いのバグ inimoniでdouble型を指定すると,ただしく扱われないらしいので以下の方法で修正する. 修正方法は[こちら](https://bvews.jpn.org/articles/notice-on-using-inimoni.html) ### 2. csvを読み込ませたい 神的な解説は[こちら](http://ct-813.hatenablog.jp/) ### 3. csvを読み込ませた 時間があったら実装してみる →[した](https://hackmd.io/@Nepia/SyFCJ9KBF) --- ## サンプルプログラム とりあえず簡単なソースを必要な部分だけ載せておきます。 ### 現在の速度を一桁ずつ出力する :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ float speed = vehicleState.Speed; panel[0] = (int)speed % 100; panel[1] = (int)speed / 10 % 10; panel[2] = (int)speed % 10; ~略~ } ``` ::: ### 現在の時刻を時分秒で出力する :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ // 現在時刻はミリ秒で取得されるので秒に直します int time = vehicleState.Time % 86400000 / 1000 % (60*60*24); // 秒を時分秒に直します int time_h = time / 3600; int time_m = (time - time_h * 3600) / 60; int time_s = time % 60; panel[3] = time_h; panel[4] = time_m; panel[5] = time_s; ~略~ } ``` <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ATSPI制作Tips:日付を跨ぐデータはvehicleState.Timeはそのまま積算されるので時刻を表示する場合は86400000で割った余りの値を使うとよい</p>&mdash; CT (@ct_813) <a href="https://twitter.com/ct_813/status/1282183127176146947?ref_src=twsrc%5Etfw">July 12, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> ::: info $24h = 60 * 60 * 24 = 86400s = 86400000ms$ ::: ### 128番地上子を踏んだら音を鳴らす :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ sound[0] = (ATS_BELL) ? ATS_SOUND_PLAY : ATS_SOUND_CONTINUE; ATS_BELL = false; ~略~ } ATS_API ATS_HANDLES WINAPI setBeaconData ( ATS_BEACONDATA beaconData){ // 地上子の番号を取得 int type = beaconData.type; // 地上子のsendDataを取得 int sendData = beaconData.optional; if (type == 128) { // 再生フラグ(グローバル変数) ATS_BELL = true; } } ``` ::: ### スペースキーを押している間,音を鳴らす :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ // no debug if (ATS_BUZZER) { if (sound[1] == ATS_SOUND_PLAYLOOPING) { sound[1] = ATS_SOUND_CONTINUE; } else { sound[1] = ATS_SOUND_PLAYLOOPING; } } else { sound[1] = ATS_SOUND_STOP; } ~略~ } ATS_API ATS_HANDLES WINAPI keyDown (int key){ if (ATS_KEY_S == key) { // 再生フラグ(グローバル変数) ATS_BUZZER = true; } } ATS_API ATS_HANDLES WINAPI keyUp (int key){ if (ATS_KEY_S == key) { // 再生フラグ(グローバル変数) ATS_BUZZER = false; } } ``` ::: ### 0.5秒毎に点滅させる(簡易) :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ deltaT = vehicleState.Time - time; time = vehicleState.Time; int INTERVAL = 500; // 500[ms] if (INTERVAl <= totalTime) { panel[3] = Math.abs(panel[3]-1); totalTime = 0; } else { totalTime += delta_T; } ~略~ } ``` ::: ### 0.5秒毎に点滅させる :::spoiler ```cpp=1 ATS_API ATS_HANDLES WINAPI Elapse ( ATS_VEHICLESTATE vehicleState, int *panel, int *sound){ ~略~ deltaT = vehicleState.Time - time; time = vehicleState.Time; int INTERVAL = 500; // 500[ms] if (time >= switchTime + INTERVAL) { panel[3] = Math.abs(panel[3]-1); switchTime += INTERVAL; } ~略~ } ``` ::: ### 指定した桁の数値を取得する関数 :::spoiler ```cpp=1 int getDigitOfNumber(int num, int digit, int rt) { int n = 0; for (int i = 0; i < 10; ++i) { if (num / pow(10, i) < 1) { n = i; break; } } if (digit > n) return rt; int result = (int)(num / pow(10, digit - 1)) % 10; if (digit >= n && result == 0) return rt; return result; } ``` 引数 - num:入力値 - digit:取得したい桁 - rt:指定してた数字以上の桁が存在しないときの返り値 ::: --- ## デバッグの方法 まずはじめに,デバッガーの設定を行ってください. 1. 「Debug」状態でビルドします. 2. ビルドして生成された`Ats.dll`をデバッグ用の車両に組み込みます 3. コードの任意の箇所にブレークポイントを設定します(行番号の横をクリック) 4. 「ローカルWindowsデバッガー」をクリックするとデバッグが開始し,Bveが起動します 5. デバッグ用の車両が指定されたシナリオをロードします 6. ブレークポイントにした行が呼ばれたとき,現在の変数とかが見れます デバッグは大変...がんばろう <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">小ネタ<br>dllの出力先をデバックしたい車両にするっていうのが常套手段<br>でも個人的には出力先はそのままdll本体だけシンボリックリンクを張るのがおススメ</p>&mdash; 停P@R (@stop_pattern) <a href="https://twitter.com/stop_pattern/status/1285621969753440257?ref_src=twsrc%5Etfw">July 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> :::info 私の環境ではBveTs5.7ではデバッグができませんでした(謎) Bvets6.0RCではデバッグができました ::: --- ## 各関数の説明 ### DllMain(HANDLE, DWORD, LPVOID) メイン関数 ### GetPluginVersion() Atsプラグインのバージョンを返す関数。何に使うのかは知らない。 ### SetVehicleSpec(ATS_VEHICLESPEC) 車両データが読み込まれたときに呼ばれる関数。 - 第1引数:車両パラメータが入った構造体 ### Initialize(int) 初期化時に呼ばれる関数。シナリオの読み込み、始発駅に戻る際に呼ばれるが、駅ジャンプでは呼ばれない。 - 第1引数:ブレーキ段数 ### Elapse(ATS_VEHICLESTATE, int*, int*) 毎フレーム呼ばれる関数。 - 第1引数:車両の状態が入った構造体 - 第2引数:パネル出力用配列(ポインタ) - 第3引数:サウンド出力用配列(ポインタ) ### SetPower(int) 力行段数が変化したときに呼ばれる関数。 - 第1引数:Pノッチ段数 ### SetBrake(int) 制動段数が変化したときに呼ばれる関数。 - 第1引数:Bノッチ段数 ### SetReverser(int) レバーサーが変化したときに呼ばれる関数。 - 第1引数:レバーサーの状態 ### KeyDown(int) キーが押されたときに呼ばれる関数。 - 第1引数:キー番号 ### KeyUp(int) キーを離したときに呼ばれる関数。 - 第1引数:キー番号 ### HornBlow(int) 警笛を押したときに呼ばれる関数。 - 第1引数:キー番号 ### DoorOpen() ドアが開いたときに呼ばれる関数。 ### DoorClose() ドアが閉まったときに呼ばれる関数。 ### SetSignal(int) 現在の閉塞の信号現示が変化したときに呼ばれる関数。 - 第1引数:信号番号 ### SetBeaconData(ATS_BEACONDATA) 地上子を踏んだ時に呼ばれる関数。 - 第1引数:地上子の情報が入った構造体 ### Load() プラグインが本体に読み込まれたときに呼ばれる関数。 ### Dispose() プラグインが本体から解放されたときに呼ばれる関数。