コンポーネント開発に興味を持った方が開発に必要な情報を調べようとした時に少しでも助けになればと思い、私が調べた情報などを紹介するためにこの記事を書きました。
この記事はコンポーネント開発に必要な全ての情報を記載している訳ではありませんが、公式のソースコードを読み解くための参考資料として活用していただけると幸いです。
この記事の中で My~ と書いている部分は開発するコンポーネント名と考えてください。
もくじ
フォルダごとの紹介・説明
LiveSplit.Core/UI/Components
LiveSplit.Core/UI
LiveSplit.Core/Model
LiveSplit.Core/TimeFormatters
LiveSplit.Core/Model/Comparisons
LiveSplit.Core/Model/RunSavers
LiveSplit.Core/Model/RunFactories
LiveSplit.Core/Options
LiveSplit.View/View
UpdateManager
開発資料リンク
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 つの式だけの場合、=> 記号を使って次のように書くことができます。
1.
2.
3.
public string ComponentName => "My Component Name";
public ComponentCategory Category => ComponentCategory.Other;
public IComponent Create(LiveSplitState state) => new MyComponent(state);
この場合、ラムダ式の => とは異なる => なので注意が必要です。(そして記号で検索するとラムダ式がヒットするという罠)
この書き方はメソッドだけでなくプロパティなどでも使用でき、プロパティで使用した場合は get-only プロパティになります。
メソッドチェーン
クラス.メソッド().メソッド()
同じクラスに対して連続して複数回メソッドを実行したい場合に、クラスのインスタンスに続いてメソッド呼び出しを . 記号でつなげて記述することができます。
メソッドチェーンを利用する場合、メソッドの戻り値が自分自身のクラスである必要があります。
拡張メソッド
static 戻り値の型 メソッド名(this 第1引数) { 処理内容 }
本来、オブジェクトを引数に持つ static なメソッドは クラス名.メソッド名(引数のオブジェクト);
という構文で呼び出します。
拡張メソッドは、この static なメソッドの呼び出しを インスタンスオブジェクト.メソッド名();
という構文で呼び出すことができます。
拡張メソッドの利点は、
- メソッドチェーンとの相性が良い
- 既存のクラスに外部からメソッドを追加できる(メソッド定義は外部の別の場所にあるので、あくまで利用側から見たらそう見えるという話)
- インターフェースにメソッドを追加できる(これもメソッド定義自体は別の場所にあるので、あくまで利用側からの見たらそう見えるという話)
逐語的文字列リテラル
@"文字列"
文字列リテラルの最初に@を付けることで逐語的文字列リテラルとなり、"\t" や "\r\n" などはタブや改行ではなくそのままの文字列 "\t"、"\r\n" として扱われるようになります。
そのため、逐語的文字列リテラルの中で改行を行うと、その位置で改行された文字列として扱われます。
List<ISegment> Run などの添え字の範囲チェック
if ((uint)index < (uint)Run.Count) { }
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 のためだけにメッセージ出力用の記述を消す必要もありません。
参考
コンポーネント開発の始めに
コンポーネントを構成する基本的なファイル
次のファイルはコンポーネント開発でほとんどの場合必要となる、基本的なファイルです。
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 チャンネルに掲載されていたコンポーネントの作り方のガイドは、コンポーネント開発の最初の一歩を踏み出す際に有用なガイドだと思うので、ここで紹介します。
簡単に和訳すると次のような内容です。
コンポーネントの作り方
- 新しいプロジェクトの作成を選び、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))]
プロジェクトのプロパティ設定
プロジェクトのプロパティで次の設定をしておくと、開発作業の一部が楽になると思います。
アプリケーション
- アセンブリ名:
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 クラスがあり、これを再描画の判定に使用することができます。
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 コンポーネントの設定画面などがこのパターンです。
ユーザーコントロール側で対処
ユーザーコントロール全体がスクロールすると使い勝手が悪いような場合は、ユーザーコントロール側で部分的にスクロールする部分を設けるなどしてサイズオーバーしないように対策します。
例えば PaceAlert や
Text
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
CurrentTime を取得するソースコードの行が一行違えば一行分の時間差が生じるくらいに「現在のタイム」なので注意。
例えば、タイマーが動いている状態で同じメソッド内で複数回 CurrentTime を取得すると全て別々の値になります。
現在の試行の区間[i] での Split タイム
LivesplitState.Run[i].SplitTime
まだラップを取ってない区間では null。
現在の試行の計測中の区間での Segment タイム
LiveSplitStateHelper.GetLiveSegmentTime(state, splitNumber, method)
現在計測中のタイムの内、まさに現在計測中の区間のタイムを取得するのが主な用途かと思います。
splitNumber の値 | 戻る値 |
---|---|
< 現在区間 | splitNumber で指定した区間から現在計測中のタイムまでの区間タイムの合計 |
== 現在区間 | 現在計測中の区間のタイム |
> 現在区間 | 現在計測中の区間のタイム |
state.Run[] の範囲外 | 例外発生 |
CurrentPhase が NotRunning の場合は TimeSpan.Zero が戻る。
現在の試行のラップ済みの区間での Segment タイム
LiveSplitStateHelper.GetPreviousSegmentTime(state, splitNumber, method)
現在計測中のタイムの内、ラップ済みの区間タイムを取得するのが主な用途かと思います。
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 をコンポーネントで作る方法を説明します。
タイムを生成する ComparisonGenerator
ComparisonGenerator で Comparison を生成する手順
- IComparisonGenerator を実装した MyComparisonGenerator クラスを作る。
- MyComponent クラスの中で MyComparisonGenerator を実体化する。
- LivesplitState.Run.ComparisonGenerators.Add() で Livesplit に登録する。
- Livesplit から MyComparisonGenerator.Generate() が呼び出される。
- .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
コンポーネント開発における基本的なインターフェース。
基本的に実装必須 MyComponent : IComponent
ソースコードに説明コメントあり。
必ず IComponent を実装する必要がある訳ではなく、例えば、タイマーレイアウト上に表示する必要がない場合は LogicComponent、タイマーレイアウト上にコントロール類を表示したい場合は ControlComponent を実装します。
int GetSettingsHashCode()
任意で実装するオプション的なメソッド。
何らかのハッシュ値を返し、その戻り値に変化があれば本体の再描画が実行されます。
既存のコンポーネントでは MySettings.GetSettingsHashCode()
を呼んでその戻り値をそのまま返す実装がされています。
Timer コンポーネントの例
このような実装をすることで、コンポーネントの設定項目に変化があればコンポーネントを再描画してくれます。
XMLLayoutSaver クラスで MyComponent.GetSettingsHashCode()
の値を使ってハッシュ値を求め、TimerForm クラスでハッシュ値を監視して変化があれば再描画します。
interface IComponentFactory : IUpdateable
実装必須 MyComponentFactory : IComponentFactory
ソースコードに説明コメントあり。
Livesplit 本体が MyComponent を利用する際に、MyComponentFactory が最初の入り口となります。
Layout Editor で表示するコンポーネント名やコンポーネントのカテゴリ(Timer、List、Infomation などの分類)などはこのクラスで設定します。
また、IComponentFactory は IUpdateable を継承しているので、MyComponentFactory は IUpdateable も実装する必要があります。
IUpdateable は Livesplit 起動時に行われるコンポーネントの更新チェックで利用されます。
enum ComponentCategory
LayoutEditor で追加する際の Timer、List、Infomation などの分類。
class InfoTextComponent : IComponent
InformationName に表示するテキストのタイトルを、InformationName に表示するテキストの内容を代入して、再描画が必要な時は Update() を呼び出します。
InformationName と InformationName は SimpleLabel クラスなので、SimpleLabel クラスも把握しておくと意図しない挙動に対処しやすくなると思います。
class InfoTimeComponent : InfoTextComponent
InformationName に表示するタイムのタイトルを、TimeValue に表示するタイムを、Formatter に TimeFormatter を代入して、再描画が必要な時は Update() を呼び出します。
内部的には Formatter が TimeValue を string 型に変換して InformationValue に代入する機能が追加された InfoTextComponent。
タイム表示に使用されている SimpleLabel ValueLabel は IsMonospaced == true なので、文字が等幅に描画されます。
InfoTimeComponent 自体は TimeSpan? 型のメンバをひとつしか持っていないので、PreviousSegment のようにひとつのタイム表示コンポーネントの中で複数のタイムを表示する場合は、表示する文字列を格納しているメンバに直接追加する必要がありそうです。(PreviousSegment コンポーネントのソースがそのように書かれていますが、他に方法がないのかは未確認)
abstract class LogicComponent : IComponent
タイマーレイアウト上には何も表示しないコンポーネントを作る場合に、この抽象クラスを実装すると楽です。
代表例は標準の Sound コンポーネント。
タイマーレイアウト上に表示するために必要なプロパティやメソッドに関連したコードを書かなくて済みます。
abstract class ControlComponent : IDeactivatableComponent
タイマーレイアウト上に Windows フォームのコントロールを表示したい場合に、この抽象クラスを実装します。
代表例は標準の Video コンポーネント。
Livesplit のサイズからコントロールのサイズを計算してくれるメソッドが用意されているので、明確な意図がないならそのメソッドにお任せで OK。
LiveSplit/LiveSplit.Core/UI
タイマーレイアウトへの表示や設定画面に関連。
class SimpleLabel
タイマーレイアウト上に文字を表示する際の1つ1つのセルに相当します。
例えば、Text コンポーネントであれば左のテキストと右のテキストのそれぞれが SimpleLabel で、Splits コンポーネントであればそれぞれの区間が行で区間名やそれぞれのタイム部分が列でその行列を構成している1つ1つのセルが SimpleLabel です。
コンポーネントで InternalComponent というオブジェクトがあったら、最終的に中身は大体これの印象。
string Text
ここに描画させたいテキストを代入します。
基本的にはここに代入したテキストが描画されますが、Width の幅に収まらないテキスト量でかつ AlternateText のメンバの中に幅に収まるテキスト量のものがあれば、AlternateText が優先されます。
描画幅に対してテキストが多くてはみ出すような場合、後ろのテキストが省略されて末尾に "..." と表示されます。
ICollection AlternateText
Text プロパティのテキストが Width の幅に収まらない場合に使用される代替テキストのコレクションです。
Text プロパティに代入したテキストが描画領域の幅に収まる場合は Text プロパティのテキストが描画されます。
Text が収まらない場合で AlternateText がメンバを持っている場合は、AlternateText のメンバの中で描画領域に収まる最も長いテキストが描画され、AlternateText の中にも収まるテキストがない場合は Text のテキストが描画されます。
bool IsMonospaced
等幅描画モードのフラグです。
true だとテキストを 1 文字単位に分解して等間隔に描画して、疑似的に等幅フォントのように表示できます。
float ActualWidth
テキストを実際に描画した際の幅の値です。
(内部的には Graphics.MeasureString() や TextRenderer.MeasureText() を使用しており、厳密には正確な大きさと一致しません)
描画範囲からテキストがはみ出て "..." の省略表示になった場合、この値には反映されません。
void Draw(Graphics g)
テキストを描画する際に呼び出します。
enum GradientType
色の単色、縦グラデーション、横グラデーション。
class GraphicsCache
再描画の判定に使用します。
詳細はタイマー画面への描画を参照。
class SettingsHelper
設定の保存、読み込みで利用します。
詳細は設定項目の値を参照。
他に、フォント設定ダイアログや色取得ダイアログに関するメソッドもここで定義されています。
LiveSplit/LiveSplit.Core/Model
Livesplit内部情報関連。
class LiveSplitState : ICloneable
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
主に 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
この 2 つに関しては asl の紹介記事の中で説明しているので、そちらもご覧ください。
class TimerModel : ITimerModel
このクラスを実体化してメソッドを呼び出すことでタイマーのスタートやリセットなどのタイマー操作を行うことができます。
また、タイマー操作で発生するイベントをキャッチすることもできます。
asl ファイルではなく dll ファイルとして Autosplitter 機能を実現する場合はこれを利用します。(asl ファイルでも TimerModel を利用しているものがあります)
class TimeStamp
タイマーの精度に関係するタイムスタンプ。
struct AtomicDateTime
Attempt のメンバである計測開始日時と終了日時は AtomicDateTime? 型で管理されています。
日時は AtomicDateTime.Time から取得できますが、この Time メンバは Time 型ではなく DateTime 型であることに注意。
LiveSplit/LiveSplit.Core/TimeFormatters
タイムを文字にして表示するための書式関連。
interface ITimeFormatter
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 の時に変換する文字などのプロパティを設定します。
プロパティ \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 と同じ
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
小数点以下の表示桁数です。
標準的な Livesplit コンポーネントの設定画面などにおいて、多くは 0.00 が最多桁数の設定となっていますが、enum TimeAccuracy では 0.000 まで定義されています。
TimeAccuracy | 表示 |
---|---|
Seconts | 0 |
Tenths | 0.0 |
Hundredths | 0.00 |
Milliseconds | 0.000 |
enum DigitsFormat
時間、分などが 0 の時に 0 で埋めるか非表示にするかのパターンです。
DigitsFormat | 表示 |
---|---|
SingleDigitSeconds | 1 |
DoubleDigitSeconds | 01 |
SingleDigitMinutes | 0:01 |
DoubleDigitMinutes | 00:01 |
SingleDigitHours | 0:00:01 |
DoubleDigitHours | 00:00:01 |
enum NullFormat
タイムが記録されていない場合など、TimeSpan? が null のときにどのように表示するかのパターンです。
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
任意の比較対象タイムを取得する場合は Time t = IComparisons[ComparisonName];
や
TimeSpan? ts = IComparisons[ComparisonName][TimingMethod];
のようにすれば良いと思います。
ComparisonName は各 ComparisonGenerator クラスの ComparisonName で取得可能、現在 Livesplit で有効になっている Comparison は LivesplitState.Run.Comparisons に登録されています。
PB の ComparisonName は class Run で定義されているので注意。
ISegment のメンバとして取得可能。
interface IComparisonGenerator
比較対象タイムを生成するときに利用します。
ジェネレータはあくまで比較対象の「タイムそのもの」を生成するためのものです。
LiveSplit/LiveSplit.Core/Model/RunSavers
記録の外部出力関係。
public class XMLRunSaver : IRunSaver
lss ファイルへの書き出し。
public class ExcelRunSaver : IRunSaver
Excelに履歴データを出力する関係。
実際に出力したExcelを見ながらソースを読むと、タイムの取得に関して非常に参考になると思います。
このクラスの中では Excel に出力する際に TimeSpan.TotalDays で日付換算しており、おそらくは Excel の日付表示の書式に合わせているためだと思います。
LiveSplit/LiveSplit.Core/Model/RunFactories
記録の外部入力関係。
public class StandardFormatsRunFactory : IRunFactory
lss ファイルの読み取り。
LiveSplit.Core/Options
本体設定に関連するクラスやログ出力など。
public enum BackgroundType
背景色のタイプ。単色、垂直グラデーション、水平グラデーション、画像。
public static class Log
Trace クラスを使ってログ情報を出力します。
類似の機能を提供する Debug クラスはデバッグビルド用であるのに対して、Trace クラスはデバッグビルドでもリリースビルドでも使うことができます。
出力されたログ情報を確認するには次のようなツールが便利です。
- Autosplitter 開発でもよく利用する DebugView
- Windows 付属のイベントビューア
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 などの置き換えはここで行われています。
LiveSplit/UpdateManager
(おそらくは)コンポーネントのバージョンアップ関係。
interface IUpdateable
IComponentFactory はこれを継承しているため、MyComponentFactory には IUpdateable のメンバを含める必要があります。
説明文がなく、動作の確認もできていませんが、おそらくはネット経由のコンポーネントのバージョンアップに関係していると思います。
Version はおそらくコンポーネントのバージョン。
Speedrun Tool Development サーバの #tutorials チャンネルが整備されたことに合わせて、記述内容を一部変更
返信削除開発資料リンクのその他にて、Livesplit用コンポーネント(公式以外まとめ) へのリンクを追加
返信削除設定画面の初期値についての記述を追加
返信削除一部、文字装飾を変更
実際にタイムが格納されている場所、比較対象タイム Comparison の生成に関する記述をそれぞれひとつの項目として再編集
返信削除画面の描画についての項目を追加
設定画面について設定値の保存と読込の説明を追加
一部、項目を並び替え
TimeFormatter の説明を再構成
返信削除追加
返信削除List Run などの添え字の範囲チェック
コンポーネントで発生した例外
コンポーネントの設定画面はフォームではなく UserControl
設定画面がダイアログの範囲に収まらない場合の処理
親フォームのイベント
区間タイム取得メソッドの情報追加
Comparison の名前
タイマー操作イベントの sender は LiveSplitState
ControlComponent
LogicComponent
SimpleLabel
dl dt dd タグによる構造から section タグによる構造に変更
返信削除一部、階層構造の見直し
一部、説明文を追記
本家のフォルダ構造変更に対応
返信削除新規追加
返信削除拡張メソッド
LiveSplit.Core/Options
動作確認時のチェックポイント
既存の説明に追加・加筆
LivesplitState.CurrentTime
SettingsHelper.cs
IComponent.GetSettingsHashCode()
SimpleLabel の説明を加筆
返信削除