fc2ブログ

艦これ:独自編成IDをつくろう

2015/7/17のメンテで敵の編成IDが消えちゃいました。Σ(゚д゚lll)ガーン




先読みはまだしも、編成IDないとデータベースを作る際に結構困ったりします。例えば、

・敵の編成を記録しておいて、ドロップとの関係を調べる
 例:6-1の空母棲姫マスみたいに特定の編成でないと401がドロップしない
・敵の編成を記録して、Wiki方式に編成一覧を出力する
・敵の編成IDを表示して、どの敵と戦っているか一目瞭然にする
・編成IDを記録して、戦闘履歴を記録する……

先読み厨がいっぱいいますが、編成IDは解析面からでも常識的に考えて結構使います。さすがに羅針盤の段階で表示しないようにするだけで、戦闘中になったらいつもどおりIDが返ってくるのかと思ったら、それすらなくて「そのたった十数字節約するとこなの!? ネジでボロ儲けしてるんだからケチケチせずに増設しなさい」ってマジギレしてました。今でも許してません。冗長性って大事だよね。

編成IDがないの?じゃあ作ってしまえばいいじゃないというわけで、苦し紛れに編成IDを作ってみます。これはもちろん、サーバー側に用意されている(7/17のメンテ前までは普通に取得できた)固有の値ではなくて、ローカルに勝手に作る識別子のようなものです。運営が用意した編成IDも結構行き当たりばったりにつけられていて、1-1から1番を割り当てて5-4まで行くものの、1-5行ったら5-4の続きで500番台が出てくるし、イベントマップに行ったら急に2000番代になるし……。お世辞にもいいIDとはいえません。

そこで今回作る識別子、これをローカルIDと呼ぶことにします。仕様は3分ぐらいで思いついて、実装しながら調整したやつなんで相当ガバガバかと思いますが一案として。まずこのローカルIDは次の要件を満たさなければいけません。

ローカルIDが満たすべき要件
・ローカルIDはできれば数値でほしい
・海域番号、マップ番号、(セル番号)、敵のマスターIDの配列(api_ship_ke)、陣形 が与えられれば、ローカルIDは一意に決まらなければならない
・ローカルIDから上記の5要素を復元できなくてもよい(不可逆変換でもいい)
・既存のこれらのデータから、ローカルIDを計算して、ローカルIDによる運用にスライドできるようにしなければならない

※最初にちょっろっと公開したときはGetHashCode()で実装してしまいましたが、GetHashCode()の値に一意性の保証がないため、MD5でちゃんとハッシュを取るように変更しました。

ぱっと思いつく解決法が、ハッシュ値を取る方法。具体的にはファイルの検証等で使われるMD5の値を取ります。ローカルID→海域…陣形データは復元できないものの、一意の変換はオッケーです。ハッシュ値は衝突することがありますが、普通の32ビットintの間で取る限り、衝突する確率はめちゃくちゃ低いです。

MD5を計算するためにはまずバイナリに変換してあげなければいけません。インスタンスをそのままシリアル化してバイナリにぽいーでもいいのですが、バイナリを文字列に変換するとまずバイナリがぱっと見て何かわからないのでバグが出たときに面倒になりそう。

そこでおすすめなのは、艦これのゲームと同じく、このクラスのオブジェクトをJSONでシリアル化して、そのJSON文字列のハッシュ値を取る方法。JSONにしてしまえばぱっと見なにかわかるし、クラスの変数名や算出方法を統一すれば誰がやっても同じ値が取れます。

例えばこのような実装をしてみます。
using System.Security.Cryptography;
using System.Numerics;

///
/// ローカルな編成IDを計算するためのクラス
///

