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 のソースコードを読み解く際に必要になる項目」について少し説明しておこうと思います。
(つまり私が知らなくて、適切な検索ワードも思いつかなくて苦労した項目などの紹介です)

C# の仕様関連で知っていると便利なこと

null 許容型

型名? 変数名

int や double など C# の値型に分類される型では、その型が意味する値のみを代入できます。
 そのような値型の変数の宣言で型名に ? を付けると、その型が扱える値に加えて null も代入することができる null 許容型になります。

Livesplit ではタイムに関する値のほとんどが TimeSpan? 型の変数を利用しており、計測タイムが存在しない状態などを表す場合に null が代入され、TimeSpan.Zero とは別の意味を持っています。
 記録の履歴がない状態で Livesplit の Split が "-" と表示されるのも、TimeSpan? 型に null が代入されている状態です。

TimeSpan? 型の変数では、Seconds や Ticks といった TimeSpan のプロパティに直接アクセスすることができないため注意が必要です。
 このような場合は null 合体演算子や null 条件演算子と組み合わせて使用すると良いです。

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" として扱われるようになります。
 そのため、逐語的文字列リテラルの中で改行を行うと、その位置で改行された文字列として扱われます。

List<ISegment> Run などの添え字の範囲チェック

if ((uint)index < (uint)Run.Count) { }

添え字の範囲は 0 以上かつ Run.Count 未満なので、有効な添え字の範囲内の場合に処理をする if 文を素直に書くと if (0 <= index && index < Run.Count) { } のようにとなると思います。
このような 0 以上かつ ** 未満の場合は if ((uint)index < (uint)Run.Count) { } のように書くことができます。

2 回の比較と && 演算だったのが 1 回の比較だけとなるためパフォーマンス面で有利で、C# の最適化において定番の書き方らしいです。
添え字の範囲が 0 以上かつ int 値未満という前提において成立することに注意。

早期リターンなどの場合は条件を反転して if ((uint)index >= (uint)Run.Count) { return; } と書くことができます。

型の規定値

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 が起動します。


コンポーネント開発で利用する定番クラス

コンポーネントを開発する際に必須だったり利用する頻度が高いと思うクラスを中心に、簡単に紹介します。
 詳細な説明は、記事後半の「フォルダごとの紹介・説明」をご確認ください。

コンポーネントを構成する基本のクラス

MyComponent

コンポーネントの処理本体、インターフェース IComponent の実装。
 タイマーレイアウト上に表示する必要がなければ抽象クラス LogicComponent の実装でも OK。

MyComponentFactory

Livesplit がコンポーネントを利用する際に呼び出します。
 インターフェース IComponentFactory の実装。

MyComponentSettings

LayoutEditor で表示する設定画面。
 設定画面が不要であれば、このクラスを作る必要はありません。

一般的なコンポーネントの開発で利用

InfoTextComponent
InfoTimeComponent

タイマーレイアウト上でテキスト、タイムを表示します。

Time

Livesplit で扱うタイムの基本単位。
TimeSpan? 型の RealTime と GameTime の 2 つを一組として扱います。

SettingsHelper

設定画面の状態を保存・読み込みをする時に利用します。

ITimeFormatter

タイムを文字にして表示する時は、基本的には TimeFormatter を利用します。
Format() メソッドに TimeSpan? 型を渡して、文字列を生成します。

IComparisonGenerator

比較対象タイムを生成する時に利用します。

Livesplit が持っている情報

LiveSplitState

(おそらくは)ほぼ全ての Livesplit が持っている情報をここから取得できます。

IRun

lss ファイルが持っているのと同じ情報を取得できます。

ISegment

区間一つ分の情報を取得できます。
 比較対象タイムのタイムそのものや、履歴タイムなどもここに格納されます。

Livesplit のタイマー操作など

TimerModel

タイマーのスタート・ストップなどの操作をしたり、タイマー操作によって発生するイベントをキャッチします。


コンポーネントの処理の流れ

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

特に重要だと思ったメソッドを中心に、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 内で発生した例外は Livesplit 本体で chatch されます。

例外の発生場所と本体側の処理
例外の発生場所 本体の処理 参考(本体側のソースコード)
コンストラクタ レイアウトへの追加を中断 AddComponent() ->
MyComponentFactory.Create()
.Update() ログに出力 InvalidateForm() ->
UpdateAllComponents() ->
MyComponent.Update()
.DrawVertical()
.DrawHorizontal()
ログに出力 TimerForm_Paint() ->
PaintForm() ->
ComponentRenderer.Render

タイマー画面への描画

描画を行うメソッド

画面の描画処理は MyComponent の .DrawVertical() または .DrawHorizontal() で行います。
 Livesplit のレイアウトが垂直モードか水平モードかによって、どちらのメソッドが呼び出されるかが決まります。
 これらのメソッドは Livesplit 本体が呼び出すため、任意のタイミングで再描画を行いたい場合は Update() の中で invalidator.Invalidate() メソッドを呼び出します。

