更新日2025/02/08

asl ファイルの記述例

具体的な記述例があった方が分かり易いかなと思ったので、asl ファイルの作り方の補足資料としていくつか例を示します。

もくじ

注意事項

あくまで自己責任

この記述例の利用はあくまで自己責任でお願いします。
あくまで記述例なので、そのままコピペしても動くことを保証できません。

ファイルの文字コード

asl ファイル自体の文字コードは UTF-8 に設定しましょう。
 私の経験では Shift-JIS でも半角文字のみで記述しているなら正しく動作するのですが、日本語でコメントを入れると全く別の行でエラーが発生して原因の特定に無駄な時間を取られる場合があります。

サンプルスクリプトの前提条件

この例では、次のような条件があるものとします。

  • ゲーム内で部屋移動したタイミングでタイマー操作する
  • 部屋ごとに異なる ID が設定されている
  • 部屋の ID は先に進むほど大きい値になる
  • タイトル画面にも ID が設定されていて、部屋 ID と同じメモリアドレスで管理されている

基本的な asl の例

基本的な Autosplitter

サンプルスクリプト
Autosplitterの例
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
state("ゲームのプロセス名")
{
  int roomId : "game.exe", 0x00, 0x00; // 部屋ID
}

init
{
  vars.TITLE_ID = 0; // タイトル画面のID
}

start
{
  // 直前の画面がタイトルで、部屋IDが変わった時
  return old.roomId == vars.TITLE_ID && current.roomId != old.roomId;
}

split
{
  // 部屋IDが変わった時
  return current.roomId != old.roomId;
}

reset
{
  // タイトル画面になった時
  return current.roomId == vars.TITLE_ID;
}
解説

この例の記述では部屋を行き来するだけでラップを取るので「前に部屋に戻った時はラップを取らないで欲しい」場合は工夫が必要です。
 また私が過去に経験したケースでは「ラップ判定に使う値がポーズ中に別の値に書き換わる」ということがあり、このような場合も工夫が必要です。

変数の値を print 出力する

サンプルスクリプト
変数をprint出力する例
1.
2.
3.
4.
5.
6.
7.
8.
9.
update
{
  // 変数の値を表示したい場合の例
  print("text : " + current.Stage);
  print(string.Format("text : {0}", current.Stage));

  // これは使えない
  print($"text : {current.Stage}");
}
解説

asl のでバックで有用な、print() で変数の値を出力する場合の例。
 文字列と変数を + 記号でつなげたり、string.Format("{番号}", 変数) を使ったりすることが出来ますが、$"{変数}" は使えません。

バージョン違いに対応(基本)

基本形

サンプルスクリプト
公式ドキュメントで紹介されている方法
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
state("ゲームのプロセス名", "ver1.0")
{
  int roomID : "game.exe", 0x00, 0x00;
}
state("ゲームのプロセス名", "ver1.1")
{
  int roomID : "game.exe", 0x00, 0x01;
}
state("ゲームのプロセス名", "ver1.2")
{
  int roomID : "game.exe", 0x00, 0x02;
}

init
{
  // メモリサイズでバージョンを判断
  switch (modules.First().ModuleMemorySize)
  {
    case 00000000:
      version = "ver1.0";
      break;
    case 00000001:
      version = "ver1.1";
      break;
    case 00000002:
      version = "ver1.2";
      break;
    default:
      version = "";
      break;
  }
  
  vars.TITLE_ID = 0;
}

update
{
  // 対応していないバージョンの時はautosplitterを無効にすることもできる
  if (version == "")
    return false;
  
  return true;
}

// start, split, reset は同じなので省略
解説

公式ドキュメントで紹介されている方法で、exe ファイルのサイズを利用しています。
(厳密にはゲームのプロセスのモジュールのサイズ)

経験的に、Unity 製のゲームはこの方法では判別ができないので、後で紹介する Unity 製のゲーム用の方法を試してみてください。

基本形の改良版

