空談録

世界で5人くらいに役立ちたい

KancolleViewerのプラグイン作りに再挑戦してみた

プラグイン作るために1年くらい放置したゲームにログインする人間とは

昨日某べやで話を聞いてなんか面白そうと思ったのでもう一度作ってみました
前回は事故だった、いいね?

環境はViewerが4.1.2、Visual Studioは2015 Professionalです
特に拡張はいれてないです。背景に女の子の画像が出てるくらいです

もっとわかりやすい情報がゔぇいさん @veigrのところにあるのでそちらをどうぞ
KanColleViewer プラグインの作り方とか - CAT EARS

f:id:fantasticswallow:20150823135328j:plain

プロジェクトの用意

とりあえず新しいプロジェクトを作りましょう。

UIを追加することを考える場合は"WPF ユーザーコントロールライブラリ"を作成します
(Visual C#Windows → クラシック デスクトップ → WPF ~~)
(Visual BasicWindowsWindows デスクトップ → WPF ~~)
UIの追加予定がない場合は普通のクラスライブラリで問題ないと思います
特に決まってなければ上のを選びましょう

プロジェクトが出来上がったらNuGetから必要なパッケージを落としてきます
必須なのは
・KancolleViewer.Composition
で、VS2015の場合は
・KancolleViewer.PluginAnalyzer
を入れるとはかどります(VS2013以下の場合PluginAnalyzerは使えません)

後は必要に応じて入れる形です
今回は母港のアイテムの情報がほしかったのでKancolleWrapperも使いました

NuGetから落とすのはツール→"NuGet パッケージ マネージャ"のところからお好きな方法で
コンソールから入れる場合はこんな感じです

Install-Package KancolleViewer.Composition
Install-Package KancolleViewer.PluginAnalyzer
Install-Package KancolleWrapper

一行ずつ実行してください。PM > って出てるときにやってけばいいです

f:id:fantasticswallow:20150823135319p:plain

画像みたいな感じでいろいろ入ります。便利ですね

さて、この後PluginAnalyzerを入れてる場合、すぐにビルドが通らなくなります
この辺はAnalyzerとかでググったりすればわかります

ということでAnalyzerに導かれるままにIPluginを実装したクラスを用意します
2015であればこんな感じのコードでいいです

class Plugin : IPlugin
{
}

こうすると画像のような形でコードを書き換えようぜ!って言われるのでとりあえず全部追加してください
あ、IPluginの解決はusingだけでいいです

f:id:fantasticswallow:20150823140059p:plain

ということで追加していった結果がこちらです

f:id:fantasticswallow:20150823140220p:plain

ここまでで書いたコードは : IPlugin の文字だけです。最近のVisual Studioはすごいですね
ちなみに2013以前の場合はここまで自力で用意する必要があります(usingとinterfaceの実装はできますがExportは実装できません)

とりあえずIPluginが実装し終わればこれでプラグインとしての準備は完了です。後は自分で作るだけですね
せっかくなのでExcelに卵焼きを投げるまでの部分も書いていきます

卵焼きの準備

え?卵焼きって何!?って?
特に決めてませんが今回はこんな感じのを送りつけましょう

f:id:fantasticswallow:20150823140747p:plain

MaterialsってところにあるようなのですがKancolleWrapperのどれがどれなのかいまいちわかりません
ということで公式のプラグインのCounterの実装をガン見して書いてみるとこんな感じです

using Grabacr07.KanColleWrapper;
using Grabacr07.KanColleWrapper.Models.Raw;
using System.Reactive.Linq;

// ~~~~~~~~~~~~~~

public void Initialize()
{
    KanColleClient.Current.Proxy.api_port.TryParse<kcsapi_port>().Select(x => x.Data.api_material).Subscribe(x => materialSubscribe(x));
}

private void materialSubscribe(kcsapi_material[] source)
{
    if (source != null && 8 <= source.Length)
    {
        Fuel = source[0].api_value;
        Ammunition = source[1].api_value;
        Steel = source[2].api_value;
        Bauxite = source[3].api_value;
        // DevelopmentMaterials = source[6].api_value;
        Bucket = source[5].api_value;
        // InstantBuildMaterials = source[4].api_value;
        // ImprovementMaterials = source[7].api_value;
    }
}

internal int Fuel { get; set; }
// ...

下の実装もKancolleWrapperの実装をそのまま持ってきました。もうだめだ
こんな数字の対応覚えてるわけがなく…

データ取得のタイミングはapi_portにしてます
ぼこーのところです、はい

System.Reactive.Linqがない場合Subscribeの引数にラムダ式を入れられないような気がします
Subscribeしてるので通信があれば呼ばれます。いい世界だ

ということで卵焼きが手に入ったのでExcelに投げましょう


追記(8/24):KanColleClient.Current.Homeport.Materialsで取れるとのご指摘があったのですが、使い方が分からなかったという悲しみです
KanColleClient.Current.Homeport.Materials.PropertyChanged += hogehoge; とかやってたんですがNullReference吐かれたのと、そもそもデバッグ方法を知らないという

start2のタイミングで作られるということなのでコード書いてみるとこんな感じでしょうか?

public void Initialize()
{
    // ~~~~
    KanColleClient.Current.PropertyChanged += KanColleClient_PropertyChanged;
}

private void KanColleClient_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (!isStarted && e.PropertyName == "IsStarted")
    {
        if (KanColleClient.Current.IsStarted)
        {
            isStarted = true;
            // KanColleClient.Current.Homeport.Materials.PropertyChanged += hogemoge;
        }
    }
}
private bool isStarted = false;

