2021/04/10

Livesplit用コンポーネントの作り方

コンポーネント開発に興味を持った方が開発に必要な情報を調べようとした時に少しでも助けになればと思い、私が調べた情報などを紹介するためにこの記事を書きました

この記事はコンポーネント開発に必要な全ての情報を記載している訳ではありませんが、公式のソースコードを読み解くための参考資料として活用していただけると幸いです

この記事の中で My~ と書いている部分は開発するコンポーネント名と考えてください


開発資料リンク

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# の範囲で知っていると便利なこと

型の規定値
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 に記述しているものもあります
Discord の Speedrun Tool Development サーバの #tutorials チャンネルが整備される以前に #Guide チャンネルに掲載されていたコンポーネントの作り方のガイドは、コンポーネント開発の最初の一歩を踏み出す際に有用なガイドだと思うので、ここで紹介します

簡単に和訳すると次のような内容です
コンポーネントの作り方
  1. 新しいプロジェクトの作成を選び、C# のクラスライブラリ(.NET Framework)を選択
    .NET Framework 4.6.1 を使用すること
  2. LiveSplit.Core.dll と UpdateManager.dll をプロジェクトの参照に追加
    プロジェクトでは LiveSplit.UI.Components 名前空間を使用すること
  3. プロジェクトに MyComponent クラスを追加
    IComponent インターフェースを実装するか、実装したクラスを継承する必要がある
  4. プロジェクトに MyComponentFactory クラスを追加
    IComponentFactory を実装する必要がある
  5. MyComponentFactory.Create() メソッドが MyComponent のインスタンスを返すように実装
  6. AssemblyInfo.sc ファイルに次のような行を追加
    [assembly: ComponentFactory(typeof(MyComponentFactory))]

プロジェクトのプロパティ設定
プロジェクトのプロパティで次の設定をしておくと、開発作業の一部が楽になると思います
アプリケーション
アセンブリ名: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
    タブを開くたびに発生

クラスの実体化やメソッド呼び出しのタイミング・順序

特に重要だと思ったメソッドを中心に、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
タイマーレイアウトにテキストを表示する際に使用します
InfoTextComponentのプロパティとタイマーへの表示
InformationName に表示するテキストのタイトルを、InformationValue に表示するテキストの内容を代入して、再描画が必要な時は Update() を呼び出します
class InfoTimeComponent : InfoTextComponent
タイマーレイアウトにタイムを表示する際に使用します
InfoTimeComponentのプロパティとタイマーへの表示
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
Real Time と Game Time

この 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 の書式例
各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 の例
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
比較対象タイムを生成するときに利用します
 ジェネレータはあくまで比較対象の「タイムそのもの」を生成するためのものです

MyComparisonGeneratorとLivesplitStateの関係
比較対象タイムとして表示するためには MyComponent の中で MyComparisonGenerator クラスを実体化して LivesplitState.Run.ComparisonGenerators.Add() で追加する必要があります
 追加した MyComparisonGenerator は MyComponent.Dispose() メソッドの中などで LivesplitState.Run.ComparisonGenerators.Remove() によって後始末することを忘れないように注意
LivesplitState.Run および LivesplitState.Run[i] のメンバに含まれている各 Comparisonについて
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 などの置き換えはここで行われています

1 件のコメント:

  1. Speedrun Tool Development サーバの #tutorials チャンネルが整備されたことに合わせて、記述内容を一部変更

    返信削除