サンプルスクリプト
ハッシュ値で判別
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// バージョン判別部分以外は省略、基本形を参考にしてください
init
{
  // MD5 code by CptBrian.
  string MD5Hash;
  using (var md5 = System.Security.Cryptography.MD5.Create())
  using (var s = File.Open(modules.First().FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  {
    MD5Hash = md5.ComputeHash(s).Select(x => x.ToString("X2")).Aggregate((a, b) => a + b);
  }
  print("MD5Hash : " + MD5Hash);
  
  switch (MD5Hash)
  {
    // 省略
  }
}
解説

exe ファイルのビット配列を使って求めたハッシュ値で判別する方法です。

ゲームがバージョンアップしたけど exe ファイルのサイズは変わっていないような場合、基本形では判別ができませんがこの方法であれば判別ができます。

ただし、この方法でも Unity 製のゲームの判別はできないので、後で紹介する Unity 製のゲーム用の方法を試してみてください。

バージョン違いに対応(Unity ゲー)

Unity 製ゲームでは、ゲームを動かしているプログラムは exe ファイルではなく Assembly-CSharp.dll というファイルに含まれていることが多いため、これをバージョン判別に利用します。

全ての Unity 製ゲームに Assembly-CSharp.dll がある訳ではないので、そのようなゲームの場合はそれっぽいファイルを探してみてください。

Unity 製ゲームで使える方法 その 1

サンプルスクリプト
Assembly-CSharp.dll のサイズを利用
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
// バージョン判別部分以外は省略、基本形を参考にしてください
init
{
  var dirPath = Path.GetDirectoryName(modules.First().FileName);
  var dataDir = Path.GetFileNameWithoutExtension(modules.First().FileName) + "_Data";
  var asmCsPath = Path.Combine(dirPath, dataDir, "Managed", "Assembly-CSharp.dll");
  print("asmCsPath : " + asmCsPath);

  //file size check by Ero
  var asmCsSize = new FileInfo(asmCsPath).Length;
  print("Assembly-CSharp.dll size : " + asmCsSize.ToString());

  switch (asmCsSize)
  {
    // 省略
  }
}
解説

考え方は基本形と同じで、exe の代わりに Assembly-CSharp.dll のサイズを利用してバージョン判別を行います。

参考にしている実装は game.MainModule を利用しているのですが、Autosplitter のドキュメントには game.MainModule ではなく modules.First() を使うように書いてあるため、ここでは modules.First() で書いています。

Unity 製ゲームで使える方法 その 2

サンプルスクリプト
Assembly-CSharp.dll のハッシュ値で判別
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
// バージョン判別部分以外は省略、基本形を参考にしてください
init
{
  var dirPath = Path.GetDirectoryName(modules.First().FileName);
  var dataDir = Path.GetFileNameWithoutExtension(modules.First().FileName) + "_Data";
  var asmCsPath = Path.Combine(dirPath, dataDir, "Managed", "Assembly-CSharp.dll");
  print("asmCsPath : " + asmCsPath);

  // MD5 code by CptBrian.
  string MD5Hash;
  using (var md5 = System.Security.Cryptography.MD5.Create())
  using (var s = File.Open(asmCsPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
  {
    MD5Hash = md5.ComputeHash(s).Select(x => x.ToString("X2")).Aggregate((a, b) => a + b);
  }
  print("MD5Hash : " + MD5Hash);
  
  switch (MD5Hash)
  {
    // 省略
  }
}
解説

基本形のハッシュ値版の方法を、exe ではなく Assembly-CSharp.dll を使って行います。

ファイルサイズではなくファイルのハッシュ値で判別を行うため、ゲームがバージョンアップしたけど exe ファイルのサイズは変わっていないような場合にも対応できます。

ロードリムーバ

サンプルスクリプト
ロードリムーバの例
1.
2.
3.
4.
5.
6.
7.
8.
9.
state("ゲームのプロセス名")
{
  bool isLoadFlg : "game.exe", 0x00, 0x10;
}

isLoading
{
  return current.isLoadFlg;
}
解説

私にロードリムーバを作った経験がないので想像での話ではありますが、たぶん「現在ロード中」を判断する材料を集める部分がかなり大変だと思います。

IGT 計測

サンプルスクリプト
IGTタイム計測の例
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
state("ゲームのプロセス名")
{
  long igt : "game.exe", 0x00, 0x11; // この例ではミリ秒とする
}

isLoading
{
  // 基本的に同期と同期の間はタイマーが実時間で進む
  // それを防ぎたい場合はここで常にtrueを返す
  return true;
}
gameTime
{
  // GameTimeをIGTと同期
  return TimeSpan.FromMilliseconds(current.igt);
}
解説

ゲーム内時間はゲームによって整数だったり浮動小数だったり、秒だったりミリ秒だったりするので、例えば画面では hh:mm:ss のように表示されていても、その表示のままモメリ検索するとヒットしないことがあります。

発展的な asl の例

タイマースタート時に初期化したい変数がある

サンプルスクリプト
onStart で初期化
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
state("ゲームのプロセス名")
{
  int roomId : "game.exe", 0x00, 0x00;
}

init
{
  // 最も進んだ部屋のIdをここに代入
  // タイマースタート時に0に初期化する
  vars.mostAdcancedRoomId = 0;
}

onSrart
{
  // ここでタイマースタート時の変数初期化
  vars.mostAdcancedRoomId = 0;
}

update
{
  if (vars.mostAdvancedRoomId < current.roomId)
  {
    vars.mostAdcancedRoomId = current.roomId;
  }
}

start
{
  return old.roomId == 0 && current.roomId != -1;
}
解説

スタートと同時に変数の初期化を行いたい場合があります。
 例えば、自動ラップや自動リセット、多重の自動ラップ防止などを制御するための変数の初期化です。

このような変数の初期化は onStart{ } ブロックで行うと良いです。onStart{ } は Livesplit ver1.8.17 以降の機能であることに注意。

注意が必要な例
スタート判定時に初期化
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
start
{
  if (old.roomId == 0 && current.roomId != -1)
  {
    // タイマースタートの条件を満たした時に初期化する例
    // この場合、手動スタートすると自動ラップなどが動作しない場合がある
    vars.mostAdcancedRoomId = 0;
    
    return true;
  }
}
解説

start{ } ブロックのスタート条件の判定を行っている if 文の中で変数の初期化をすることも可能ですが、そこで初期化する変数を利用した機能は自動スタートした時しか正常に動作しなくなるので、asl 使用時に注意が必要となります。

ゲーム終了でタイマーを自動リセット

サンプルスクリプト
ゲーム終了でタイマーを自動リセット
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
state("ゲームのプロセス名") {}

init
{
  vars.timerModel = new TimerModel {CurrentState = timer};
}

reset
{
  // チェックボックス操作を有効にするためのブロック
  // 実際のリセット時の処理は exit ブロックで行う
  return false;
}

exit
{
  if (settings.ResetEnabled && (timer.CurrentPhase != TimerPhase.Ended))
    vars.timerModel.Reset();
}
解説

init{ } でタイマーモデルを作成し、exit{ } でタイマーモデルのリセットを実行しますが、そのままだと自動リセットを off に出来ない問題があるため、reset{ } で対策しています。

タイマーストップ状態でもリセットしたい

タイマーを直接操作してリセット
asl text
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
update
{
  // タイマーストップ状態のリセット用
  // タイマーモデルオブジェクトのメソッドを利用する
  if ((/*タイマーストップ状態でのリセット条件*/) &&
      (timer.CurrentPhase == TimerPhase.Ended) &&
      settings.ResetEnabled)
  {
    new TimerModel {CurrentState = timer}.Reset();
  }
}

reset
{
  // 通常のリセット用
  // ここで自動リセットできるのはタイマーが計測中orポーズ中
  if (/*通常のリセット条件*/)
  {
    return true;
  }
}
解説

タイマーが計測中かポーズ中の時しか reset{ } による自動リセットは働きません。タイマーストップ状態で自動リセットしたい場合は別の方法が必要です。

TimerModel オブジェクトを実体化して CurrentState に timer を渡すことで、タイマーを直接操作できるようになります。
 update{ } ブロックの中でタイマーストップ状態でのリセット判定を行い、.Reset() メソッドを実行することでリセットできます。

ラップ済みの区間再走時にラップしない

サンプルスクリプト
ラップ済みの区間をチェックする
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
state("ゲームのプロセス名")
{
  // この例では roomId が 0-19 だったとする
  int roomId : "game.exe", 0x00, 0x00; // 部屋ID
}

startup
{
  for (int index = 0; index <= 19; ++index)
  {
    string segmentName = "Room " + index;
    settings.Add("room" + index.ToString(), true, segmentName);
  }
}

onStart
{
  // ラップ済み区間チェック用
  // bool の初期値は false なので、この場合は初期化不要
  vars.triggeredSplits = new bool[20];
}

split
{
  if ((current.roomId > old.roomId) && (!vars.triggeredSplits[current.roomId]))
  {
    vars.triggeredSplits[current.roomId] = true;
    return settings["room" + current.roomId];
  }
}
解説

ラップ済みかをチェックするための bool 配列を用意して、ラップする前は false、ラップしたら true にします。
 この例では roomId を 0-19 と配列に都合の良い範囲にしていますが、実際は複雑になる可能性があるので注意。

ポインタが一瞬 null になる現象に対応

サンプルスクリプト
アドレスの値を別の変数で保持する
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
state("ゲームのプロセス名")
{
  int roomId : "game.exe", 0x00, 0x00; // 部屋ID
}

init
{
  vars.TITLE_ID = 0; // タイトル画面のID
  vars.roomId = vars.TITLE_ID; // 値の保持用変数
}

start
{
  vars.roomId = vars.TITLE_ID; // 保持用変数の初期化

  // 直前の画面がタイトルで、部屋IDが変わった時
  return old.roomId == vars.TITLE_ID && current.roomId != old.roomId;
}

split
{
  // 部屋IDが変わった時
  // currentと保持用変数とを比較する
  if (current.roomId > vars.roomId)
  {
    vars.roomId = current.roomId; // 保持用変数を更新
    return true;
  }
}

reset
{
  // current.roomId == 0 を条件にすると、このケースでは誤作動の危険があるため
  // リセット条件は他の条件にする必要がある
  // return current.roomId == vars.TITLE_ID;
}
解説

メモリアドレスを特定できたと思っても、そのアドレスの値を観察してみると一瞬だけポインタが null になって値を見失う現象が発生することがあります。
 Cheat Engine で監視していると一瞬だけポインタが ?? ?? ?? ?? になったり値が ?? になる現象です。(表示の更新間隔が長いと見逃すことがあるため、60 fps 相当の更新間隔などに設定しておくと良いです)

値を見失っている時はその型の初期値が入っているものとして扱われます。
 おそらく c# の初期値の仕様によるものだと思います。

このような現象が発生するようなケースで split の条件を return current.roomId != old.roomId; にしていると誤作動する危険があるので、この例のように、正常な current の値を保持するための変数 vars を使用する方法があります。

情報表示

プレイヤーが通常では知り得ないゲーム内の情報を表示すると SRC の General Gameplay Rules が定める禁止行為とみなされるリスクがあるため、記録を SRC に申請することを考えてるならば注意が必要です。

Text コンポーネントを直接書き換え

サンプルスクリプト
Textに情報を表示
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
state("ゲームのプロセス名")
{
  int roomID : "game.exe", 0x00, 0x00;
}
startup
{
  // タイマーレイアウトからTextコンポーネントを探してリストアップ
  vars.tcss = new List<System.Windows.Forms.UserControl>();
  foreach (LiveSplit.UI.Components.IComponent component in timer.Layout.Components)
  {
    if (component.GetType().Name == "TextComponent")
    {
      vars.tc = component;
      vars.tcss.Add(vars.tc.Settings);
      print("[ASL] Found text component at " + component); // 確認用メッセージ
    }
  }
  // 確認用メッセージ
  print("[ASL] *Found " + vars.tcss.Count.ToString() + " text component(s)*");
}

update
{
  // この例では Text が1つ以上あったなら、1つ目に情報を書き込む
  if (vars.tcss.Count > 0)
  {
    vars.tcss[0].Text1 = "RoomID"; // Text1は左寄せの文字
    vars.tcss[0].Text2 = current.roomId.ToString(); // Text2は右寄せの文字
  }
}
解説

既存の Text コンポーネントをリストに追加して、リストに含まれている Text コンポーネントを先頭から順番に使用します。

asl のユーザーは事前に必要な数だけ Text コンポーネントをレイアウトに追加しておく必要があります。

この方法の弱点は、asl からの情報表示以外でも Text コンポーネントを使いたい場合に Text コンポーネントの配置に制限ができてしまうこと、複数の Text コンポーネントに情報を表示する場合に Text コンポーネントの順番を並び替えても表示する項目の順番は変えられないことです。

カスタム変数を利用(ver1.8.30 以降)

サンプルスクリプト
asl text
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
state("ゲームのプロセス名")
{
  int roomID : "game.exe", 0x00, 0x00;
}

update
{
  // カスタム変数にstring型の値をセット
  // カスタム変数名(この場合は room)はユーザー側の設定で必要なので、伝える
  timer.Run.Metadata.SetCustomVariable("room", current.roomId.ToString());
}
解説

asl からはカスタム変数に値を渡すだけにしておいて、Text コンポーネントへの表示は Text コンポーネントでの設定によって行います。
カスタム変数は string 型の値しか扱えないため、値をセットする際に string 型に変換することを忘れないようにご注意ください。

この方法の場合、ユーザーは asl からの情報表示用と普通にテキストを表示する用とで複数の Text コンポーネントを任意の順番で並べることができます。

ただし、Text コンポーネントの設定を行うために、ユーザーがカスタム変数の名前を知っている必要があります。

asl で関数を使用する

戻り値がない関数の例
関数定義、戻り値がない場合
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
// 関数定義したいブロックは用途に合わせて決めます
// 基本的には startup or init で定義すれば良いと思います
startup
{
  // 戻り値なしの場合は Action

  // 引数なし
  var Log1 = (Action)(() => print("[asl] test1"));

  // こう書いても OK
  var Log2 = new Action(() => print("[asl] test2"));
  Action Log3 = () => print("[asl] test3");

  // 引数 1 つ Action<T>
  var Log4 = (Action<string>)(output => print("[asl] " + output));

  // こう書いても OK
  var Log5 = new Action<string>(output => print("[asl] " + output));
  Action<string> Log6 = output => print("[asl] " + output);

  // 複数の引数 Action<T, T, ..., T>
  var Log7 = (Action<string, int, int>)((label, oldValue, currentValue) => print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue)));

  // こう書いても OK
  var Log8 = new Action<string, int, int>((label, oldValue, currentValue) => print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue)));
  Action<string, int, int> Log9 = (label, oldValue, currentValue) => print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue));

  // 式が 2 つ以上ある場合、あるいは複数行に渡って記述する場合
  // var 変数名 = の形の場合は、() => {} 全体がカッコの中にあることに注意
  var Log11 = (Action<string, int, int>)((label, oldValue, currentValue) =>
  {
    if (oldValue != currentValue)
      print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue));
  });

  var Log12 = new Action<string, int, int>((label, oldValue, currentValue) =>
  {
    if (oldValue != currentValue)
      print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue));
  });

  Action<string, int, int> Log13 = (label, oldValue, currentValue) =>
  {
    if (oldValue != currentValue)
      print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue));
  };

  // 使用例
  Log1();
  Log2();
  Log3();

  Log4("test4");
  Log5("test5");
  Log6("test6");

  Log7("test7", 1, 2);
  Log8("test8", 1, 2);
  Log9("test9", 1, 2);

  Log11("test11", 1, 2);
  Log12("test12", 1, 2);
  Log13("test13", 1, 2);

  // ブロックをまたいで使いたい場合

  // var 変数名 = の形のものは vars.変数名 = とすることで
  // ブロックをまたいで使えるようになります

  vars.Log21 = (Action)(() => print("[asl] test11"));
  vars.Log22 = (Action<string>)(output => print("[asl] " + output));
  vars.Log23 = (Action<string, int, int>)((label, oldValue, currentValue) =>
  {
    if (oldValue != currentValue)
      print(string.Format("[asl] {0} : {1} -> {2}", label, oldValue, currentValue));
  });
}