試してませんがたぶんこれで落ちない気もします
まあこの後が大変でしょうが…
(最初はプロパティ名で更新してTimerで5秒待機させたあとに更新するみたいな処理書いてた)
一つだけのプロパティを見るならまだ行けそうですが複数のデータをまとめたい とか思うとつらそうです
(更新順序が守られるという前提の上で、特定のプロパティの更新を拾ったらすべて変更されたと見なすのもありかとおもいますが、実装を理解する必要がありますし、実装が変わらないという前提も必要なので微妙)

Excelにデータを投げつける

今回のプラグイン作成の75%がExcel関係になっています。訳が分からないよ

4割くらい関係ないので話を飛ばします
そのために「すでに書き込むためのxlsxファイルが用意してある」という前提でやります

ということで世界のどこかにtamagoyaki.xlsxを用意します
このtamagoyaki.xlsxはシートが一つあるだけのまっさらな奴ですね

まずはMicrosoft.Office.Interop.Excelを参照に追加します
Excel使いの皆さまなら余裕でしょう

次にExcelのアプリケーションのインスタンスを生成します
しないと話になりません

こんな感じのコードでいきます

private static readonly string workbookPath = "hogehogehoge\tamagoyaki.xlsx";

internal static void Initialize()
{
    app = new Application();

    wb = app.Workbooks.Open(workbookPath);
    sh = (Worksheet)wb.Sheets.Item[1];

    if (sh.ListObjects.Count == 0)
    {
        var rg = sh.Range["A1", "F1"];
        ((Range)rg.Item[1, 1]).Value = "時間";
        ((Range)rg.Item[1, 2]).Value = "燃料";
        ((Range)rg.Item[1, 3]).Value = "弾薬";
        ((Range)rg.Item[1, 4]).Value = "鉄鋼";
        ((Range)rg.Item[1, 5]).Value = "ボーキ";
        ((Range)rg.Item[1, 6]).Value = "バケツ";
        ls = sh.ListObjects.Add(XlListObjectSourceType.xlSrcRange, rg, XlListObjectHasHeaders: XlYesNoGuess.xlYes);
    }
    else
    {
        ls = sh.ListObjects[1];
    }
}

private static Application app;
private static Workbook wb;
private static Worksheet sh;
private static ListObject ls;