public class UserEnemyID
{
///
/// 海域番号(ex.5-4-1の5)
///

public int api_maparea_id { get; set; }
///
/// マップ番号(ex.5-4-1の4)
///

public int api_mapinfo_no { get; set; }
///
/// セル番号(ex.5-4-1の1)
///

public int api_cell_id { get; set; }
///
/// 敵編成の型。戦闘APIのapi_ship_ke
///

public List api_ship_ke { get; set; }
///
/// 敵の陣形。api_formationから敵の部分だけ抽出
///

public int api_formation_enemy { get; set; }

///
/// ハッシュコードをフルで返します。シリアル化してから比較するため値比較されます。
///

/// ハッシュコード(フル)
public int MakeLongHashCode()
{
//JSONでシリアル化 手動実装(キリッ)
StringBuilder sb = new StringBuilder();
sb.Append("{\"api_maparea_id\":");
sb.Append(api_maparea_id);
sb.Append(",\"api_mapinfo_no\":");
sb.Append(api_mapinfo_no);
sb.Append(",\"api_cell_id\":");
sb.Append(api_cell_id);
sb.Append(",\"api_ship_ke\":[");
sb.Append(string.Join(",", api_ship_ke));
sb.AppendFormat("],\"api_formation_enemy\":");
sb.Append(api_formation_enemy);
sb.Append("}");

string json = sb.ToString();
//UTF8でバイナリに変換
byte[] binary = Encoding.UTF8.GetBytes(json);
//MD5の計算
MD5 cropto = new MD5CryptoServiceProvider();
byte[] hashByte = cropto.ComputeHash(binary);
//MD5を数値に変換
BigInteger hashValue = new BigInteger(hashByte);

//MD5をintに収まるようにMODを取る
int longHashCode = (int)(hashValue % int.MaxValue);

Console.WriteLine("JSON:{0}", json);
Console.WriteLine("MD5:{0}", hashValue);
Console.WriteLine("LongHashCode:{0}", longHashCode);

return longHashCode;
}

///
/// 海域番号+マップ番号+ハッシュコードの数値を返します。シリアル化してから比較するため値比較されます。
///

/// ハッシュコード(フル)
/// ハッシュコード(短縮版)
public int MakeShortHashCode(int longhash)
{
int shorthash = Math.Abs(longhash % 1000);//LongHashの下3桁(マイナスなら絶対値で反転)
shorthash += api_mapinfo_no * 1000;
shorthash += api_maparea_id * 10000;

Console.WriteLine("ShortHashCode:{0}", shorthash);

return shorthash;
}
}


ここでMakeLongHashCode()とMakeShortHashCode(int longhash)の2つが出てきますが、MakeLongHashCode()がローカルIDにあたるもので、内部のデータ処理はこの値を擬似的な編成IDとして処理をします。LongHashCodeは32ビットintの値の範囲内(-2,147,483,648 ~ 2,147,483,647)の数値が頻繁に出るので表示用には適さないので、表示用に変換したMakeShortHashCode()というのもあわせて実装します

解説

LongHashCodeの部分(MD5を取る部分)
JSON文字列を作る。シリアル化のライブラリ使ってないけど、この程度のJSONだったらStringBuilderで手動でやったほうが早いし面倒じゃない気がする。DynamicJSONで、手動でシリアル化した結果と、ライブラリつかってシリアル化した結果が一致するのを確認。
JSON文字列→バイナリ。エンコードは何でもいいけどとりあえずUTF-8で。
バイナリ→MD5。ここらへんはテンプレどおり。MD5は通常16進数の羅列で表記することが多いけど、結局は128ビット変数なんでBigIntegerで一時的に受け止める。
MD5(128ビット)→int(32ビット)。今まで編成IDはintで定義してたので、ローカルIDもintに切り落とす必要がある(ぶっちゃけ16ビットでも十分すぎるぐらい)。これはint.MaxValueでBigIntegerの剰余を取ることで、範囲を切り詰めることができる。


LongHashCodeからShortHashCodeへの変換定義は次のとおりです。

LongHashCodeからShortHashCodeへの変換定義
・LongHashCodeが与えられているものとする
・ShortHashCodeは5桁~6桁(程度)の数字とする(何桁でもいいけど見づらくならずに、同一マップで編成IDがダブらない程度に)
・以下のように変換する
 ShortHashCode = AABCCC
 ※A:海域番号(5-4だったら5) 海域番号が1桁の場合Aは1桁
 ※B:マップ番号(5-4だったら4)
 ※CCC:LongHashCode下3桁
 (longHashCode % 1000(注) の絶対値 )