update
{
  // 使用例
  // vars.変数名 に代入すればブロックをまたいで使えます
  vars.Log21();
  vars.Log22("test12");
  vars.Log23("test13", 1, 2);
}
戻り値がある関数の例
関数定義、戻り値がある場合
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
// 関数定義したいブロックは用途に合わせて決めます
// 基本的には startup or init で定義すれば良いと思います
startup
{
  // 戻り値ありの場合は Func

  // Action と同じく、
  // var 変数名 = (Func<>) でも
  // var 変数名 = new Func<> でも
  // Func<> 変数名 = でも OK

  // Func<T, T, ..., T, TResult> の TResult が戻り値の型
  var Func1 = (Func<string>)(() => timer.Run.CategoryName);
  var Func2= new Func<string>(() => timer.Run.CategoryName);
  Func<string> Func3 = () => timer.Run.CategoryName;

  // 式が 2 つ以上ある場合、あるいは複数行に渡って記述する場合
  // var 変数名 = の形の場合は、() => {} 全体がカッコの中にあることに注意
  // return が必須
  vars.Func13 = (Func<int, TimingMethod, string>)((index, method) =>
  {
    var name = timer.Run[index].Name;
    var time = timer.Run[index].SplitTime[method];
    return string.Format("{0} : {1}", name, time);
  });

  // 使用例
  print("[asl] Func1 : " + Func1());
  print("[asl] Func2 : " + Func2());
  print("[asl] Func3 : " + Func3());

  print("[asl] Func13 : " + vars.Func13(1, TimingMethod.RealTime));

  // ブロックをまたいで使いたい場合

  vars.Func21 = (Func<string>)(() => timer.Run.CategoryName);
  vars.Func22 = (Func<TimingMethod, string>)(method => timer.CurrentTime[method].ToString());
  vars.Func23 = (Func<int, TimingMethod, string>)((index, method) =>
  {
    var name = timer.Run[index].Name;
    var time = timer.Run[index].SplitTime[method];
    return string.Format("{0} : {1}", name, time);
  });
}