app にApplicationのコンストラクタで入れます
interfaceにしか見えないのですがまあ詳しいことはよくわかりません
ちなみにApplicationClassにした場合相互運用がうんたら言われます

あとはパスからWorkbookを読み込んでシートを拾ってきてListObjectを用意するだけです
なんでListObject?って思うかもしれませんが、ListObjectだと「n行目の位置を保持して、追加したn += 1する」みたいな、どこまで書いたかを保持する必要がなくなるからです
毎回起動時にデータがどこまであるか調べるのはあれですし、どこかのセルに位置を書き込んだりするとユーザーの変更に対応できません
そういった意味では「次はn行目に書く」ということをまったく意識しないで追加できる形がベストなわけです

ちなみに上のコード何かが間違ってるのでヘッダーが別にされます。よくわかりません

次に卵焼きを受け取る部分を書いていくとこんな感じです

internal static void AddData(int fu, int am, int st, int bx, int bc)
{
    var row = ls.ListRows.AddEx();
    var rg = row.Range;
    ((Range)rg.Item[1, 1]).Value = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss");
    ((Range)rg.Item[1, 2]).Value = fu;
    ((Range)rg.Item[1, 3]).Value = am;
    ((Range)rg.Item[1, 4]).Value = st;
    ((Range)rg.Item[1, 5]).Value = bx;
    ((Range)rg.Item[1, 6]).Value = bc;
}

ListObject.ListRows.AddExでListObjectの末尾に行が一つ増えます
あとはその行のRangeを拾ってデータを押し込むだけです

ということでこれで完成!ってしたいのですがこのままだとExcelのプロセスが死にません。ファイルごと抱えて生き伸びてしまうので最高に害悪です
しかし解決方法が謎だったのでググってるとこんな記事が

Excelを解放したい(2) Workbook().CloseとApplication.Quitと(2) : 趣味のプログラムあれこれ

なるほどWorkbook.CloseとApplication.Quitを使えと

ということで追加するとこんな感じ

internal static void Quit()
{
    wb.Save();
    ls = null;
    sh = null;
    wb.Close(false);
    wb = null;
    app.Quit();
    app = null;
}

全部nullにしなくてよいと思いますが検証したくなかったのでこんな感じです
これをPluginが死ぬタイミングで呼ぶことにしましょう

で、そういうイベントは?って言うと見当たらないわけですね
とりあえずNotifierっていうこれまた公式のプラグインのソースを見るとIDisposableを実装しています
ということはIDisposableを実装しとくとDisposeが呼ばれそうなので実装しときます
で、追加されたDispose内でさっきのQuitを呼ぶことでプラグインがDisposeされるときにExcelのプロセスも死ぬようになります

追記(8/24):こっちについても@veigrさんのほうから説明されました


ということでKancolleViewerがどうこうというよりMEFで拡張を作成した場合は、IDisposableを実装することで後処理が行えるようになるようです

Managed Extensibility Framework - Documentation に詳しく書いてあるようです
"Disposing the container"のところが目的の項目ですかね

Disposing the container

A container instance is generally the lifetime holder of parts. Part instances created by the container have their lifetime conditioned to the container’s lifetime. The way to signal the end of the container lifetime is by disposing it. The implications of disposing a container are:
・Parts that implement IDisposable will have the Dispose method called
・Reference to parts held on the container will be cleaned up
・Shared parts will be disposed and cleaned up
・Lazy exports won’t work after the container is disposed
・Operations might throw System.ObjectDisposedException

箇条書きの一つ目が「Parts(今回であればプラグイン)がIDisposableを実装してれば、Disposeが呼ばれる」みたいに書いてるので、何か後処理がしたい場合はIDisposableを実装しときましょう
いままで一度もDispose内で保存処理をしたことはないですがプラグイン側で保存したいときもDisposeでやればいいと思います?



なんか分かりにくい記事になってしまった
死因はおそらくExcelにデータを投げた点だと思われます

でも今更頑張りたくもないという
まあ誰も見ないし問題ないでしょう

この辺で