(注)C#では、マイナスの数字の剰余を求めると答えがマイナスになるので絶対値処理をかぶせました(-2mod5 = -2になる)。ハッシュ関数自体が一様な変換なので、mod5が2だろうが3だろうがあんまり関係がないはず。

例1:5-4のセル1でapi_ship_keが[0,1,2,3,4,5]、陣形が1の場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
ShortHashCodeのほうが見やすいことがわかったでしょうか?

例2:例1と全く同じ値の別インスタンスを作成した場合(そのままインスタンス.GetHashCode()では異なる例)
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
インスタンスが(参照が)いくら変わろうが、値が同じならHashCodeも同じという点が確認できます。実質的な値比較です。

例3:例1のセル番号を1→2に変えた場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":2,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-22229292957627216802792691185990212046
LongHashCode:-1936480517
ShortHashCode:54517
当然、値が変わればHashCodeも変わります

一意性を保証しつつ、値のみに着目したローカルIDを作ることが出来ました!内部処理用のローカルID(LongHashCode)と、表示用のローカルID(ShortHashCode)をわけることで、表示用のローカルIDからはどのマップが特定でき(下3桁の数字は飛び飛びになりますが)、なおかつ内部処理用のローカルIDは十分な桁数を確保することで衝突を出来る限りおこらなくできます。

なので、ほっぽで今後、以前編成IDのあった場所に変な5,6桁の数字が出てたら、こしあんさんが勝手に作った表示用のローカルID(ShortHashCode)だと思ってください(これを伝えたかった)

このローカルIDの計算方法は、後ほど、DataLibraryをコミットしたらgithubで公開する予定です。→公開しました。リンクはこ↑こ↓
https://github.com/koshian2/HoppoAlpha.DataLibrary/blob/master/HoppoAlpha.DataLibrary/DataObject/UserEnemyID.cs
スポンサーサイト



5/18のアプデ以降に出撃中のキャラ数・装備数を取得する方法

完全に専ブラ関係者向けの記事です。

5/18のメンテ以降api_get_member/ship2が廃止され、かわりにapi_get_member/ship_deckが新説されました。ship_deckのAPIの構造自体は、api_ship_dataは母港API・api_shipの配列で、api_deck_dataが母港API・api_deck_portの配列というあんまship2と変わらない構造なのですが、大きな違いが1つあります。それは、ship2では所持している全艦のデータを送ってきたのに対して、ship_deckでは出撃している艦のデータのみ送ってくるようになりました。にもかかわらず運営頭おかしいので、ドロップした艦のデータをship_deckで送ってこないのです。したがって、ship_deckのみを追っていると、ドロップしても保有キャラ数の変動は捉えられても、保有装備数の変動は捉えられないという現象が起きます。これを解決する方法をまとめました。

おおまかなロジックは以下のとおりです。

(1) 初期装備とキャラの対応をデータベースとして記録しておく(この場合はapi_start2のapi_mst_shipのapi_idとの対応)
(2) 出撃前に母港状況をバッファリングしておく(api_req_map/startのタイミング)
(3) 戦闘結果で、データベースに初期装備のデータがあれば、そこから初期装備を推定して船を追加。なければ適当にダミーデータを放り込んで船を追加(api_req_sortie/battleresult や api_req_combined_battle/battleresultのタイミング)
(4) 帰投後に(2)と同じ方法で母港状況をバッファリングして、出撃前との差分を取る(api_portのタイミングで出撃前のバッファリングがnullかどうかで判定するのが簡単かと)
(5) 差分で新規の艦が存在すれば、データベースに初期装備を記録する