update
{
  // 使用例
  // vars.変数名 に代入すればブロックをまたいで使えます
  print("[asl] Func21 : " + vars.Func21());
  print("[asl] Func22 : " + vars.Func22(TimingMethod.RealTime));
  print("[asl] Func23 : " + vars.Func23(1, TimingMethod.RealTime));
}
解説

使用できるブロック名が決まっている asl において、クラスのメソッドのようにして関数を定義することができませんが、デリゲート+ラムダ式による関数の定義は可能です。

元々、関数を代入できるデリゲートというクラスがあって、デリゲートの派生として戻り値のない Action と戻り値のある Func とがあり、ここではそれらを使用します。

(引数リスト) => 式 という書き方がラムダ式で、これによって名前のない関数を定義しています。
式が複数ある場合は (引数リスト) => { 式; 式; 式; } のように {} が必須、というかこれが元々の形です。

vars.変数 に代入することで、ブロックをまたいでも関数を呼び出すことができます。

current と old の監視

基本形

サンプルスクリプト
current と old の値が変わっていたら print 出力
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
state("ゲームのプロセス名")
{
  int roomId : "game.exe", 0x00, 0x00;
}

startup
{
  // 監視メソッド
  vars.Watch = (Action<IDictionary<string, object>, IDictionary<string, object>, string>)((oldLookup, currentLookup, key) =>
  {
    var oldValue     = oldLookup[key];
    var currentValue = currentLookup[key];
    if (!oldValue.Equals(currentValue))
      print(string.Format("{0} : {1} -> {2}", key, oldValue, currentValue));
  });
}