.DrawVertical() の場合は float width が、.DrawHorizontal() の場合は float height が渡され、これが描画範囲の幅 or 高さです。
 高さ or 幅 が足りないように思うかもしれませんが、MyComponent 自身が管理する値 VerticalHeight or HorizontalWidth なので Livesplit 本体からもらう必要がないのです。

(Update() でも引数で float width, float height をもらいますが、試した限りでは .DrawVertical() や .DrawHorizontal() がもらう width や height とは異なる値で、よく分かりません)

ブラシ・ペン

使ったら後片付けを忘れずに。

インスタンスを作る際に using を利用するか、最後に .Dispose() を呼び出します。
(既存のコンポーネントを見ると後片付けをしていないように見えるものもありますが、おそらくは GC が働いているものと思います)

GraphicsCache

LiveSplit.Core.UI に GraphicsCache クラスがあり、これを再描画の判定に使用することができます。

GraphicsCache の使用例
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.
class MyComponent : IComponent
{
    GraphicsCache Cache { get; set; }
    
    MyComponent()
    {
        // GraphicsCache のインスタンスを作成
        Cache = new GraphicsCache();
    }
    
    void Update(IInvalidator invalidator, LiveSplitState state, float width, float height, LayoutMode mode)
    {
        // Cache.HasChanged = false にする
        Cache.Restart();
        
        // 変更の有無を判定したい値を渡す
        Cache["任意の名前"] = (変数やオブジェクト);
        
        // 変更があったら再描画
        if (invalidator != null && Cache.HasChanged)
        {
            invalidator.Invalidate(0, 0, width, height);
        }
    }
}

.Update() で使用するのが一般的で、最初に .Restart() メソッドで変更判定フラグをリセットして、変更の有無を判定したい値を渡すと判定が行われ、フラグが立った場合は再描画を行います。

