# 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>— あすくとついでに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>— 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>— 停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()
プラグインが本体から解放されたときに呼ばれる関数。