update
{
  // 監視メソッドの呼び出し
  vars.Watch(old, current, "roomId");

  // 単純形
  if (current.roomId != old.roomId)
  {
    print(string.Format("{0} : {1} -> {2}", "roomId", old.roomId, current.roomId));
  }
}
解説

動作確認などで state ブロックで定義した変数の値が変わったことを監視したい場合があります。
 単純に考えると、例にある単純型のように変数名を複数回書く必要があり、複数の変数を監視しようとすると結構な手間になってしまいます。

そこで、変数名を書く回数が少なくて済む方法がここで紹介している監視メソッド。
 Mitchell Merry さんのテンプレートがオリジナル?

詳しい解説

current と old の中身は ASLState.Date で、これは ExpandObject 型のオブジェクト。
 ExpandObject 型のオブジェクトは Dictionary に変換できて、例えば、値の取得 value = current.stage; は value = (IDictionary)current["stage"]; のように書くことができます。

Autosplitter は「startup → init(バージョン判別)→ state → updat などのループ」という順番に処理が流れるので、startup ブロックの段階だと current も old もまだ定義されていないため、わざわざ引数で渡す必要があります。

監視対象が複数ある場合

サンプルスクリプト
current と old の値が変わっていたら print 出力
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
state("ゲームのプロセス名")
{
  int roomId     : "game.exe", 0x00, 0x00;
  bool isLoadFlg : "game.exe", 0x00, 0x10;
}

