この記事はコンポーネント開発に必要な全ての情報を記載している訳ではありませんが、公式のソースコードを読み解くための参考資料として活用していただけると幸いです
この記事の中で My~ と書いている部分は開発するコンポーネント名と考えてください
もくじ
開発資料リンク
あらかじめ知っておきたい C# の知識
コンポーネント開発で使用する基本的なファイル
プロジェクトのプロパティ設定
設定画面について
クラスの実体化やメソッド呼び出しのタイミング・順序
コンポーネントを作る際に把握しておきたいクラスなど
フォルダごとの紹介・説明
LiveSplit.Core/UI/Components
LiveSplit.Core/Model
LiveSplit.Core/UI
LiveSplit.Core/TimeFormatters
LiveSplit.Core/Model/Comparisons
UpdateManager
LiveSplit.Core/Model/RunSavers
LiveSplit.Core/Model/RunFactories
LiveSplit/LiveSplit.View/View
開発資料リンク
あらかじめ知っておきたい C# の知識
コンポーネント開発で使用する基本的なファイル
プロジェクトのプロパティ設定
設定画面について
クラスの実体化やメソッド呼び出しのタイミング・順序
コンポーネントを作る際に把握しておきたいクラスなど
フォルダごとの紹介・説明
LiveSplit.Core/UI/Components
LiveSplit.Core/Model
LiveSplit.Core/UI
LiveSplit.Core/TimeFormatters
LiveSplit.Core/Model/Comparisons
UpdateManager
LiveSplit.Core/Model/RunSavers
LiveSplit.Core/Model/RunFactories
LiveSplit/LiveSplit.View/View
開発資料リンク
Livesplit 用コンポーネント開発の資料はこの記事だけではありません(ただし、記事公開時点での日本語の資料はおそらくこの記事だけ)公式の開発ガイドはおそらく存在しないと思いますが、Speedrun Tool Development にて開発のチュートリアルが紹介されています
- 公式資料
-
Livesplit のソースコード
公式コンポーネントのソースコード
ver0.11.0までであればドキュメントが存在しているぽいです
Livesplit やコンポーネントの仕様を確認したい場合などに、Github で公開されている本体のソースや既存のコンポーネントのソースを読み解くのも有用です
ただし、公式のソースコードは複数にフォルダ分けされた大量のファイルに書かれており、どこに何が書かれているかをある程度知っていないと目的のソースコードにたどり着くことも難しいように思います
- Speedrun Tool Development
-
英語ができるならば Discord の Speedrun Tool Development サーバで聞くという選択肢もあります
2021年12月に #tutorials チャンネルの整備が行われ、紹介されているコンポーネント作成のチュートリアルも新たなものになりました
チュートリアル自体は GitHub で公開されているので、Speedrun Tool Development サーバに参加しなくても読むことができます
非常に細かく説明されているので、まずはチュートリアルを読んで、足りない部分をこの記事で補完することをお勧めします
- その他
-
GitHub にてコンポーネント開発のテンプレートも公開されています
ソースコードの中にコメントが書かれており、開発のヒントとして非常に参考になると思います
あらかじめ知っておきたい C# の知識
Livesplit のソースコードは C# で書かれており、読み解くにも作るにも C# の知識が必要ですC# の基礎知識は各自で学んでいただくとして、ここでは「C# の入門書には出てこなさそうだけど Livesplit のソースコードを読み解く際に必要になる項目」について少し説明しておこうと思います
(つまり私が知らなくて、適切な検索ワードも思いつかなくて苦労した項目などの紹介です)
- null 許容型
型名?
- int や double など C# の値型に分類される型では、その型が意味する値のみを代入できます
そのような値型の変数の宣言で型名に ? を付けると、その型が扱える値に加えて null も代入することができる null 許容型になります
Livesplit ではタイムに関する値のほとんどが TimeSpan? 型の変数を利用しており、計測タイムが存在しない状態などを表す場合に null が代入され、TimeSpan.Zero とは別の意味を持っています
記録の履歴がない状態で Livesplit の Split が "-" と表示されるのも、TimeSpan? 型に null が代入されている状態です
TimeSpan? 型の変数では、Seconds や Ticks といった TimeSpan のプロパティに直接アクセスすることができないため注意が必要です
このような場合は null 合体演算子や null 条件演算子と組み合わせて使用すると良いです - null 合体演算子
??
- 例えば TimeSpan? 型の変数 tsn から TimeSpan 型の変数 ts に代入をしようとする場合、変数 tsn に null が入っている可能性があるため
ts = tsn;
のように書くとエラーが発生します
もし tsn が null の場合は特定の値を tp に代入し、null でない場合はその値を tp に代入するのであれば、ts = tsn ?? TimeSpan.Zero;
のように ?? 演算子を使うことができます
これはts = (tsn != null) ? tsn : TimeSpan.Zero;
と同じ意味です - null 条件演算子
nullを取り得るオブジェクト?.メンバ
- null を取り得るオブジェクトのメンバにアクセスしようとして
オブジェクト.メンバ
のように記述する場合、オブジェクトが null でないことを if 文などで確認したうえでアクセスする必要があります
もしオブジェクトが null のときにメンバも null を返したいのであれば、null 条件演算子を使うことで記述を簡素化できます
例えば、Time.RealTime を取得しようとして Time が null の時は RealTime も null としたい場合であればTimeSpan? ts = Time?.RealTime;
となります
Time が null でなければ、普通に Time.RealTime の値を取得できます
また、null 合体演算子と組み合わせることもでき、例えば Time.RealTime を取得しようとして、Time が null の時は RealTime は TimeSpan.Zero としたい場合はTimeSpan? ts = Time?.RealTime ?? TimeSpan.Zero;
となります - 本体が式の関数
戻り値型 関数名 => 式
- 関数の中身が 1 つの式だけの場合、=> 記号を使って
ComponentFactory でよく見る記述(一部抜粋)1.
2.
3.public string ComponentName => "My Component Name"; public ComponentCategory Category => ComponentCategory.Other; public IComponent Create(LiveSplitState state) => new MyComponent(state);
この場合、ラムダ式の => とは異なる => なので注意が必要です(そして記号で検索するとラムダ式がヒットするという罠)
この書き方はメソッドだけでなくプロパティなどでも使用でき、プロパティで使用した場合は get-only プロパティになります - メソッドチェーン
クラス.メソッド().メソッド()
- 同じクラスに対して連続して複数回メソッドを実行したい場合に、クラスのインスタンスに続いてメソッド呼び出しを . 記号でつなげて記述することができます
メソッドチェーンを利用する場合、メソッドの戻り値が自分自身のクラスである必要があります - 逐語的文字列リテラル
@"文字列"
- 文字列リテラルの最初に@を付けることで逐語的文字列リテラルとなり、\t や \r\n などはタブや改行ではなくそのままの文字 \t、\r\n として扱われるようになります
また、逐語的文字列リテラルの中で改行を行うと、その位置で改行された文字列として扱われます
- 型の規定値
- C# では明示的な初期化を行わなかった場合、その変数には型の規定値が入っています
基本的には「0 埋め」で、型によって 0、false、null のどれかに解釈されます
詳細は Microsoft のドキュメントにてご確認ください
列挙型の場合は「式 (E)0 によって生成される値」とあり、基本的には値が 0 のメンバで、値の指定をしていなければ先頭のメンバと考えれば良さそうです
フィールドで型だけ定義されていて、コンストラクタなどで初期化をしていなくて、一度も代入が行われないままの状態でその変数の値を使う記述があった場合、型の規定値が使われています - AssemblyInfoからバージョンを取得する
- コンポーネントの名称、説明、バージョンは複数個所に記述する必要があり、特にバージョンは名称と説明に比べて書き換える頻度が高いと思います
バージョンの値を書いたり使ったりする場所は、AssemblyInfo、MyComponentFactory、MyComponentSettings が挙げられます
もし全て同じ値で良いならば書き換えるのは AssemblyInfo だけにして、MyComponentFactory や MyComponentSettings ではSystem.Reflection.Assembly.GetExecutingAssembly().GetName().Version
を使って AssemblyInfo で書いたバージョンの値を使用することができます
参考
アセンブリ情報やバージョンを取得する - 現在実行中のメソッドやプロパティの名前を取得する
- 作っているコンポーネントの中からではなく、Livesplit から呼び出されるメソッドやプロパティもあります
呼び出されるタイミングや順番などを把握しておくと、意図しないトラブルを避けることにつながることもあります
System.Reflection.MethodBase.GetCurrentMethod().Name
で現在実行中のメソッド名などを取得できます
CurrentMethod という名前が付いていますがプロパティやコンストラクタでも使用できます
参考
現在実行中のメソッド名などの情報を取得する
本体が式の関数になっているメソッドやプロパティ、自動プロパティなどでも名前を取得したい場合は、基本の形に書き換えることで対応できます本体が式の関数を書き換えて名前を取得する例1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.// get-only のプロパティ public string ComponentName => "My Component Name"; // 基本の形に書き換えて名前を取得 public string ComponentName { get { System.Diagnostics.Debug.WriteLine(System.Reflection.MethodBase.GetCurrentMethod()); return "My Component Name"; } } // メソッド public IComponent Create(LiveSplitState state) => new MyComponent(state); // 基本の形に書き換えて名前を取得 public IComponent Create(LiveSplitState state) { return new MyComponent(state); }
自動プロパティを書き換えて名前を取得する例1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.// 自動プロパティ public MySettings Settings { get; set; } // 基本の形に書き換えて名前を取得 private MySettings _settings; public MySettings Settings { get { System.Diagnostics.Debug.WriteLine(System.Reflection.MethodBase.GetCurrentMethod()); return _settings; } set { System.Diagnostics.Debug.WriteLine(System.Reflection.MethodBase.GetCurrentMethod()); _settings = value; } }
- デバッグ出力
- GUI アプリケーションで printf デバッグをする場合、メッセージをメッセージボックスに出力する方法が有名だと思います
しかし、場合によってはメッセージを[出力]ウィンドなどに出力したほうが都合が良いこともあり、そのような時はSystem.Diagnostics.Debug.WriteLine()
が便利です
Debug はデバッグ用のクラスということもあり、Debug ビルドする時だけ有効で Release ビルドでは無効になるため、後になって Release のためだけにメッセージ出力用の記述を消す必要もありません
参考
VS.NETでデバッグ・メッセージを出力するには?
コンポーネント開発で使用する基本的なファイル
次のファイルはコンポーネント開発でほとんどの場合必要となる、基本的なファイルです- MyComponent.cs
- コンポーネントの中心的なプログラムを記述
- MyComponentFactory.cs
- Livesplit からコンポーネントを呼び出す際に使用
- MyComponentSettings.Designer.cs
MyComponentSettings.cs
MyComponentSettings.resx - LayoutEditor で表示する設定画面
- AssemblyInfo.cs
- VisualStudio でプロジェクトを作成すると自動的に生成されるファイル
ここに[assembly: ComponentFactory(typeof(MyComponentFactory))]
を追加
実際に公開されているコンポーネントのソースを見ると、この1文を MyComponentFactory.cs に記述しているものもあります
簡単に和訳すると次のような内容です
- コンポーネントの作り方
-
- 新しいプロジェクトの作成を選び、C# のクラスライブラリ(.NET Framework)を選択
.NET Framework 4.6.1 を使用すること - LiveSplit.Core.dll と UpdateManager.dll をプロジェクトの参照に追加
プロジェクトではLiveSplit.UI.Components
名前空間を使用すること - プロジェクトに MyComponent クラスを追加
IComponent インターフェースを実装するか、実装したクラスを継承する必要がある - プロジェクトに MyComponentFactory クラスを追加
IComponentFactory を実装する必要がある - MyComponentFactory.Create() メソッドが MyComponent のインスタンスを返すように実装
- AssemblyInfo.sc ファイルに次のような行を追加
[assembly: ComponentFactory(typeof(MyComponentFactory))]
- 新しいプロジェクトの作成を選び、C# のクラスライブラリ(.NET Framework)を選択
- アプリケーション
- アセンブリ名:
Livesplit.MyComponent
規定の名前空間:LiveSplit.UI.Components
- ビルドイベント
- ビルド後:
copy $(TargetDir)$(TargetFileName) "Livesplit\Componentsのパス"
dll をビルドすると自動的にここで指定した Livesplit\Component フォルダにコピーされます - デバッグ
- 開始動作
外部プログラムの開始:"livesplit.exeのパス"
デバッグを開始すると自動的に Livesplit が起動します
設定画面について
- フォームのサイズ
- フォーム全体の最大幅 476 px、最大高さ 516 px(スクロールバーなしの場合)
縦方向のスクロールバーが表示されている場合
フォーム全体の最大幅 459 px
margin 上下左右とも 3 px(デフォルト値)
padding 上下 6 px、左右 7 px
使用可能サイズ+余白
幅 462 + 14 - TableLayoutPanel
- 基本的に TableLayoutPanel の中にコントロールを配置します
1行の高さは 27px で、配置するフォームそれぞれにアンカー左右を設定
TableLayoutPanel の幅 462 px
margin 上下左右とも 3 px(デフォルト値)
padding 上下左右とも 0 px(デフォルト値)
公式のコンポーネントでは TableLayoutPanel → グループボックス → TableLayoutPanel のような入れ子をよく見ますが、MSDN のガイドラインでは TableLayoutPanel を入れ子にすることは推奨されていません
これは入れ子ではない? - フォント
- MS UI ゴシック、9pt が標準
VisualStudio デフォルト値のままで大丈夫なはず - コンボボックスなどに列挙型のメンバを表示させる方法
- コンボボックスの選択肢は列挙型のメンバと対応していると、可読性や拡張性などで有利だと思います
参考
列挙型をコンボボックスで選択できるようにするウルテク
enum を文字列で置き換えて ComboBox に表示する - 設定画面に関係するイベント
-
- Load
初めてタブが開かれる時に発生
設定画面を閉じると、次にタブが開かれたときにも発生 - VisibleChanged
タブを開くたびに発生
- Load
クラスの実体化やメソッド呼び出しのタイミング・順序
特に重要だと思ったメソッドを中心に、Livesplit から呼び出されるタイミングや順序をまとめてみましたシーケンス図のような図ですが、私の不勉強によりシーケンス図としても Livesplit の挙動としても必ずしも正確な図ではないことをご了承ください
- Livesplit起動のから終了まで
lsl ファイルに MyComponent が保存されておらず LayoutEdirot で MyComponent を追加した場合と、lsl ファイルに MyComponent が保存されていて Livesplit 起動時に MyComponent が自動的に追加される場合とで、MyComponentFactory の IUpdateable メンバが読み取られるタイミングや、保存された設定値の読み込みの有無などの違いがあります
MyComponent が有効になっている間は GetSsettingsHashCode() と Update() が繰り返し呼び出されます
図に記載していませんが、Livesplit 本体を右クリックしたときに MyComponent.ContextMenuControls が呼び出されます- 設定画面を開いて閉じる
- すでに MyComponent が有効になっている状態で LayoutEditor を開いたときは、次の図のようにメソッドが呼び出されます
初めてフォームを開いたとき発生する Load() イベントでフォームの初期化を行うのはよくある方法と思いますが、コンポーネントにおいては設定画面を開こうとした時に初めて Load() イベントが発生することに注意が必要です(LayoutEditor ではなく、そこから開く「設定画面」です)
コンポーネントをレイアウトに追加しただけ、Livesplit 起動時にすでにコンポーネントが有効になっている、などの場合は Load() イベントが発生していない状態でもコンポーネントが正常に動く必要があります
設定画面を表示するための初期化にとどめる、どこかで自分で Load() を呼び出す、といった対策が必要だと思います - コンポーネントをレイアウトから削除
- レイアウトから MyComponent を削除したり Livesplit を終了すると MyComponent.Dispose() が呼び出されます
LyaoutEditor で削除しただけではまだ完全に削除されておらず、削除した状態で LayoutEditor の OK を押すと Dispose() が呼び出されます
Dispose() 呼び出し後でも GetSsettingsHashCode() と Update() の呼び出しが行われることがありました
Dispose() の中で MyComparisonGenerator やタイマーイベントなど何らかの後処理をする場合、 Update() で削除したオブジェクトへのアクセスが発生することがあるので、例外の発生に注意です - 私が苦労したポイント
- ここまでの説明と重複がありますが、私が苦労したポイントをまとめておきます
- LayoutEditor でコンポーネントを追加しただけでは、フォームの Load() イベントは発生しない
- LayoutEditor でコンポーネントを追加しただけでは、MyComparison.Generate() は呼び出されない
- Livesplit の終了を実行 or コンポーネントの削除を実行しても、Update() などの繰り返し処理が数回行われる場合がある
- LayoutEditor でコンポーネントの削除が完了すると Update() は呼び出されなくなるが、OK ボタンを押して削除が確定するまでは Dispose() は呼び出されない
- LayoutEditor でコンポーネントを削除してからキャンセルボタンを押したとき、MyCpmparison クラスは一旦破棄されるぽいが MyComponent.Disporse() は呼び出されないため、null へのアクセスが発生する
- SplitEditor を開いている間はタイマー操作はできないが、LayoutEditor を開いている間はタイマー操作ができるため、コンポーネントの設定画面を開いている間にもタイマーの状態が変化する場合がある
コンポーネントを作る際に把握しておきたいクラスなど
コンポーネントを開発する際に必須だったり利用する頻度が高いと思うクラスを中心に、簡単に紹介します詳細な説明は、記事後半の「フォルダごとの紹介・説明」をご確認ください
コンポーネント開発で用意する基本のクラス
- MyComponent
- コンポーネントの処理本体
インターフェース IComponent の実装
タイマーレイアウト上に表示する必要がなければ抽象クラス LogicComponent の実装でも OK - MyComponentFactory
- Livesplit がコンポーネントを利用する際に呼び出します
インターフェース IComponentFactory の実装 - MyComponentSettings
- LayoutEditor で表示する設定画面
設定画面が不要であれば、このクラスを作る必要はありません
一般的なコンポーネントの開発で実装、あるいは利用
- InfoTextComponent
InfoTimeComponent - タイマーレイアウト上でテキスト、タイムを表示します
- Time
- (おそらくは)Livesplit のタイムの基本単位のような扱い
- SettingsHelper
- 設定画面の状態を保存・読み込みをする時に利用します
- ITimeFormatter
- タイムを文字にして表示する時は、基本的には TimeFormatter を利用します
- IComparisonGenerator
- 比較対象タイムを生成する時に利用します
Livesplit が持っている情報
- LiveSplitState
- (おそらくは)ほぼ全ての Livesplit が持っている情報をここから取得できます
- IRun
- lss ファイルが持っているのと同じ情報を取得できます
- ISegment
- 区間一つ分の情報を取得できます
比較対象タイムのタイムそのものや、履歴タイムなどもここに格納されます
実際にタイムの値が格納されている場所
多くのタイムは Run[i] からたどった場所に格納されています
Run[i] は ISegment 配列で、SegmentEditor で作成した各区間に対応していて、最初の区間は Run[0]、最後の区間は Run[Run.Count-1] です
これらのタイムは Time 構造体を基本単位として扱っていることに注意
Split タイムは各区間終了時のタイマー開始からのタイム、Segment タイムはその区間のみのタイムです
多くは Split タイムが格納されていて、Segment タイムが欲しい場合はその区間の Split タイムから 1 つ前の区間の Split タイムを引くことで求められます(i==0 のときは SplitTime==SegmentTime)
- 計測中のタイム
LivesplitState.CurrentTime
現在のタイマー開始からのタイムLivesplitState.Run[i].SplitTime
現在の試行の Split タイム
まだラップを取ってない区間では null- 自己べタイム
LivesplitState.Run[i].BestSegmentTime
これまでで最も早かった Segment タイム、いわゆる区間ゴールドタイムLivesplitState.Run[i].PresonalBestSplitTime
自己べの時の、その区間の Split タイム- 履歴タイム
LivesplitState.Run[i].SegmentHistory[AttemptID]
AttemptID で指定した試行 ID の Segment タイムで、試行 ID は通常は 1 から始まります
LivesplitState.Run.AttemptHistory[i].Index
から取得できる ID が有効な試行 ID です- 比較対象タイム
LivesplitState.Run[i].Comparisons[ComparisonName]
ComparisonName で指定した Comparison の Split タイム
Livesplit のタイマー操作など
- TimerModel
- タイマーのスタート・ストップなどの操作をしたり、タイマー操作によって発生するイベントをキャッチします
フォルダごとの紹介・説明
公式 Github のフォルダごとに、重要だと思うものを紹介しますソースコードの中にメンバやメソッドの説明がコメントされているものもあります
- LiveSplit/LiveSplit.Core/UI/Components
- コンポーネントを作る際に重要なクラス
- interface IComponent : IDisposable
- 実装必須
MyComponent : IComponent
ソースコードに説明コメントあり - interface IComponentFactory : IUpdateable
- 実装必須
MyComponentFactory : IComponentFactory
ソースコードに説明コメントあり - enum ComponentCategory
- LayoutEditor で追加する際のカテゴリ分け
- class InfoTextComponent : IComponent
- タイマーレイアウトにテキストを表示する際に使用します
InformationName に表示するテキストのタイトルを、InformationValue に表示するテキストの内容を代入して、再描画が必要な時は Update() を呼び出します - class InfoTimeComponent : InfoTextComponent
- タイマーレイアウトにタイムを表示する際に使用します
InformationName に表示するタイムのタイトルを、TimeValue に表示するタイムを、Formatter に TimeFormatter を代入して、再描画が必要な時は Update() を呼び出します
内部的には Formatter が TimeValue を string 型に変換して InformationValue に代入する機能が追加された InfoTextComponent
InfoTimeComponent 自体は TimeSpan? 型のメンバをひとつしか持っていないので、PreviousSegment のようにひとつのタイム表示コンポーネントの中で複数のタイムを表示する場合は、表示する文字列を格納しているメンバに直接追加する必要がありそうです(PreviousSegment コンポーネントのソースがそのように書かれているが、他に方法がないのかは未確認)
- LiveSplit/LiveSplit.Core/Model
- Livesplit内部情報関連
- class LiveSplitState : ICloneable
- Livesplit の内部情報はここから取得します
値の書き換えも可能なので、取り扱いには注意 - int CurrentSplitIndex
- 現在の区間番号
タイマー開始前は -1、最初の区間のインデックスは 0 で、走り終えてタイマーストップした時は Run.Count と同じ値になります
ISegment 配列の Run[] の添え字として区間番号を使用する場合、配列は Run[0] から Run[Run.Count-1] までであり、Run[-1] や Run[Run.Count] は存在しない配列番号であることに注意
- interface IRun : IList<ISegment>, ICloneable, INotifyPropertyChanged
- 主に SplitEditor で表示される項目を保持していて、LivesplitState のメンバとして取得可能
Run[] は ISegment の配列で、AttemptHistory は lss ファイルの AttemptHistory に対応
AttemptHistory は Attempt 構造体の配列なので添え字は 0 から始まりますが、試行 ID の値 AttemptHistory[0].Index は通常は1から始まるので注意 - class Run : IRun, INotifyPropertyChanged
- コンポーネント開発では IRun クラスの方を利用すると思いますが、このクラスのソースコードには説明コメントが書かれているので、IRun の説明コメントを調べたい時はこちらを見た方が良い場合があります
- interface ISegment : ICloneable
- 区間1つ分の情報を保持していて、過去のタイム履歴もここに含まれています
LivesplitState 内では Run[] が ISegment の配列として含まれていることに注意
タイム履歴からタイムを取得する場合は AttemptID と TimingMethod を指定して次のようにしますが、AttemptIDで指定するため SegmentHistory[1]から始まることに注意
SegmentHistory[AttemptID][TimingMethod]
- struct Attempt
- lss ファイルの AttemptHistory タグ内の 1 件分に対応
試行ID、開始日時、終了日時、その時のストップタイムが含まれています
ここに含まれているタイムはストップタイムのみで、区間タイムの履歴は含まれていないことに注意
区間タイムの履歴は ISegment の SegmentHistory に含まれています - static class LiveSplitStateHelper
- タイム取得に関する関数が含まれています
ソースコードに説明コメントあり - struct Time
- タイムを扱うコンポーネントを作るなら、絶対に知っておかなければならないと思います
TimeSpan? RealTime と TimeSpan? GameTime をメンバに持つ構造体で、Livesplit は基本的に Time 型でタイムを扱っているように思います(TimeSpan? は TimeSpan の null 許容型)
Time.RealTime
のようにメンバを直接指定する他、Time[TimingMethod]
のように指定することもできます
また、Time 型は + と - の演算ができます - enum TimerPhase
- 停止中、計測中など、タイマーの状態
- enum TimingMethod
- RealTime と GameTime
この 2 つに関しては asl の紹介記事の中で説明しているので、そちらもご覧ください - class TimerModel : ITimerModel
- このクラスを実体化してメソッドを呼び出すことでタイマーのスタートやリセットなどのタイマー操作を行うことができます
また、タイマー操作で発生するイベントをキャッチすることもできます
asl ファイルではなく dll ファイルとして Autosplitter 機能を実現する場合はこれを利用します(asl ファイルでも TimerModel を利用しているものがあります) - class TimeStamp
- タイマーの精度に関係するタイムスタンプ
- struct AtomicDateTime
- Attempt のメンバである計測開始日時と終了日時は AtomicDateTime? 型で管理されています
日時は AtomicDateTime.Time から取得できますが、この Time メンバは Time 型ではなく DateTime 型であることに注意
- LiveSplit/LiveSplit.Core/UI
- 設定画面関連
- enum GradientType
- 色の単色、縦グラデーション、横グラデーション
- class SettingsHelper
- 設定の保存、読み込みで利用します
- LiveSplit/LiveSplit.Core/TimeFormatters
- タイムを文字にして表示するための書式関連
- interface ITimeFormatter
- TimeSpan? を文字化する場合は Format() メソッドを利用します
既存の TimerFormatter クラスを使用することもできるし、必要に応じて ITimeFormatter を実装したクラスを作ることもできます
このファイル内でマイナスとダッシュの記号が定義されているため、頭の隅に置いておくと良さそう - class GeneralTimeFormatter : ITimeFormatter
- ITimeFormatter を実装した、基本の TimeFormatter
必要に応じてタイムの桁数などの書式、TimeSpan? が null の時に変換する文字などをプロパティに設定します
TimeFormatter の書式例
- class RegularTimeFormatter : GeneralTimeFormatter
class DeltaTimeFormatter : GeneralTimeFormatter
class PossibleTimeSaveFormatter : GeneralTimeFormatter
class AutomaticPrecisionTimeFormatter : GeneralTimeFormatter
class ShortTimeFormatter : GeneralTimeFormatter - GeneralTimeFormatter を継承して、いくつかのプロパティの値が設定されている TimeFormatter
- enum TimeAccuracy
- 1秒、0.1秒、など表示タイムの精度
- enum DigitsFormat
- TimeFormat.cs 内で定義
時間、分などが 0 の時に 0 で埋めるか非表示にするか - enum NullFormat
- タイムが記録されていない場合など、TimeSpan? が null の時に表示する文字、記号
Digits、Accuracy の設定に対応した NullFormat の例
- LiveSplit/LiveSplit.Core/Model/Comparisons
- 比較対象のタイム関連
- interface IComparisons : IDictionary<string, Time>, ICloneable
- 任意の比較対象タイムを取得する場合は
Time t = IComparisons[ComparisonName];
やTimeSpan? ts = IComparisons[ComparisonName][TimingMethod];
のようにすれば良いと思います
IComparisons[ComparisonName] = t;
のように Time 型を代入することはできますが、IComparisons[ComparisonName][TimingMethod] = ts; のような TimeSpan 型の代入はできません
TimeSpan? 型の値を代入したい場合は一旦 Time 型の変数に代入してからその Time 型変数を IComparisons[ComparisonName] に代入する必要があります
ComparisonName は各 ComparisonGenerator クラスの ComparisonName で取得可能、現在 Livesplit で有効になっている Comparison は LivesplitState.Run.Comparisons に登録されています
PB の ComparisonName は class Run で定義されているので注意
ISegment のメンバとして取得可能 - interface IComparisonGenerator
- 比較対象タイムを生成するときに利用します
ジェネレータはあくまで比較対象の「タイムそのもの」を生成するためのものです
比較対象タイムとして表示するためには MyComponent の中で MyComparisonGenerator クラスを実体化してLivesplitState.Run.ComparisonGenerators.Add()
で追加する必要があります
追加した MyComparisonGenerator はMyComponent.Dispose()
メソッドの中などでLivesplitState.Run.ComparisonGenerators.Remove()
によって後始末することを忘れないように注意
- IList<IComparisonGenerator> ComparisonGenerators
- 比較対象タイムを生成するための MyComparisonGenerator をここに登録します
ここに MyComparisonGenerator を登録すると、MyComparisonGenerator.Name が LivesplitState.Run.Comparisons に登録されます
生成タイムの更新が必要なタイミングでMyComparisonGenerator.Generate()
が呼び出されます - IList<string> CustomComparisons
- 手動で入力したタイムや既存のタイムテーブルのタイムからなる Comparison の名前をここに登録します
登録された名前は LivesplitState.Run.Comparisons にも登録されます - IEnumerable<string> Comparisons
- 現在有効な Comparison の名前が登録されます
Run.ComparisonGenerators に追加するはずの ComparisonGenarator を間違えて CustomComparisons に追加すると、同じ ComaprisonName が重複して登録される場合があり、重複登録は Comparison の切替で不具合を発生させるので注意 - 実際に Comparison のタイムを格納する変数がある場所
- タイムの値を格納する変数があるのは ISegment の Comparisons メンバの中で、実際のプログラムでは
LivesplitState.Run[i].Comparisons[comparisonName]
から、ISegment 配列に含まれる comparisonName という名前の Comparison の Time 構造体を取得できます
TimeSpan の値を取得する場合はLivesplitState.Run[i].Comparisons[comparisonName][method]
に格納されています
値を代入する場合は注意があって、LivesplitState.Run[i].Comparisons[comparisonName]
に Time 構造体を代入することはできますが、LivesplitState.Run[i].Comparisons[comparisonName][method] に TimeSpan? を代入することはできません - MyComparison.Generate() が呼び出されるタイミング
- 私が確認した範囲では次の場合に Generate() が呼び出されます
あくまで実際に動かしてみて確認した範囲であり、モレがあったり不正確だったりする可能性があるので、ご注意ください
- コンポーネントが含まれている状態で Livesplit を起動
- タイマーリセット
- タイマーストップの状態でスタート or リセットして待機状態に戻す
- lss ファイルをロード
- 右クリックメニューから開く Settings 画面を OK で閉じる
- SegmentEditor を Cancel で閉じる
タイマーレイアウトに MyComponent を追加しただけでは Generate() は呼び出されないので注意
私の経験では MyComponent のコンストラクタで MyComponentGenerator を実体化してすぐに自分で Generate() を呼び出しても履歴タイムの取得などがうまくいかない場合がありました
なので、追加直後に MyComparison の Generate() が呼び出されないのは Livesplit の仕様と考えて、Generate() の呼び出しは Livesplit に任せてしまうのもアリかなと思います
- LiveSplit/UpdateManager
- (おそらくは)コンポーネントのバージョンアップ関係
- interface IUpdateable
- IComponentFactory はこれを継承しているため、MyComponentFactory には IUpdateable のメンバを含める必要があります
説明文がなく、動作の確認もできていませんが、おそらくはネット経由のコンポーネントのバージョンアップに関係していると思います
Version はおそらくコンポーネントのバージョン
- LiveSplit/LiveSplit.Core/Model/RunSavers
- 記録の外部出力関係
- public class XMLRunSaver : IRunSaver
- lss ファイルへの書き出し
- public class ExcelRunSaver : IRunSaver
- 実際に出力したExcelを見ながらソースを読むと、タイムの取得に関して非常に参考になると思います
このクラスの中では Excel に出力する際に TimeSpan.TotalDays で日付換算しており、おそらくは Excel の日付表示の書式に合わせているためだと思います
- LiveSplit/LiveSplit.Core/Model/RunFactories
- 記録の外部入力関係
- public class StandardFormatsRunFactory : IRunFactory
- lss ファイルの読み取り
- LiveSplit/LiveSplit.View/View
- コンポーネント個別のフォームを除く、Livesplit のフォーム
- public partial class TimerForm : Form
- Livesplit を起動すると表示される一般的に Livesplit と認識されているフォームで、LivesplitState や TimerModel などはここで実体化しています
コンポーネントの Update() はここで呼び出しています - public partial class LayoutEditorDialog : Form
- 右クリック → Edit Layout で開く Layout Editor
- public partial class LayoutSettingsControl : UserControl
- 右クリック → Edit Layout → Layout で開く Layout Settings
最初の Layout タブの内容はここに含まれていますが、それ以外のタブは各コンポーネントに含まれているフォームを表示しています - public partial class RunEditorDialog : Form
- 右クリック → Edit Splits で開く Splits Editor
- public partial class SettingsDialog : Form
- 右クリック → Settings で開く Settings
- public partial class ShareRunDialog : Form
- 右クリック → Share で開く Sharer
Twitter に投稿するテキスト中の $title や $splittime などの置き換えはここで行われています
Speedrun Tool Development サーバの #tutorials チャンネルが整備されたことに合わせて、記述内容を一部変更
返信削除