(1)のデータをユーザーに1から取らせないといけないので、ある程度の初期データを与えておいたほうがよいでしょう。そこで艦これt・old(http://www51.atpages.jp/kancollev/)から2014年9月頃の初期装備があったときのマスターデータを取得し、定数として与えておきます。艦これt oldは普通のテーブルですが、これをExcel等で動的にコードを作成したものがこちら↓

KancolleDOld.cs
https://www.dropbox.com/s/g16s85fcol6q75i/KancolleDOld.cs?dl=0

ここらへんはライブラリ化したいよね。

つぎに差分を求める部分です。
DefaultSlotitemDataBase.cs
https://www.dropbox.com/s/mrkibk5bddqci0r/DefaultSlotitemDataBase.cs?dl=0

出撃前にはこのBeforeSortieProcess()を読ませて、帰投後にはAfterReturnProcess()を読ませます。戦闘終了後のドロップ(battleresult)と建造で入手したタイミング(getship)でAddCollectionを読ませます。シリアル化にProtoBufを使っていますが、適宜お使いのライブラリに読み替えてください。開発者の方みんなすごいんでそこまで解説しなくてもわかるんじゃないかな…(解説めんどいだけ)

あとこのデータベースから船のデータオブジェクトを初期化する操作は各自実装してください。適当にstaticなメソッド作っておけばいいんで。

(追記)このDBから船のインスタンスを作るところで少しはまる可能性があるので、ほっぽアルファで使ってるソースを書いておきます。ApiShipや艦船データのコレクションの定義は環境によって異なるとは思いますが適宜読み替えてください。

    public class ApiShip
{
public int api_id { get; set; }
public int api_sortno { get; set; }
public int api_ship_id { get; set; }
public int api_lv { get; set; }
public List api_exp { get; set; }
public int api_nowhp { get; set; }
public int api_maxhp { get; set; }
public int api_leng { get; set; }
public List api_slot { get; set; }
public List api_onslot { get; set; }
public List api_kyouka { get; set; }
public int api_backs { get; set; }
public int api_fuel { get; set; }
public int api_bull { get; set; }
public int api_slotnum { get; set; }
public int api_ndock_time { get; set; }
public List api_ndock_item { get; set; }
public int api_srate { get; set; }
public int api_cond { get; set; }
public List api_karyoku { get; set; }
public List api_raisou { get; set; }
public List api_taiku { get; set; }
public List api_soukou { get; set; }
public List api_kaihi { get; set; }
public List api_taisen { get; set; }
public List api_sakuteki { get; set; }
public List api_lucky { get; set; }
public int api_locked { get; set; }
public int api_locked_equip { get; set; }
public int api_sally_area { get; set; }

//ディープコピー
public ApiShip Clone()
{
// シリアル化した内容を保持しておくためのMemoryStreamを作成
using (MemoryStream stream = new MemoryStream())
{
// バイナリシリアル化を行うためのフォーマッタを作成
BinaryFormatter f = new BinaryFormatter();
// 現在のインスタンスをシリアル化してMemoryStreamに格納
f.Serialize(stream, this);
// ストリームの位置を先頭に戻す
stream.Position = 0L;
// MemoryStreamに格納されている内容を逆シリアル化する
return (ApiShip)f.Deserialize(stream);
}
}

//完全なダミーデータ
public static ApiShip MakeDummy()
{
ApiShip oship = new ApiShip();
//初期値でNULLになるプロパティの初期化
oship.api_exp = Enumerable.Repeat(0, 3).ToList();
oship.api_slot = Enumerable.Repeat(-1, 5).ToList();
oship.api_onslot = Enumerable.Repeat(0, 5).ToList();
oship.api_kyouka = Enumerable.Repeat(0, 5).ToList();
oship.api_ndock_item = Enumerable.Repeat(0, 2).ToList();

oship.api_karyoku = Enumerable.Repeat(0, 2).ToList();
oship.api_raisou = Enumerable.Repeat(0, 2).ToList();
oship.api_taiku = Enumerable.Repeat(0, 2).ToList();
oship.api_soukou = Enumerable.Repeat(0, 2).ToList();
oship.api_kaihi = Enumerable.Repeat(0, 2).ToList();
oship.api_taisen = Enumerable.Repeat(0, 2).ToList();
oship.api_sakuteki = Enumerable.Repeat(0, 2).ToList();
oship.api_lucky = Enumerable.Repeat(0, 2).ToList();
//値を与えておかないと大変なことになるプロパティ
oship.api_id = APIPort.ShipsDictionary.Keys.Max() + 100;//キーの最大値+100 1だと轟沈したとき面倒なことになる可能性あり

return oship;
}

//マスターIDを指定して新規艦の作成
public static ApiShip MakeNewShip(int shipid)
{
ApiShip oship = MakeDummy();

//マスターIDに入っているか
ApiMstShip dship;
if (!APIMaster.MstShipsDictionary.TryGetValue(shipid, out dship)) return oship;
//装備IDの最大値の取得
int slotid = APIPort.ShipsDictionary.Values.SelectMany(x => x.api_slot).Max() + 1;
//装備スロットの作成
List slots = oship.api_slot.Select(x => x).ToList();
DefaultSlotitemTable.DefaultSlotitemRecord records;
if(DefaultSlotitemDataBase.Collection.Items.TryGetValue(shipid, out records))
{
foreach(int i in Enumerable.Range(0, Math.Min(slots.Count, records.MasterSlotitemID.Count)))
{
if(records.MasterSlotitemID[i] != -1)
{
slots[i] = slotid;
slotid++;
}
}
}

//dshipからパラメーターのラッピング
//api_idはダミーで初期化済み
oship.api_sortno = dship.api_sortno;
oship.api_ship_id = shipid;
oship.api_lv = 1;
//api_expはダミー 5
oship.api_nowhp = dship.api_taik[0];
oship.api_maxhp = oship.api_nowhp;
oship.api_leng = dship.api_leng;
oship.api_slot = slots;
oship.api_onslot = dship.api_maxeq.Select(x => x).ToList();//そのままリスト指定だと参照渡しになってしまうため 10
//api_kyoukaはそのまま
oship.api_backs = dship.api_backs;
oship.api_fuel = dship.api_fuel_max;
oship.api_bull = dship.api_bull_max;
oship.api_slotnum = dship.api_slot_num;//15
//api_ndock_timeは0なのでそのまま
//api_ndock_itemも初期値なのでそのまま
//api_srateはよくわからないのでそのまま
oship.api_cond = 40;//入手時のcond
oship.api_karyoku = dship.api_houg.Select(x => x).ToList();//20
oship.api_raisou = dship.api_raig.Select(x => x).ToList();
oship.api_taiku = dship.api_tyku.Select(x => x).ToList();
oship.api_soukou = dship.api_souk.Select(x => x).ToList();
//api_kaihiは消えてるのでそのまま
//api_taisenも消えてるのでそのまま 25
//api_sakutekiも(ry
oship.api_lucky = dship.api_luck.Select(x => x).ToList();
//api_lockedも0なので
//api_locked_equipも同様
//api_sally_areaも同様 30

return oship;
}
}


注意点
・マスターデータからインスタンスを作る際、参照型の変数はディープコピー(この例ではLINQでディープコピーしています)してあげないと、個々の船のパラメーターが書き換えられたときに、マスターデータ側も書き換わってしまうため注意
・船のインスタンスに与えるID(api_id)ですが、正確な値が取得できないので、艦船データのコレクションからIDの最大値+100で与えています。最大値+1にしてしまうと次のような不具合がおこる可能性があります。
出撃時:第1艦隊 ID1,.2,3

1戦目:第1艦隊 ID3が大破 ドロップなし

2戦目:第1艦隊 ID3が轟沈 ドロップあり(このときドロップ艦のIDが3として推定されてしまう可能性がある)
本当はApiShipにIDのキャレット的なパラメーターを用意してあげればいいけど、1周でドロップ100隻はまずあり得ないのでそこら辺の離したパラメーターでOKだと思います。
・帰投後、母港APIが呼ばれたときにIDと装備の答え合わせが行われますが、出撃中は装備コレクションにはデータがないので、装備検索等で未反映のドロップ艦の装備を呼び出す場合はエラー対策が必要。ただ、これはship2の環境でも同様。

・艦船データをDictionaryとして与えているのは、ApiShipのapi_idをキーとさせて、O(1)操作で艦船データを取り出すため。Listで定義してLINQでいちいち検索しているとO(n)になって遅くなると思います。
プロフィール

こしあん

Author:こしあん
(:3[____]
【TwitterID : koshian2】
【ほしい物リスト】http://goo.gl/bDtvG2

Twitter
カウンター
天気予報

-天気予報コム- -FC2-
カテゴリ
月別アーカイブ
最新記事
最新トラックバック
検索フォーム
リンク