startup
{
  // 監視メソッド

  // 全て監視
  vars.WatchAll = (Action<IDictionary<string, object>, IDictionary<string, object>>)((oldLookup, currentLookup) =>
  {
    foreach (var key in currentLookup.Keys)
    {
      var oldValue     = oldLookup[key];
      var currentValue = currentLookup[key];
      if (!oldValue.Equals(currentValue))
        print(string.Format("{0} : {1} -> {2}", key, oldValue, currentValue));
    }
  });

  // 複数の任意の変数を監視
  vars.WatchKeys = (Action<IDictionary<string, object>, IDictionary<string, object>, string[]>)((oldLookup, currentLookup, keys) =>
  {
    foreach (var key in keys)
    {
      var oldValue     = oldLookup[key];
      var currentValue = currentLookup[key];
      if (!oldValue.Equals(currentValue))
        print(string.Format("{0} : {1} -> {2}", key, oldValue, currentValue));
    }
  });
}

update
{
  // 監視メソッドの呼び出し

  // 全て監視
  vars.WatchAll(old, current);

  // 複数の任意の変数を監視
  vars.WatchKeys(old, current, new string[] {"roomId", "isLoadFlg"});
}
解説

監視対象が複数ある場合、その数だけメソッドを呼び出すよりも、元から複数対応可能なメソッドにしておいた方が使い勝手が良いです。
 元々、メソッド内で current and old を辞書型に変換して特定のキーでの値を取り出すということをやっているので、辞書型に変換するのは 1 回だけにして複数の値をまとめて取り出した方が効率も良いです。

全て監視する例は引数が他より 1 つ少なくて済み、使うだけなら最も楽だと思います。

しかし、動作確認の段階によってはもう監視する必要がなくなる変数が出てきたりするので、監視対象は指定できたほうが使い勝手が良い場合もあります。

イベント購読 (ver1.8.17 以降)

サンプルスクリプト
ScriptableAutoSplit.dll ver1.8.17 での追加メソッド
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
// タイマースタート時
onStart
{
  vars.room = 1; // 開始時の部屋id
}

// タイマーリセット時
onReset
{
  vars.room = 0; // タイトル画面のid
}

// ラップ時
onSplit
{
  vars.room = current.roomId;
}
解説

ScriptableAutoSplit.dll ver1.8.17 にて構文が追加され、onStart, onSplit, onReset メソッドが使用できるようになりました。
 イベント購読・解除の記述が不要になるため asl の作成が楽になります。

ただし、追加されたのはこの 3 つだけなので、他のタイマーイベントを利用したい場合は従来と同じ方法を使う必要があります。

イベント購読(ver1.8.16 以前、汎用的な方法)

サンプルスクリプト
イベント発生時に変数の初期化などを行う
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
state("ゲームのプロセス名")
{
  int roomId : "game.exe", 0x00, 0x00; // 部屋ID
}

startup
{
  // タイマースタート時
  vars.TimerStart = (EventHandler)((s, e) =>
  {
    vars.room = 1; // 開始時の部屋id
  });
  timer.OnStart += vars.TimerStart;

  // タイマーリセット時
  // Resetのみ EventHandler ではなく EventHandlerT<TimerPhase> なので注意
  vars.TimerReset = (LiveSplit.Model.Input.EventHandlerT<TimerPhase>)((s, e) =>
  {
    vars.room = 0; // タイトル画面のid
  });
  timer.OnReset += vars.TimerReset;

  // ラップ時
  vars.TimerSplit = (EventHandler)((s, e) =>
  {
    vars.room = current.roomId;
  });
  timer.OnSplit += vars.TimerSplit;
}

