OSDN Git Service

終了時に直近の提督経験値を記録する
[kancollesniffer/KancolleSniffer.git] / KancolleSniffer / Logger.cs
1 // Copyright (C) 2014, 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>\r
2 //\r
3 // Licensed under the Apache License, Version 2.0 (the "License");\r
4 // you may not use this file except in compliance with the License.\r
5 // You may obtain a copy of the License at\r
6 //\r
7 //    http://www.apache.org/licenses/LICENSE-2.0\r
8 //\r
9 // Unless required by applicable law or agreed to in writing, software\r
10 // distributed under the License is distributed on an "AS IS" BASIS,\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
12 // See the License for the specific language governing permissions and\r
13 // limitations under the License.\r
14 \r
15 using System;\r
16 using System.Collections.Generic;\r
17 using System.IO;\r
18 using System.Linq;\r
19 using System.Text;\r
20 \r
21 namespace KancolleSniffer\r
22 {\r
23     [Flags]\r
24     public enum LogType\r
25     {\r
26         None = 0,\r
27         Mission = 1 << 0,\r
28         Battle = 1 << 1,\r
29         Material = 1 << 2,\r
30         CreateItem = 1 << 3,\r
31         CreateShip = 1 << 4,\r
32         RemodelSlot = 1 << 5,\r
33         Achivement = 1 << 6,\r
34         All = (1 << 7) - 1\r
35     }\r
36 \r
37     public class Logger\r
38     {\r
39         private LogType _logType;\r
40         private readonly ShipInfo _shipInfo;\r
41         private readonly ItemInfo _itemInfo;\r
42         private readonly BattleInfo _battleInfo;\r
43         private Action<string, string, string> _writer;\r
44         private Func<DateTime> _nowFunc;\r
45         public const string DateTimeFormat = @"yyyy\-MM\-dd HH\:mm\:ss";\r
46         private dynamic _battle;\r
47         private dynamic _map;\r
48         private dynamic _basic;\r
49         private int _kdockId;\r
50         private DateTime _prevTime;\r
51         private int[] _currentMaterial = new int[Enum.GetValues(typeof(Material)).Length];\r
52         private int _materialLogInterval = 10;\r
53         private bool _start;\r
54         private int _lastExp = -1;\r
55         private DateTime _lastDate;\r
56         private DateTime _endOfMonth;\r
57         private DateTime _nextDate;\r
58 \r
59         public int MaterialLogInterval\r
60         {\r
61             set { _materialLogInterval = value; }\r
62         }\r
63 \r
64         public string OutputDir\r
65         {\r
66             set { _writer = new LogWriter(value).Write; }\r
67         }\r
68 \r
69         public Logger(ShipInfo ship, ItemInfo item, BattleInfo battle)\r
70         {\r
71             _shipInfo = ship;\r
72             _itemInfo = item;\r
73             _battleInfo = battle;\r
74             _writer = new LogWriter().Write;\r
75             _nowFunc = () => DateTime.Now;\r
76         }\r
77 \r
78         public void EnableLog(LogType type)\r
79         {\r
80             _logType = type;\r
81         }\r
82 \r
83         public void SetWriter(Action<string, string, string> writer, Func<DateTime> nowFunc)\r
84         {\r
85             _writer = writer;\r
86             _nowFunc = nowFunc;\r
87         }\r
88 \r
89         public void FlashLog()\r
90         {\r
91             FlashAchivementLog();\r
92         }\r
93 \r
94         public void InspectMissionResult(dynamic json)\r
95         {\r
96             var r = (int)json.api_clear_result;\r
97             var rstr = r == 2 ? "大成功" : r == 1 ? "成功" : "失敗";\r
98             var material = new int[7];\r
99             if (r != 0)\r
100                 ((int[])json.api_get_material).CopyTo(material, 0);\r
101             foreach (var i in new[] {1, 2})\r
102             {\r
103                 var attr = "api_get_item" + i;\r
104                 if (!json.IsDefined(attr) || json[attr].api_useitem_id != -1)\r
105                     continue;\r
106                 var count = (int)json[attr].api_useitem_count;\r
107                 var flag = ((int[])json.api_useitem_flag)[i - 1];\r
108                 if (flag == 1)\r
109                     material[(int)Material.Bucket] = count;\r
110                 else if (flag == 2)\r
111                     material[(int)Material.Burner + 2] = count; // 高速建造材と開発資材が反対なのでいつか直す\r
112                 else if (flag == 3)\r
113                     material[(int)Material.Development - 2] = count;\r
114             }\r
115             if ((_logType & LogType.Mission) != 0)\r
116             {\r
117                 _writer("遠征報告書",\r
118                     string.Join(",", _nowFunc().ToString(DateTimeFormat),\r
119                         rstr, json.api_quest_name, string.Join(",", material)),\r
120                     "日付,結果,遠征,燃料,弾薬,鋼材,ボーキ,開発資材,高速修復材,高速建造材");\r
121             }\r
122         }\r
123 \r
124         public void InspectMapStart(dynamic json)\r
125         {\r
126             _start = true;\r
127             _map = json;\r
128             _battle = null;\r
129             if ((_logType & LogType.Material) != 0)\r
130                 WriteMaterialLog(_nowFunc());\r
131         }\r
132 \r
133         public void InspectMapNext(dynamic json)\r
134         {\r
135             _map = json;\r
136         }\r
137 \r
138         public void InspectBattle(dynamic json)\r
139         {\r
140             if (_battle != null) // 通常の夜戦は無視する\r
141                 return;\r
142             _battle = json;\r
143         }\r
144 \r
145         public void InspectBattleResult(dynamic result)\r
146         {\r
147             if ((_logType & LogType.Achivement) != 0 && result.api_get_exmap_rate())\r
148             {\r
149                 var rate = result.api_get_exmap_rate is string\r
150                     ? int.Parse(result.api_get_exmap_rate)\r
151                     : (int)result.api_get_exmap_rate;\r
152                 if (rate != 0)\r
153                 {\r
154                     _writer("戦果", _nowFunc().ToString(DateTimeFormat) + "," + _lastExp + "," + rate,\r
155                         "日付,経験値,EO");\r
156                 }\r
157             }\r
158             if ((_logType & LogType.Battle) == 0 || _map == null || _battle == null)\r
159             {\r
160                 _map = _battle = null;\r
161                 return;\r
162             }\r
163             var fships = GenerateFirendShipList();\r
164             var eships = GenerateEnemyShipList();\r
165             var cell = (int)_map.api_no;\r
166             var boss = "";\r
167             if (_start)\r
168                 boss = "出撃";\r
169             if (cell == (int)_map.api_bosscell_no || (int)_map.api_event_id == 5)\r
170                 boss = _start ? "出撃&ボス" : "ボス";\r
171             var dropType = result.api_get_ship() ? result.api_get_ship.api_ship_type : "";\r
172             if (result.api_get_useitem())\r
173             {\r
174                 if (dropType == "")\r
175                     dropType = "アイテム";\r
176                 else\r
177                     dropType += "+アイテム";\r
178             }\r
179             var dropName = result.api_get_ship() ? result.api_get_ship.api_ship_name : "";\r
180             if (result.api_get_useitem())\r
181             {\r
182                 var itemName = _itemInfo.GetUseItemName((int)result.api_get_useitem.api_useitem_id);\r
183                 if (dropName == "")\r
184                     dropName = itemName;\r
185                 else\r
186                     dropName += "+" + itemName;\r
187             }\r
188             var fp = _shipInfo.GetFighterPower(BattleInfo.DeckId(_battle));\r
189             var fpower = fp[0] == fp[1] ? fp[0].ToString() : fp[0] + "~" + fp[1];\r
190             _writer("海戦・ドロップ報告書", string.Join(",", _nowFunc().ToString(DateTimeFormat),\r
191                 result.api_quest_name,\r
192                 cell, boss,\r
193                 result.api_win_rank,\r
194                 BattleFormationName((int)_battle.api_formation[2]),\r
195                 FormationName(_battle.api_formation[0]),\r
196                 FormationName(_battle.api_formation[1]),\r
197                 result.api_enemy_info.api_deck_name,\r
198                 dropType, dropName,\r
199                 string.Join(",", fships),\r
200                 string.Join(",", eships),\r
201                 fpower, _battleInfo.EnemyFighterPower,\r
202                 AirControlLevelName(_battle)),\r
203                 "日付,海域,マス,ボス,ランク,艦隊行動,味方陣形,敵陣形,敵艦隊,ドロップ艦種,ドロップ艦娘," +\r
204                 "味方艦1,味方艦1HP,味方艦2,味方艦2HP,味方艦3,味方艦3HP,味方艦4,味方艦4HP,味方艦5,味方艦5HP,味方艦6,味方艦6HP," +\r
205                 "敵艦1,敵艦1HP,敵艦2,敵艦2HP,敵艦3,敵艦3HP,敵艦4,敵艦4HP,敵艦5,敵艦5HP,敵艦6,敵艦6HP," +\r
206                 "味方制空値,敵制空値,制空状態"\r
207                 );\r
208             _map = _battle = null;\r
209             _start = false;\r
210         }\r
211 \r
212         private IEnumerable<string> GenerateFirendShipList()\r
213         {\r
214             int deckId = BattleInfo.DeckId(_battle);\r
215             if (_battle.api_nowhps_combined() && (int)_battle.api_nowhps_combined[1] != -1)\r
216             {\r
217                 var main = _shipInfo.GetDeck(0);\r
218                 var guard = _shipInfo.GetDeck(1);\r
219                 return main.Zip(guard, (m, g) =>\r
220                 {\r
221                     if (m == -1 && g == -1)\r
222                         return ",";\r
223                     var name = "";\r
224                     var hp = "";\r
225                     if (m != -1)\r
226                     {\r
227                         var sm = _shipInfo.GetStatus(m);\r
228                         name = $"{sm.Name}(Lv{sm.Level})";\r
229                         hp = $"{sm.NowHp}/{sm.MaxHp}";\r
230                     }\r
231                     name += "・";\r
232                     hp += "・";\r
233                     if (g != -1)\r
234                     {\r
235                         var sg = _shipInfo.GetStatus(g);\r
236                         name += $"{sg.Name}(Lv{sg.Level})";\r
237                         hp += $"{sg.NowHp}/{sg.MaxHp}";\r
238                     }\r
239                     return name + "," + hp;\r
240                 }).ToList();\r
241             }\r
242             var deck = _shipInfo.GetDeck(deckId);\r
243             return deck.Select(id =>\r
244             {\r
245                 if (id == -1)\r
246                     return ",";\r
247                 var s = _shipInfo.GetStatus(id);\r
248                 return $"{s.Name}(Lv{s.Level}),{s.NowHp}/{s.MaxHp}";\r
249             }).ToList();\r
250         }\r
251 \r
252         private IEnumerable<string> GenerateEnemyShipList()\r
253         {\r
254             var result = _battleInfo.EnemyResultStatus;\r
255             if (result.Length <= 6)\r
256                 return result.Select(s => s.Id == -1 ? "," : $"{s.Name},{s.NowHp}/{s.MaxHp}").ToList();\r
257             var main = result.Take(6);\r
258             var guard = result.Skip(6);\r
259             return main.Zip(guard, (m, g) =>\r
260             {\r
261                 if (m.Id == -1 && g.Id == -1)\r
262                     return ",";\r
263                 var name = "";\r
264                 var hp = "";\r
265                 if (m.Id != -1)\r
266                 {\r
267                     name = $"{m.Name}";\r
268                     hp = $"{m.NowHp}/{m.MaxHp}";\r
269                 }\r
270                 name += "・";\r
271                 hp += "・";\r
272                 if (g.Id != -1)\r
273                 {\r
274                     name += $"{g.Name}";\r
275                     hp += $"{g.NowHp}/{g.MaxHp}";\r
276                 }\r
277                 return name + "," + hp;\r
278             }).ToList();\r
279         }\r
280 \r
281         private string FormationName(dynamic f)\r
282         {\r
283             if (f is string) // 連合艦隊のときは文字列\r
284                 f = int.Parse(f);\r
285             switch ((int)f)\r
286             {\r
287                 case 1:\r
288                     return "単縦陣";\r
289                 case 2:\r
290                     return "複縦陣";\r
291                 case 3:\r
292                     return "輪形陣";\r
293                 case 4:\r
294                     return "梯形陣";\r
295                 case 5:\r
296                     return "単横陣";\r
297                 case 11:\r
298                     return "第一警戒航行序列";\r
299                 case 12:\r
300                     return "第二警戒航行序列";\r
301                 case 13:\r
302                     return "第三警戒航行序列";\r
303                 case 14:\r
304                     return "第四警戒航行序列";\r
305                 default:\r
306                     return "単縦陣";\r
307             }\r
308         }\r
309 \r
310         private static string BattleFormationName(int f)\r
311         {\r
312             switch (f)\r
313             {\r
314                 case 1:\r
315                     return "同航戦";\r
316                 case 2:\r
317                     return "反航戦";\r
318                 case 3:\r
319                     return "T字戦(有利)";\r
320                 case 4:\r
321                     return "T字戦(不利)";\r
322                 default:\r
323                     return "同航戦";\r
324             }\r
325         }\r
326 \r
327         private string AirControlLevelName(dynamic json)\r
328         {\r
329             // BattleInfo.AirControlLevelは夜戦で消されているかもしれないので、こちらで改めて調べる。\r
330             if (!json.api_kouku())\r
331                 return "";\r
332             var stage1 = json.api_kouku.api_stage1;\r
333             if (stage1 == null)\r
334                 return "";\r
335             if (stage1.api_f_count == 0 && stage1.api_e_count == 0)\r
336                 return "";\r
337             switch ((int)stage1.api_disp_seiku)\r
338             {\r
339                 case 0:\r
340                     return "航空均衡";\r
341                 case 1:\r
342                     return "制空権確保";\r
343                 case 2:\r
344                     return "航空優勢";\r
345                 case 3:\r
346                     return "航空劣勢";\r
347                 case 4:\r
348                     return "制空権喪失";\r
349                 default:\r
350                     return "";\r
351             }\r
352         }\r
353 \r
354         public void InspectBasic(dynamic json)\r
355         {\r
356             _basic = json;\r
357             if ((_logType & LogType.Achivement) == 0)\r
358                 return;\r
359             var now = _nowFunc();\r
360             var exp = (int)json.api_experience;\r
361             var isNewMonth = _endOfMonth == DateTime.MinValue || now.CompareTo(_endOfMonth) >= 0;\r
362             var isNewDate = _nextDate == DateTime.MinValue || now.CompareTo(_nextDate) >= 0;\r
363             if (isNewDate || isNewMonth)\r
364             {\r
365                 if (_lastDate != DateTime.MinValue)\r
366                 {\r
367                     _writer("戦果", _lastDate.ToString(DateTimeFormat) + "," + _lastExp + ",0", "日付,経験値,EO");\r
368                 }\r
369                 _writer("戦果", now.ToString(DateTimeFormat) + "," + exp + ",0", "日付,経験値,EO");\r
370                 if (isNewMonth)\r
371                 {\r
372                     _endOfMonth = new DateTime(now.Year, now.Month, DateTime.DaysInMonth(now.Year, now.Month), 22, 0, 0);\r
373                     if (_endOfMonth.CompareTo(now) <= 0)\r
374                     {\r
375                         var days = _endOfMonth.Month == 12\r
376                             ? DateTime.DaysInMonth(_endOfMonth.Year + 1, 1)\r
377                             : DateTime.DaysInMonth(_endOfMonth.Year, _endOfMonth.Month);\r
378                         _endOfMonth = _endOfMonth.AddDays(days);\r
379                     }\r
380                 }\r
381                 _nextDate = new DateTime(now.Year, now.Month, now.Day, 5, 0, 0);\r
382                 if (now.Hour >= 5)\r
383                     _nextDate = _nextDate.AddDays(1);\r
384                 if (_nextDate.Day == 1)\r
385                     _nextDate = _nextDate.AddDays(1);\r
386             }\r
387             _lastDate = now;\r
388             _lastExp = exp;\r
389         }\r
390 \r
391         private void FlashAchivementLog()\r
392         {\r
393             if ((_logType & LogType.Achivement) == 0)\r
394                 return;\r
395             if (_lastDate != DateTime.MinValue)\r
396             {\r
397                 _writer("戦果", _lastDate.ToString(DateTimeFormat) + "," + _lastExp + ",0", "日付,経験値,EO");\r
398             }\r
399         }\r
400 \r
401         public void InspectCreateItem(string request, dynamic json)\r
402         {\r
403             if ((_logType & LogType.CreateItem) == 0)\r
404                 return;\r
405             var values = HttpUtility.ParseQueryString(request);\r
406             var name = "失敗";\r
407             var type = "";\r
408             if (json.api_slot_item())\r
409             {\r
410                 var spec = _itemInfo.GetSpecByItemId((int)json.api_slot_item.api_slotitem_id);\r
411                 name = spec.Name;\r
412                 type = spec.TypeName;\r
413             }\r
414             _writer("開発報告書",\r
415                 _nowFunc().ToString(DateTimeFormat) + "," +\r
416                 string.Join(",", name, type,\r
417                     values["api_item1"], values["api_item2"], values["api_item3"], values["api_item4"],\r
418                     Secretary(), _basic.api_level),\r
419                 "日付,開発装備,種別,燃料,弾薬,鋼材,ボーキ,秘書艦,司令部Lv");\r
420         }\r
421 \r
422         public void InspectCreateShip(string request)\r
423         {\r
424             var values = HttpUtility.ParseQueryString(request);\r
425             _kdockId = int.Parse(values["api_kdock_id"]);\r
426         }\r
427 \r
428         public void InspectKDock(dynamic json)\r
429         {\r
430             if ((_logType & LogType.CreateShip) == 0 || _basic == null || _kdockId == 0)\r
431                 return;\r
432             var kdock = ((dynamic[])json).First(e => e.api_id == _kdockId);\r
433             var material = Enumerable.Range(1, 5).Select(i => (int)kdock["api_item" + i]).ToArray();\r
434             var ship = _shipInfo.GetSpec((int)kdock.api_created_ship_id);\r
435             var avail = ((dynamic[])json).Count(e => (int)e.api_state == 0);\r
436             _writer("建造報告書",\r
437                 _nowFunc().ToString(DateTimeFormat) + "," +\r
438                 string.Join(",", material.First() >= 1500 ? "大型艦建造" : "通常艦建造",\r
439                     ship.Name, ship.ShipTypeName, string.Join(",", material), avail, Secretary(), _basic.api_level),\r
440                 "日付,種類,名前,艦種,燃料,弾薬,鋼材,ボーキ,開発資材,空きドック,秘書艦,司令部Lv");\r
441             _kdockId = 0;\r
442         }\r
443 \r
444         private string Secretary()\r
445         {\r
446             var ship = _shipInfo.GetShipStatuses(0)[0];\r
447             return ship.Name + "(" + ship.Level + ")";\r
448         }\r
449 \r
450         public void InspectMaterial(dynamic json)\r
451         {\r
452             if ((_logType & LogType.Material) == 0)\r
453                 return;\r
454             foreach (var e in json)\r
455                 _currentMaterial[(int)e.api_id - 1] = (int)e.api_value;\r
456             var now = _nowFunc();\r
457             if (now - _prevTime < TimeSpan.FromMinutes(_materialLogInterval))\r
458                 return;\r
459             WriteMaterialLog(now);\r
460         }\r
461 \r
462         public void WriteMaterialLog(DateTime now)\r
463         {\r
464             _prevTime = now;\r
465             _writer("資材ログ",\r
466                 now.ToString(DateTimeFormat) + "," +\r
467                 string.Join(",", _currentMaterial),\r
468                 "日付,燃料,弾薬,鋼材,ボーキ,高速建造材,高速修復材,開発資材,改修資材");\r
469         }\r
470 \r
471         public void SetCurrentMaterial(int[] material)\r
472         {\r
473             _currentMaterial = material;\r
474         }\r
475 \r
476         public void InspectRemodelSlot(string request, dynamic json)\r
477         {\r
478             if ((_logType & LogType.RemodelSlot) == 0)\r
479                 return;\r
480             var now = _nowFunc();\r
481             var values = HttpUtility.ParseQueryString(request);\r
482             var id = int.Parse(values["api_slot_id"]);\r
483             var name = _itemInfo.GetName(id);\r
484             var level = _itemInfo.GetStatus(id).Level;\r
485             var success = (int)json.api_remodel_flag == 1 ? "○" : "×";\r
486             var certain = int.Parse(values["api_certain_flag"]) == 1 ? "○" : "";\r
487             var useName = "";\r
488             var useNum = "";\r
489             if (json.api_use_slot_id())\r
490             {\r
491                 var use = (int[])json.api_use_slot_id;\r
492                 useName = _itemInfo.GetName(use[0]);\r
493                 useNum = use.Length.ToString();\r
494             }\r
495             var after = (int[])json.api_after_material;\r
496             var diff = new int[after.Length];\r
497             for (var i = 0; i < after.Length; i++)\r
498                 diff[i] = _currentMaterial[i] - after[i];\r
499             var ship1 = Secretary();\r
500             var ship2 = "";\r
501             var ships = _shipInfo.GetShipStatuses(0);\r
502             if (ships.Length >= 2)\r
503                 ship2 = ships[1].Name + "(" + ships[1].Level + ")";\r
504             _writer("改修報告書",\r
505                 now.ToString(DateTimeFormat) + "," +\r
506                 string.Join(",", name, level, success, certain, useName, useNum,\r
507                     diff[(int)Material.Fuel], diff[(int)Material.Bullet], diff[(int)Material.Steal],\r
508                     diff[(int)Material.Bouxite],\r
509                     diff[(int)Material.Development], diff[(int)Material.Screw],\r
510                     ship1, ship2),\r
511                 "日付,改修装備,レベル,成功,確実化,消費装備,消費数,燃料,弾薬,鋼材,ボーキ,開発資材,改修資材,秘書艦,二番艦");\r
512         }\r
513     }\r
514 \r
515     public class LogWriter\r
516     {\r
517         private readonly IFile _file;\r
518         private readonly string _outputDir;\r
519 \r
520         public interface IFile\r
521         {\r
522             string ReadAllText(string path);\r
523             void AppendAllText(string path, string text);\r
524             void Delete(string path);\r
525             bool Exists(string path);\r
526         }\r
527 \r
528         private class FileWrapper : IFile\r
529         {\r
530             // Shift_JISでないとExcelで文字化けする\r
531             private readonly Encoding _encoding = Encoding.GetEncoding("Shift_JIS");\r
532 \r
533             public string ReadAllText(string path) => File.ReadAllText(path, _encoding);\r
534 \r
535             public void AppendAllText(string path, string text)\r
536             {\r
537                 File.AppendAllText(path, text, _encoding);\r
538             }\r
539 \r
540             public void Delete(string path)\r
541             {\r
542                 File.Delete(path);\r
543             }\r
544 \r
545             public bool Exists(string path) => File.Exists(path);\r
546         }\r
547 \r
548         public LogWriter(string outputDir = null, IFile file = null)\r
549         {\r
550             _outputDir = outputDir ?? AppDomain.CurrentDomain.BaseDirectory;\r
551             _file = file ?? new FileWrapper();\r
552         }\r
553 \r
554         public void Write(string file, string s, string header)\r
555         {\r
556             var path = Path.Combine(_outputDir, file);\r
557             var csv = path + ".csv";\r
558             var tmp = path + ".tmp";\r
559             if (_file.Exists(tmp))\r
560             {\r
561                 try\r
562                 {\r
563                     _file.AppendAllText(csv, _file.ReadAllText(tmp));\r
564                     _file.Delete(tmp);\r
565                 }\r
566                 catch (IOException)\r
567                 {\r
568                 }\r
569             }\r
570             if (!_file.Exists(csv))\r
571                 s = header + "\r\n" + s;\r
572             foreach (var f in new[] {csv, tmp})\r
573             {\r
574                 try\r
575                 {\r
576                     _file.AppendAllText(f, s + "\r\n");\r
577                     break;\r
578                 }\r
579                 catch (IOException e)\r
580                 {\r
581                     if (f == tmp)\r
582                         throw new LogIOException("報告書の出力中にエラーが発生しました。", e);\r
583                 }\r
584             }\r
585         }\r
586     }\r
587 \r
588     public class LogIOException : Exception\r
589     {\r
590         public LogIOException()\r
591         {\r
592         }\r
593 \r
594         public LogIOException(string message) : base(message)\r
595         {\r
596         }\r
597 \r
598         public LogIOException(string message, Exception inner) : base(message, inner)\r
599         {\r
600         }\r
601     }\r
602 }