例えば、設定項目が変更された時に再描画したいのであれば Cache["Settings"」 = Settings.GetSettingsHashCode(); のようにします。


設定画面

Layout Editor から開く Layout Settings で表示する設定画面(MyComponentSettings)はユーザーコントロールとして作成します。

MyComponentSettings ユーザーコントロール

基本的なサイズの設定

ユーザーコントロール全体の最大幅 476 px、最大高さ 516 px(スクロールバーなしの場合)

縦方向のスクロールバーが表示されている場合
ユーザーコントロール全体の最大幅 459 px

margin 上下左右とも 3 px(デフォルト値)
padding 上下 6 px、左右 7 px

使用可能サイズ+余白
幅 462 + 14

設定画面の親フォームの幅が固定で高さの最小値が決まっているので、設定画面も横幅固定にして作るという考え方もあります。

あるいは、設定画面はサイズ変更に対応できるように作っておいて親フォームに合わせて変形させる考え方もあります。
例えば、PaceAlert コンポーネントでは設定画面に配置しているコントロール類のアンカーとドックを適切に設定したうえで、MyComponentSettings のコンストラクタで自分自身に対して Dock = DockStyle.Fill; に設定しています。

設定画面がダイアログの範囲に収まらない場合の処理

設定画面が表示されるダイアログのサイズは決まっていますが、MyComponentSettings ユーザーコントロールがそこに収まらない場合もあります。(ダイアログのサイズは横方向は固定ですが、縦方向には伸ばすことができます)

ダイアログにおまかせ

ユーザーコントロール側で何も対策せずにサイズオーバーした場合、ダイアログ側が縦方向のスクロールバーを出してくれてユーザーコントロール全体がスクロールするようになります。
例えば標準の Splits コンポーネントの設定画面などがこのパターンです。

ユーザーコントロール側で対処

ユーザーコントロール全体がスクロールすると使い勝手が悪いような場合は、ユーザーコントロール側で部分的にスクロールする部分を設けるなどしてサイズオーバーしないように対策します。
例えば PaceAlertText Images の設定画面がこのパターンです。

配置するコントロール類

TableLayoutPanel

基本的に TableLayoutPanel の中にコントロールを配置します。
 1行の高さは 27px で、配置するコントロールそれぞれにアンカー左右を設定。

TableLayoutPanel の幅 462 px

margin 上下左右とも 3 px(デフォルト値)
padding 上下左右とも 0 px(デフォルト値)

公式のコンポーネントでは TableLayoutPanel → グループボックス → TableLayoutPanel のような入れ子をよく見ますが、MSDN のガイドラインでは TableLayoutPanel を入れ子にすることは推奨されていません。
 これは入れ子ではない?

フォント

MS UI ゴシック、9pt が標準。
 VisualStudio デフォルト値のままで大丈夫なはず。

コンボボックスなどに列挙型のメンバを表示させる方法

コンボボックスの選択肢は列挙型のメンバと対応していると、可読性や拡張性などで有利だと思います。

参考

列挙型をコンボボックスで選択できるようにするウルテク
enum を文字列で置き換えて ComboBox に表示する

ちなみに、コンボボックスは文字入力できる状態がデフォルトです。
 単に項目の選択のみで文字入力が不要な場合は、プロパティの DropDownStyl に DropDownList を設定します。

設定画面に関係するイベント

MyComponentSettings のイベント

Load

初めてタブが開かれる時に発生。
設定画面を閉じると、次にタブが開かれたときにも発生。

コントロール類の初期設定はここで行うのが一般的です。

VisibleChanged

タブを開くたびに発生。

参考

【C#】Windows.Formsのイベント順序
Windows Form を開いたり閉じたりした時の各イベントが図でまとめられているので、非常に参考になります。
 ただし、コンポーネントの設定画面においては、一部、発生しないイベントもあります。

親フォームのイベント

親フォーム(LayoutSettingsDialog)は MyComponentSettings.ParentForm で取得できます。
ただし親フォームの Load イベント後に取得する必要があり、MyComponentSettings のコンストラクタでは取得できないので注意。

これによって、親フォームのイベントを購読することができます。
一般的にイベントを購読したら解除も必要ですが、イベント発生側とイベント購読側の寿命に差がない場合は解除必須ではないらしいです。

親フォームを開いた

MyComponentSettings.Load イベント時に親フォームを取得する場合は MyComponentSettings タブを直接開いた場合しかイベントを拾えないぽいので、自分自身のイベントである MyComponentSettings.Load や MyComponentSettings.VisibleChanged を利用したほうが確実だと思います。

  • VisibleChanged
  • Activated
  • Shown
親フォームを閉じた

MyComponentSettings 自身は設定画面が閉じられたことを検知できないぽいので、親フォームのイベント購読で利用価値があるのはここかなと思います。

  • FormClosed
  • Deactivate
  • VisibleChanged
親フォームを開きながら別のウィンドウをアクティブにした
  • Deactivate
別のウィンドウがアクティブな状態から親フォームをアクティブにした
  • Activated

設定項目の値

設定項目の初期値

設定項目の初期値は、基本的に MyComponentSettings のコンストラクタで行います。
 MyComponent をレイアウトに追加した場合、設定項目の値はこの初期値が使用されます。

MyComponent を追加した状態で .lsl ファイルを保存している場合、MyComponentSettings.SetSettings() にて SettingsHelper.Parse****() メソッドを使って .lsl ファイルから読み取った値を変数に代入します。

MyComponent のバージョンアップなどで設定項目が増えた際に、コンストラクタで与えた初期値が使用されない場合があるため注意が必要です。
 例えば、MyComponent のバージョンアップをした際に古いバージョンの時に保存した .lsl ファイルを新しいバージョンで読み取る場合があり、新しいバージョンで追加された設定項目に対応する値が .lsl ファイルに存在しないため SettingsHelper.Parse****()定義されたデフォルト値が与えられます。
 SettingsHelper.Parse****() で定義されたデフォルト値が初期値として与えたい値と一致しているならば良いのですが、異なる値だった場合に不具合の原因となり得ます。

SettingsHelper.Parse****() メソッドの第2引数に初期値を渡すことで、その設定項目に対応する値が .lsl ファイルになかった場合でも、意図した通りの初期値を得ることができます。

設定値の保存と読込

設定値の保存と読込
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
class MyComponentSettings : UserControl
{
    private int CreateSettingsNode(XmlDocument document, XmlElement parent)
    {
        // XML に任意の名前で設定項目の値を書き込み、
        //設定項目のハッシュ値を返す
        return SettingsHelper.CreateSetting(document, parent, "任意の名前", 設定項目);
    }

    public void SetSettings (XmlNode node)
    {
        // .lslから読み取った XML から任意の名前に対応する値を読み取って、
        // 設定項目に代入
        // Parse**** は設定項目の型に対応するものを使用する
        設定項目 = SettingsHelper.Parse****(element["任意の名前"]);
    }
}
保存

SettingsHepler.CreateSetting()
値 → XML → .lsl

第4引数のハッシュ値が返ってきます。
 複数の設定項目がある場合はそれらすべてのハッシュ値を ^ 演算子でつなげ、その結果を CreateSettingsNode() の return 値とします。

読込

SettingsHelper.Parse****()
.lsl → XML → 値
****の部分は値の型に応じて決まります。

第2引数に値を渡すことができ、その詳細については「設定項目の初期値」をご参照ください。


実際にタイムの値が格納されている場所

Livesplit のタイムは Time 構造体を基本単位として扱われており、基本的に Split タイムと Segment タイムのどちらかです。(一部、例外もあり)
 Split タイムはタイマー開始から各区間終了時までのタイム、Segment タイムはその区間のみの区間タイムです。

LiveSplitState からアクセスできるタイム


多くのタイムは LivesplitState.Run あるいは LivesplitState.Run[] からたどった場所に格納されています。
 Run は [] の有無で扱いが異なっており、LivesplitState.Run は IRun を実装したクラスで、LivesplitState.Run[] は List<ISegment> です。
 Run[] は SegmentEditor で作成した各区間に対応していて、最初の区間は Run[0]、最後の区間は Run[Run.Count-1] です。

 タイムの種類によって Split タイムか Segment タイムかが決まっているため、例えば、Segment タイムが欲しいのに Split タイムしかないような場合は、その区間の Split タイムから 1 つ前の区間の Split タイムを引くことで Segment タイムを求めることができます。(ただし、最初の区間の場合は SplitTime==SegmentTime)

計測中のタイム

タイムの種類によって、LivesplitState のメンバであったり Run のメンバであったりと色々なので注意が必要です。
 また、LiveSplitStateHelper クラスのメソッドによって取得できるものもあります。

現在のタイマー開始からのタイム

LivesplitState.CurrentTime

現在の試行の区間[i] での Split タイム

LivesplitState.Run[i].SplitTime
まだラップを取ってない区間では null。

現在の試行の計測中の区間での Segment タイム

LiveSplitStateHelper.GetLiveSegmentTime(state, splitNumber, method)
現在計測中のタイムの内、まさに現在計測中の区間のタイムを取得するのが主な用途かと思います。

splitNumber の値と戻り値
splitNumber の値 戻る値
< 現在区間 splitNumber で指定した区間から現在計測中のタイムまでの区間タイムの合計
== 現在区間 現在計測中の区間のタイム
> 現在区間 現在計測中の区間のタイム
state.Run[] の範囲外 例外発生

CurrentPhase が NotRunning の場合は TimeSpan.Zero が戻る。

現在の試行のラップ済みの区間での Segment タイム

LiveSplitStateHelper.GetPreviousSegmentTime(state, splitNumber, method)
現在計測中のタイムの内、ラップ済みの区間タイムを取得するのが主な用途かと思います。

splitNumber の値と戻り値
splitNumber の値 戻る値
< 現在区間 splitNumber で指定した区間の区間タイム
== 現在区間 null
> 現在区間 null
state.Run[] の範囲外 例外発生

CurrentPhase が NotRunning の場合は null が戻る。

自己ベストタイム

区間ベストタイム

LivesplitState.Run[i].BestSegmentTime
区間[i] での区間ベストタイム。

自己べスト時の、区間[i] での Split タイム

LivesplitState.Run[i].PresonalBestSplitTime
自己ベスト時に区間[i] を通過した時のタイマースタートからのタイム。

履歴タイム

ゴールタイムは Run のメンバからたどった先に、区間タイムは Run[i] のメンバにあるため注意です。

履歴[j] でのゴールタイム

LivesplitState.Run.AttemptHisotry[j].Time

試行 ID[AttemptID] での区間[i] での Segment タイム

LivesplitState.Run[i].SegmentHistory[AttemptID]
区間を指定する i と履歴ID を指定する AttemptID の2つが必要です。
 履歴[j] の試行 ID は LivesplitState.Run.AttemptHistory[j].Index で取得でき、通常は 1 から始まります。
 スキップしたり途中でリセットするなどによって、履歴 ID が存在していても LivesplitState.Run[i].SegmentHistory[AttemptID] に必ずしもタイムが格納されているとは限りません。

比較対象タイム

区間[i] での比較対象タイム ComparisonName での Split タイム

LivesplitState.Run[i].Comparisons[ComparisonName]
ComparisonName は Run.Comparisons[] に含まれているものが有効。

詳細は LivesplitState に含まれている各 Comparison にて説明


比較対象タイム Comparison の生成

Comparison には PB のようにタイムそのものを保持するものと、Average のようにタイムを生成するものとがあります。
 ここではタイムを生成する Comparison をコンポーネントで作る方法を説明します。
MyComparisonGeneratorとLivesplitStateの関係

タイムを生成する ComparisonGenerator

ComparisonGenerator で Comparison を生成する手順

  1. IComparisonGenerator を実装した MyComparisonGenerator クラスを作る。
  2. MyComponent クラスの中で MyComparisonGenerator を実体化する。
  3. LivesplitState.Run.ComparisonGenerators.Add() で Livesplit に登録する。
  4. Livesplit から MyComparisonGenerator.Generate() が呼び出される。
  5. .Generate() の中で生成したタイムを LivesplitState.Run[i].Comparisons[comparisonName] に代入する。

生成したタイムを Comparison に代入する際の注意

IComparisons[ComparisonName] = t; のように Time 構造体を代入することはできますが、IComparisons[ComparisonName][TimingMethod] = ts; のような TimeSpan 型の代入はできません。
 TimeSpan? 型の値を代入したい場合は一旦 Time 構造体に代入してその Time 構造体を IComparisons[ComparisonName] に代入する必要があります。

Comparison の後始末

追加した MyComparisonGenerator は MyComponent.Dispose() メソッドの中などで LivesplitState.Run.ComparisonGenerators.Remove() によって後始末することを忘れないように注意。

MyComparison.Generate() が呼び出されるタイミング

私が確認した範囲では次の場合に Generate() が呼び出されます。
 あくまで実際に動かしてみて確認した範囲であり、モレがあったり不正確だったりする可能性があるので、ご注意ください。

  • コンポーネントが含まれている状態で Livesplit を起動。
  • タイマーリセット。
  • タイマーストップの状態でスタート or リセットして待機状態に戻す。
  • lss ファイルをロード。
  • 右クリックメニューから開く Settings 画面を OK で閉じる。
  • SegmentEditor を Cancel で閉じる。

タイマーレイアウトに MyComponent を追加しただけでは Generate() は呼び出されないので注意。
 私の経験では MyComponent のコンストラクタで MyComponentGenerator を実体化してすぐに自分で Generate() を呼び出しても履歴タイムの取得などがうまくいかない場合がありました。
 なので、追加直後に MyComparison の Generate() が呼び出されないのは Livesplit の仕様と考えて、Generate() の呼び出しは Livesplit に任せてしまうのもアリかなと思います。

Comparison の名前の設定

ComparisonGenerator は IComparisonGenerator の実装なので Name プロパティを必ず持っていますが、これとは別に ComparisonName や ShortComparisonName も持っている場合があります。

Name

IComparisonGenerator で定義されているため、必須のプロパティです。
 標準の ComparisonGenerator では Name は ComparisonName を返しています。

ComparisonName

設定画面上やタイマーレイアウト上に Comparison 名を表示する場合に利用されることが多い名前です。

ShortComparisonName

任意で設定できる短い Comparison 名です。  文字の表示幅が小さくて ComparisonName の文字数では収まらないような場合に ShortComparisonName が利用されます。  例えば、標準のComparisonGenerator では "Average Segments" なら "Average"、"Best Segments" なら "Best" が設定されています。

LivesplitState に含まれている各 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? を代入することはできません。

フォルダごとの紹介・説明

公式 Github のフォルダごとに、重要だと思うものを紹介します。
 ソースコードの中にメンバやメソッドの説明がコメントされているものもあります。


LiveSplit/LiveSplit.Core/UI/Components

コンポーネントを作る際に重要なクラス。

interface IComponent : IDisposable

IComponent.cs

コンポーネント開発における基本的なインターフェース。
基本的に実装必須 MyComponent : IComponent
ソースコードに説明コメントあり。

必ず IComponent を実装する必要がある訳ではなく、例えば、タイマーレイアウト上に表示する必要がない場合は LogicComponent、タイマーレイアウト上にコントロール類を表示したい場合は ControlComponent を実装します。

interface IComponentFactory : IUpdateable

IComponentFactory.cs

実装必須 MyComponentFactory : IComponentFactory
ソースコードに説明コメントあり。

Livesplit 本体が MyComponent を利用する際に、MyComponentFactory が最初の入り口となります。
Layout Editor で表示するコンポーネント名やコンポーネントのカテゴリ(Timer、List、Infomation などの分類)などはこのクラスで設定します。

また、IComponentFactory は IUpdateable を継承しているので、MyComponentFactory は IUpdateable も実装する必要があります。
IUpdateable は Livesplit 起動時に行われるコンポーネントの更新チェックで利用されます。

enum ComponentCategory

ComponentCategory.cs

LayoutEditor で追加する際の Timer、List、Infomation などの分類。

class InfoTextComponent : IComponent

InfoTextComponent.cs

タイマーレイアウトにテキストを表示する際に使用します。
InfoTextComponentのプロパティとタイマーへの表示

InformationName に表示するテキストのタイトルを、InformationName に表示するテキストの内容を代入して、再描画が必要な時は Update() を呼び出します。

InformationName と InformationName は SimpleLabel クラスなので、SimpleLabel クラスも把握しておくと意図しない挙動に対処しやすくなると思います。

class InfoTimeComponent : InfoTextComponent

InfoTimeComponent.cs

タイマーレイアウトにタイムを表示する際に使用します。
InfoTimeComponentのプロパティとタイマーへの表示

InformationName に表示するタイムのタイトルを、TimeValue に表示するタイムを、Formatter に TimeFormatter を代入して、再描画が必要な時は Update() を呼び出します。
 内部的には Formatter が TimeValue を string 型に変換して InformationValue に代入する機能が追加された InfoTextComponent。

InfoTimeComponent 自体は TimeSpan? 型のメンバをひとつしか持っていないので、PreviousSegment のようにひとつのタイム表示コンポーネントの中で複数のタイムを表示する場合は、表示する文字列を格納しているメンバに直接追加する必要がありそうです。(PreviousSegment コンポーネントのソースがそのように書かれているが、他に方法がないのかは未確認)

abstract class LogicComponent : IComponent

LogicComponent.cs

タイマーレイアウト上には何も表示しないコンポーネントを作る場合に、この抽象クラスを実装すると楽です。

代表例は標準の Sound コンポーネント

タイマーレイアウト上に表示するために必要なプロパティやメソッドに関連したコードを書かなくて済みます。

abstract class ControlComponent : IDeactivatableComponent

ControlComponent.cs

タイマーレイアウト上に Windows フォームのコントロールを表示したい場合に、この抽象クラスを実装します。

代表例は標準の Video コンポーネント

Livesplit のサイズからコントロールのサイズを計算してくれるメソッドが用意されているので、明確な意図がないならそのメソッドにお任せで OK。


LiveSplit/LiveSplit.Core/UI

タイマーレイアウトへの表示や設定画面に関連。

class SimpleLabel

SimpleLabel.cs

タイマーレイアウト上に文字を表示する際の1つ1つのセルに相当します。

例えば、Text コンポーネントであれば左のテキストと右のテキストのそれぞれが SimpleLabel で、Splits コンポーネントであればそれぞれの区間が行で区間名やそれぞれのタイム部分が列でその行列を構成している1つ1つのセルが SimpleLabel です。

コンポーネントで InternalComponent というオブジェクトがあったら、中身は大体これの印象。

基本的に、表示するテキストを Text プロパティに代入して、.Drow() メソッドを実行することでそのテキストをタイマーレイアウトに表示します。

Text プロパティに加えて、必要に応じて ICollection<string> AlternateText プロパティにもテキストを代入していると Text と AlternateText の中から表示部分の幅に収まる最大文字数のものをタイマーレイアウトに表示します。

enum GradientType

GradientType.cs

色の単色、縦グラデーション、横グラデーション。

class GraphicsCache

GraphicsCache.cs

再描画の判定に使用します。
詳細はタイマー画面への描画を参照。

class SettingsHelper

SettingsHelper.cs

設定の保存、読み込みで利用します。
詳細は設定項目の値を参照。


LiveSplit/LiveSplit.Core/Model

Livesplit内部情報関連。

class LiveSplitState : ICloneable

LiveSplitState.cs

Livesplit の内部情報はここから取得します。
 値の書き換えも可能なので、取り扱いには注意。

LiveSplitState のタイマー操作イベントを購読した場合、sender は LiveSplitState です。

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

IRun.cs

主に SplitEditor で表示される項目を保持していて、LivesplitState のメンバとして取得可能。
 Run[] は ISegment の配列で、AttemptHistory は lss ファイルの AttemptHistory に対応。
 AttemptHistory は Attempt 構造体の配列なので添え字は 0 から始まりますが、試行 ID の値 AttemptHistory[0].Index は通常は1から始まるので注意。

class Run : IRun, INotifyPropertyChanged

Run.cs

コンポーネント開発では IRun クラスの方を利用すると思いますが、このクラスのソースコードには説明コメントが書かれているので、IRun の説明コメントを調べたい時はこちらを見た方が良い場合があります。

interface ISegment : ICloneable

ISegment.cs

区間1つ分の情報を保持していて、過去のタイム履歴もここに含まれています。
 LivesplitState 内では Run[] が ISegment の配列として含まれていることに注意。

タイム履歴からタイムを取得する場合は AttemptID と TimingMethod を指定して次のようにしますが、AttemptIDで指定するため SegmentHistory[1]から始まることに注意。
SegmentHistory[AttemptID][TimingMethod]

struct Attempt

Attempt.cs

lss ファイルの AttemptHistory タグ内の 1 件分に対応。
 試行ID、開始日時、終了日時、その時のストップタイムが含まれています。
 ここに含まれているタイムはストップタイムのみで、区間タイムの履歴は含まれていないことに注意。
 区間タイムの履歴は ISegment の SegmentHistory に含まれています。

static class LiveSplitStateHelper

LiveSplitStateHelper.cs

タイム取得に関する関数が含まれています。
 ソースコードに説明コメントあり。

struct Time

Time.cs

タイムを扱うコンポーネントを作るなら、絶対に知っておかなければならないと思います。
 TimeSpan? RealTime と TimeSpan? GameTime をメンバに持つ構造体で、Livesplit は基本的に Time 型でタイムを扱っているように思います。(TimeSpan? は TimeSpan の null 許容型)
 Time.RealTime のようにメンバを直接指定する他、Time[TimingMethod] のように指定することもできます。
 また、Time 型は + と - の演算ができます。

enum TimerPhase

TimerPhase.cs

停止中、計測中など、タイマーの状態。

enum TimingMethod

TimingMethod.cs

RealTime と GameTime。
Real Time と Game Time

この 2 つに関しては asl の紹介記事の中で説明しているので、そちらもご覧ください。

class TimerModel : ITimerModel

ITimerModel.cs

このクラスを実体化してメソッドを呼び出すことでタイマーのスタートやリセットなどのタイマー操作を行うことができます。
 また、タイマー操作で発生するイベントをキャッチすることもできます。
 asl ファイルではなく dll ファイルとして Autosplitter 機能を実現する場合はこれを利用します。(asl ファイルでも TimerModel を利用しているものがあります)

class TimeStamp

TimeStamp.cs

タイマーの精度に関係するタイムスタンプ。

struct AtomicDateTime

AtomicDateTime.cs

Attempt のメンバである計測開始日時と終了日時は AtomicDateTime? 型で管理されています。
 日時は AtomicDateTime.Time から取得できますが、この Time メンバは Time 型ではなく DateTime 型であることに注意。


LiveSplit/LiveSplit.Core/TimeFormatters

タイムを文字にして表示するための書式関連。

interface ITimeFormatter

ITimeFormatter.cs

TimeSpan? を文字化する場合は Format() メソッドを利用します。
 既存の TimerFormatter クラスを使用することもできるし、必要に応じて ITimeFormatter を実装したクラスを作ることもできます。

このファイル内でマイナスとダッシュの記号が定義されているため、頭の隅に置いておくと良さそう。

class GeneralTimeFormatter : ITimeFormatter

class RegularTimeFormatter : GeneralTimeFormatter
class DeltaTimeFormatter : GeneralTimeFormatter
class PossibleTimeSaveFormatter : GeneralTimeFormatter
class AutomaticPrecisionTimeFormatter : GeneralTimeFormatter
class ShortTimeFormatter : GeneralTimeFormatter

GeneralTimeFormatter.cs
RegularTimeFormatter.cs
DeltaTimeFormatter.cs
PossibleTimeSaveFormatter.cs
AutomaticPrecisionTimeFormatter.cs
ShortTimeFormatter.cs

ITimeFormatter を実装した GeneralTimeFormatter と、GeneralTimeFormatter を継承した各種 TimeFormatter があります。
 必要に応じてタイムの桁数などの書式や TimeSpan? が null の時に変換する文字などのプロパティを設定します。

各種 TimeFormatter のプロパティ初期値
プロパティ
\class
General Regular Delta Possible Automatic
Precision
Short
Accuracy Seconds Seconds Tenths Seconds Hundredths Hundredths
Digits
Format
Single
Digit
Seconds
Single
Digit
Minutes
Single
Digit
Minutes
Null
Format
Dash ZeroWith
Accuracy
Dash ZeroWith
Accuracy
ZeroWith
Accuracy
ShowDays false
ShowPlus false true
Drop
Decimals
false true false
Automatic
Precision
false true

空欄は General と同じ

各種 TimeFormatter.Format() の例
TimeSpan
\class
General Regular Delta Possible Automatic
Precision
Short
1.234 1 0:01 +1.2 1 0:01.23 1.23
36:34:56.789 36:34:56 36:34:56 +36:34:56 36:34:56 36:34:56.78 36:34:56.78
TimeSpan
.Zero
0 0:00 +0.0 0 0:00 0.00
null - 0 - - 0 0.00

ShortTimeFormatter の Formatter() にだけ 2 つの引数を渡すオーバーロードがありますが、この第 2 引数の TimeFormat 列挙型は DigitsFormat 列挙型への切り替えが推奨されている型であることに注意しましょう。

bool ShowDays

true だったら、 24 時間以上のタイムにおいて、例えば 47:59:10 を 1d 23:59:10 と表示します。

bool ShowPlus

true だったら、0 を含むプラスのタイムに + を付けます。

bool DropDecimals

true だったら、1m 以上のタイムでは小数点以下を表示しません。

bool AutomaticPrecision

true だったら、小数点以下部分において後続の 0 を表示しません。
 例えば Accuracy が Hundredths (0.00) の場合、10.00 は 10 と表示されます。

enum TimeAccuracy

TimeAccuracy.cs

小数点以下の表示桁数です。
 標準的な Livesplit コンポーネントの設定画面などにおいて、多くは 0.00 が最多桁数の設定となっていますが、enum TimeAccuracy では 0.000 まで定義されています。

TimeAccuracy の表示例
TimeAccuracy 表示
Seconts 0
Tenths 0.0
Hundredths 0.00
Milliseconds 0.000

enum DigitsFormat

TimeFormat.cs

時間、分などが 0 の時に 0 で埋めるか非表示にするかのパターンです。

DigitsFormat の表示例
DigitsFormat 表示
SingleDigitSeconds 1
DoubleDigitSeconds 01
SingleDigitMinutes 0:01
DoubleDigitMinutes 00:01
SingleDigitHours 0:00:01
DoubleDigitHours 00:00:01

enum NullFormat

NullFormat.cs

タイムが記録されていない場合など、TimeSpan? が null のときにどのように表示するかのパターンです。

Digits、Accuracy の設定に対応した NullFormat の例
Accuracy Seconds Hundredths Seconds Hundredths
DigitsFormat SingleDigit
Seconds
SingleDigit
Seconds
DoubleDigit
Minutes
DoubleDigit
Hours
入力例 0 0.00 00:00 00:00:00.00
Dash - - - -
ZeroWithAccuracy 0 0.00 0 0.00
ZeroDotZeroZero 0.00 0.00 0.00 0.00
ZeroValue 0 0.00 00:00 00:00:00.00
Dashes - -.-- --:-- --:--:--.--

LiveSplit/LiveSplit.Core/Model/Comparisons

比較対象のタイム関連。

interface IComparisons : IDictionary<string, Time>, ICloneable

IComparisons.cs

任意の比較対象タイムを取得する場合は Time t = IComparisons[ComparisonName];TimeSpan? ts = IComparisons[ComparisonName][TimingMethod]; のようにすれば良いと思います。

ComparisonName は各 ComparisonGenerator クラスの ComparisonName で取得可能、現在 Livesplit で有効になっている Comparison は LivesplitState.Run.Comparisons に登録されています。
 PB の ComparisonName は class Run で定義されているので注意。

ISegment のメンバとして取得可能。

interface IComparisonGenerator

IComparisonGenerator.cs

比較対象タイムを生成するときに利用します。
 ジェネレータはあくまで比較対象の「タイムそのもの」を生成するためのものです。


LiveSplit/LiveSplit.Core/Model/RunSavers

記録の外部出力関係。

public class XMLRunSaver : IRunSaver

XMLRunSaver.cs

lss ファイルへの書き出し。

public class ExcelRunSaver : IRunSaver

ExcelRunSaver.cs

Excelに履歴データを出力する関係。

実際に出力したExcelを見ながらソースを読むと、タイムの取得に関して非常に参考になると思います。
 このクラスの中では Excel に出力する際に TimeSpan.TotalDays で日付換算しており、おそらくは Excel の日付表示の書式に合わせているためだと思います。


LiveSplit/LiveSplit.Core/Model/RunFactories

記録の外部入力関係。

public class StandardFormatsRunFactory : IRunFactory

StandardFormatsRunFactory.cs

lss ファイルの読み取り。


LiveSplit/LiveSplit.View/View

コンポーネント個別のフォームを除く、Livesplit のフォーム。

public partial class TimerForm : Form

TimerForm.cs

Livesplit を起動すると表示される一般的に Livesplit と認識されているフォームで、LivesplitState や TimerModel などはここで実体化しています。
 コンポーネントの Update() はここで呼び出しています。

public partial class LayoutEditorDialog : Form

LayoutEditorDialog.cs

右クリック → Edit Layout で開く Layout Editor。

public partial class LayoutSettingsControl : UserControl

LayoutSettingsControl.cs

右クリック → Edit Layout → Layout で開く Layout Settings。
 最初の Layout タブの内容はここに含まれていますが、それ以外のタブは各コンポーネントに含まれているフォームを表示しています。

public partial class RunEditorDialog : Form

RunEditorDialog.cs

右クリック → Edit Splits で開く Splits Editor。

public partial class SettingsDialog : Form

SettingsDialog.cs

右クリック → Settings で開く Settings。

public partial class ShareRunDialog : Form

ShareRunDialog.cs

右クリック → Share で開く Sharer。
 Twitter に投稿するテキスト中の $title や $splittime などの置き換えはここで行われています。


LiveSplit/UpdateManager

(おそらくは)コンポーネントのバージョンアップ関係。

interface IUpdateable

IUpdateable.cs

IComponentFactory はこれを継承しているため、MyComponentFactory には IUpdateable のメンバを含める必要があります。
 説明文がなく、動作の確認もできていませんが、おそらくはネット経由のコンポーネントのバージョンアップに関係していると思います。
 Version はおそらくコンポーネントのバージョン。

7 件のコメント:

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

    返信削除
  2. 開発資料リンクのその他にて、Livesplit用コンポーネント(公式以外まとめ) へのリンクを追加

    返信削除
  3. 設定画面の初期値についての記述を追加
    一部、文字装飾を変更

    返信削除
  4. 実際にタイムが格納されている場所、比較対象タイム Comparison の生成に関する記述をそれぞれひとつの項目として再編集
    画面の描画についての項目を追加
    設定画面について設定値の保存と読込の説明を追加
    一部、項目を並び替え

    返信削除
  5. TimeFormatter の説明を再構成

    返信削除
  6. 追加
    List Run などの添え字の範囲チェック
    コンポーネントで発生した例外
    コンポーネントの設定画面はフォームではなく UserControl
    設定画面がダイアログの範囲に収まらない場合の処理
    親フォームのイベント
    区間タイム取得メソッドの情報追加
    Comparison の名前
    タイマー操作イベントの sender は LiveSplitState
    ControlComponent
    LogicComponent
    SimpleLabel

    返信削除
  7. dl dt dd タグによる構造から section タグによる構造に変更
    一部、階層構造の見直し
    一部、説明文を追記

    返信削除