start {return current.roomId == 1;}
reset {return current.roomId == 0;}
split {return current.roomId > vars.room}

shutdown
{
  // 購読したら忘れずに解除
  timer.OnStart -= vars.TimerStart;
  timer.OnReset -= vars.TimerReset;
  timer.OnSplit -= vars.TimerSplit;
}
解説

(s, e) => { }; はイベントハンドラのラムダ式を使った定型文で、(s, e) は仮引数 (object sender, EventArgs e) のことです。
 s はイベントを送信したオブジェクトで、LivesplitState (or TimerModel ?)。
 e は Reset 以外のイベントでは EventArgs 型で値は null、Reset の場合は TimerPhase 型で値は直前の TimerPhase が入っています。
 イベントハンドラの中で s も e も利用しなくても問題ないですが、定型文なので書いておく必要はあります。

購読解除の記述を忘れると解除できなくなった購読が残り続けて、解除の記述をした後でも「エラーの原因の行が特定できない謎のエラー」が発生し続けるようになる場合があります。
 私の経験では、Livesplit を再起動しても残り続け、Livesplit を終了して少し時間を置いてから起動することでエラーを解消できた、ということがありました。

asl 標準(start, split, reset)以外のタイマー操作

サンプルスクリプト
ラップした時に一時停止
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
state("Livesplit"){ }

startup
{
  // timerを渡してTimerModelクラスを実体化して
  // タイマー操作メソッドを使用
  vars.timerModel = new TimerModel {CurrentState = timer};
}

// onSplitはver1.8.17以降で使用可能
onSplit
{
  // タイマー操作したい場所でTimerModelのメソッドを呼び出す
  vars.timerModel.Pause();
}
解説

TimerModel クラスを利用することで、タイマー操作メソッドが使用できます。
 実体化する際に CurrentState = timer とすることを忘れずに。

この例では「ラップを取った時にタイマーを一時停止する」動作ですが、メソッドを呼び出す場所は if の中に入ることが多いのではと思います。
 TimerModel のメソッドによるタイマー操作の他の例として、タイマーストップ後に自動リセットするものを見たことがあります。

カテゴリによってタイマー操作の判定条件を変える

自動判別

サンプルスクリプト
カテゴリ名から自動的に判別
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
split
{
  if (timer.Run.CategoryName == "Any%")
  {
    // 略
  }
  if (timer.Run.CategoryName == "100%")
  {
    // 略
  }
  else
  {
    // 略
  }
}
解説

複数のカテゴリに対応しようとした場合に、カテゴリによってタイマー操作する条件が異なることがあります。
 timer.Run.CategoryName で Splits Editor のカテゴリ欄のテキストが取得できるので、これを使って条件分岐します。

この例の方法だと、カテゴリ名のテキストが一言一句同じでなければならないという制限があることに注意。
 SRC のデータベースからカテゴリ名を選択しているなら大丈夫だと思いますが、手入力を想定している場合は正規表現を使うなどして対処が必要かも。

手動判別

サンプルスクリプト
手動で選択してもらう
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
startup
{
  settings.Add("category100", true, "100% mode");
}

split
{
    if (settings["category100"])
  {
    // 略
  }
  else
  {
    // 略
  }
}
解説

asl の設定項目を利用して、ユーザーに手動でカテゴリを選択してもらう方式の例です。
 この例の場合は、チェックボックスにチェックを入れると 100% カテゴリ用のモード、チェックが入っていないとそれ以外のカテゴリ用のモードで動作します。

カテゴリの数や種類によってはチェックボックスの構造などが複雑化する場合があります。
 このような場合は本当はラジオボタンが望ましいのにチェックボックスで選択する UI しか使えない仕様のため、カテゴリの選択肢が 2 つ以上になると、今のチェック状況だとどのカテゴリに対応したモードになるかが分かりにくくなるという問題があります。

実験的なasl

asl 本来の用途とは異なるであろう、実験的な asl です。
 asl には色々な制約があるため時には不自由を感じることもありますが、実はエラーの発生を回避してしまえば案外自由だったりします。

ホットキー送信

サンプルスクリプト
完走したらホットキーを送信
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
state("LiveSplit")
{
}

init
{
  // 直前のフレームのタイマーの状態を保持
  vars.oldPhase = TimerPhase.NotRunning;
}

update
{
  // 完走したらホットキーを送信
  if(timer.CurrentPhase == TimerPhase.Ended &&
      vars.oldPhase == TimerPhase.Running)
  {
    // SendKeysはアクティブウィンドウに対しての送信
    SendKeys.Send("A");
  }
  
  // 現在フレームのタイマー状態を保持
  vars.oldPhase = timer.CurrentPhase;
}
解説

実験的な asl その 1
完走したタイミングで自動的にキー入力を送信して他のソフトのホットキーを反応させる、ということが asl でできます。

Comparisonの書き換え

サンプルスクリプト
ラップタイムに応じて次の区間の目標タイムを計算
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
state("LiveSplit")
{
}

init
{
  // const vars
  vars.COMPARISON_NAME = "Add 50 sec"; // ここで指定した名前の列がある場合のみ動作する
  
  // vars
  vars.oldPhase = TimerPhase.NotRunning;
  vars.oldSegmentIndex = -1;
  
  // Comparisonsから決められた名前のComparisonを検索
  vars.comparison = "";
  foreach (string comparisonName in timer.Run.Comparisons)
  {
    if (comparisonName == vars.COMPARISON_NAME)
    {
      vars.comparison = comparisonName;
      print("found \"" + comparisonName + "\" Comparison");
    }
    else
    {
      print("not found \"" + vars.COMPARISON_NAME + "\" Comparison");
    }
  }
  
  // 検索結果の確認
  if (vars.comparison == vars.COMPARISON_NAME)
  {
    for (int i = 0; i < timer.Run.Count; i++)
    {
      TimeSpan t = timer.Run[i].Comparisons[(string)vars.comparison].RealTime ?? TimeSpan.Zero;
      print("[" + i + "] : " + t);
    }
  }
}

update
{
  // Comparisonがなかった場合は何もしない
  if (vars.comparison != vars.COMPARISON_NAME)
    return false;
  
  // タイマースタート時にとリセット時にComparisonを初期化
  if ( (timer.CurrentPhase == TimerPhase.Running &&
      vars.oldPhase == TimerPhase.NotRunning) ||
      (timer.CurrentSplitIndex == -1 &&
      vars.oldSegmentIndex != -1) )
  {
    print("Timer Start");
    
    for (int i = 0; i < timer.Run.Count; i++)
    {
      LiveSplit.Model.Time time = new LiveSplit.Model.Time();
      time.RealTime = new TimeSpan(0, 0, (i+1)*50);
      timer.Run[i].Comparisons[(string)vars.comparison] = time;
    }
    vars.oldPhase = timer.CurrentPhase;
    vars.oldSegmentIndex = timer.CurrentSplitIndex;
    return false;
  }
  
  // ラップ時のComparison再計算
  if (timer.CurrentSplitIndex > vars.oldSegmentIndex)
  {
    // 前区間までのタイムを取得
    TimeSpan previousSplitTime = TimeSpan.Zero;
    previousSplitTime = timer.CurrentTime.RealTime ?? TimeSpan.Zero;
    print("previous split time : " + previousSplitTime);
    
    // 現在区間以降のComparisonを再計算
    int j = 0;
    for (int i = timer.CurrentSplitIndex; i < timer.Run.Count; i++)
    {
      LiveSplit.Model.Time time = new LiveSplit.Model.Time();
      time.RealTime = new TimeSpan(0, 0, (j+1)*50) + previousSplitTime;
      timer.Run[i].Comparisons[(string)vars.comparison] = time;
      j++;
    }
    
    vars.oldSegmentIndex = timer.CurrentSplitIndex;
    return false;
  }

  vars.oldPhase = timer.CurrentPhase;
  vars.oldSegmentIndex = timer.CurrentSplitIndex;
}
解説

実験的な asl その 2
asl で Textコンポーネントの書き換えができるなら、Livesplit 本体が持つ他の情報も書き換えられるのでは? と思ったので挑戦してみました。

この例では、ラップを取った時に「次の区間の目標タイム」=「前の区間までのタイム +50 秒」となるように Comparison のタイムを再計算しています。
 スタート・ストップ・ラップなどのタイマー操作やロードリムーバの制御を行うのが asl 本来の用途なので、このような Comparison の書き換えを行うならばコンポーネントを作って行うのが本来の在り方ではあると思います。

7 件のコメント:

  1. 変数の値をprint出力する、ゲーム終了でタイマーを自動リセット、ラップ済みの区間再走時にラップしない を追加

    返信削除
  2. 追加
    タイマースタート時に初期化したい変数がある
    asl で関数を使用する

    返信削除
  3. 追加
    タイマーストップ状態でもリセットしたい

    返信削除
  4. ソースコードをコピーするボタンを追加

    返信削除
  5. バージョン違いに対応の例を増加
    カテゴリによってタイマー操作の判定条件を変える例を追加

    返信削除
  6. カテゴリによってタイマー操作の判定条件を変えるで手動でカテゴリ選択する例を追加
    current と old の監視を追加
    asl で関数を使用するを再構築

    返信削除
  7. 情報表示の項目にカスタム変数を使った場合の例を追加

    返信削除