1 // Copyright (C) 2013, 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>
\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
7 // http://www.apache.org/licenses/LICENSE-2.0
\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
16 using System.Collections.Generic;
\r
17 using System.Drawing;
\r
19 using System.Windows.Forms;
\r
20 using System.Xml.Serialization;
\r
21 using static System.Math;
\r
23 namespace KancolleSniffer
\r
25 public class QuestStatus
\r
27 public int Id { get; set; }
\r
28 public int Category { get; set; }
\r
29 public string Name { get; set; }
\r
30 public QuestCount Count { get; set; }
\r
31 public int Progress { get; set; }
\r
32 public Color Color { get; set; }
\r
35 public enum QuestInterval
\r
44 public class QuestSpec
\r
46 public QuestInterval Interval { get; set; }
\r
47 public int Max { get; set; }
\r
48 public int[] MaxArray { get; set; }
\r
49 public bool AdjustCount { get; set; } = true;
\r
50 public int Shift { get; set; }
\r
53 public class QuestSortie : QuestSpec
\r
55 public string Rank { get; set; }
\r
56 public int[] Maps { get; set; }
\r
57 public int[] ShipTypes { get; set; }
\r
59 public static int CompareRank(string a, string b)
\r
61 const string ranks = "SABCDE";
\r
62 return ranks.IndexOf(a, StringComparison.Ordinal) -
\r
63 ranks.IndexOf(b, StringComparison.Ordinal);
\r
66 public bool Check(string rank, int map, bool boss)
\r
68 return (Rank == null || CompareRank(rank, Rank) <= 0) &&
\r
69 (Maps == null || Maps.Contains(map) && boss);
\r
73 public class QuestEnemyType : QuestSpec
\r
75 public int[] EnemyType { get; set; } = new int[0];
\r
77 public int CountResult(IEnumerable<ShipStatus> enemyResult) =>
\r
78 enemyResult.Count(ship => ship.NowHp == 0 && EnemyType.Contains(ship.Spec.ShipType));
\r
81 public class QuestPractice : QuestSpec
\r
83 public bool Win { get; set; }
\r
84 public bool Check(string rank) => !Win || QuestSortie.CompareRank(rank, "B") <= 0;
\r
87 public class QuestMission : QuestSpec
\r
89 public int[] Ids { get; set; }
\r
90 public bool Check(int id) => Ids == null || Ids.Contains(id);
\r
93 public class QuestDestroyItem : QuestSpec
\r
95 public int[] Items { get; set; }
\r
96 public bool Check(int id) => Items == null || Items.Contains(id);
\r
99 public class QuestPowerup : QuestSpec
\r
103 public class QuestCount
\r
105 public int Id { get; set; }
\r
106 public int Now { get; set; }
\r
107 public int[] NowArray { get; set; }
\r
110 public QuestSpec Spec { get; set; }
\r
112 public bool AdjustCount(int progress)
\r
114 if (!Spec.AdjustCount)
\r
116 if (NowArray != null)
\r
118 if (progress != 100)
\r
120 NowArray = NowArray.Zip(Spec.MaxArray, Max).ToArray();
\r
139 var now = Now + Spec.Shift;
\r
140 var max = Spec.Max + Spec.Shift;
\r
141 var low = (int)Ceiling(max * progress / 100.0);
\r
142 var high = (int)Ceiling(max * next / 100.0);
\r
145 Now = low - Spec.Shift;
\r
150 Now = high - 1 - Spec.Shift;
\r
156 public override string ToString()
\r
159 return $"{NowArray.Count(n => n == 1)}/{Spec.MaxArray.Length}";
\r
160 return NowArray != null
\r
161 ? string.Join(" ", NowArray.Zip(Spec.MaxArray, (n, m) => $"{n}/{m}"))
\r
162 : $"{Now}/{Spec.Max}";
\r
165 public string ToToolTip()
\r
170 new[] {"2-4", "6-1", "6-3", "6-4"}.Zip(NowArray, (map, flag) => flag == 1 ? map : "")
\r
171 .Where(s => !string.IsNullOrEmpty(s)));
\r
174 public bool Cleared => NowArray?.Zip(Spec.MaxArray, (n, m) => n >= m).All(x => x) ?? Now >= Spec.Max;
\r
178 public class QuestCountList
\r
180 private const QuestInterval Daily = QuestInterval.Daily;
\r
181 private const QuestInterval Weekly = QuestInterval.Weekly;
\r
182 private const QuestInterval Monthly = QuestInterval.Monthly;
\r
183 private const QuestInterval Quarterly = QuestInterval.Quarterly;
\r
186 /// このテーブルは七四式電子観測儀を参考に作成した。
\r
187 /// https://github.com/andanteyk/ElectronicObserver/blob/develop/ElectronicObserver/Data/Quest/QuestProgressManager.cs
\r
189 private static readonly Dictionary<int, QuestSpec> QuestSpecs = new Dictionary<int, QuestSpec>
\r
191 {201, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 201: 敵艦隊を撃滅せよ!
\r
192 {216, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 216: 敵艦隊主力を撃滅せよ!
\r
193 {210, new QuestSortie {Interval = Daily, Max = 10}}, // 210: 敵艦隊を10回邀撃せよ!
\r
194 {211, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {7, 11}}}, // 211: 敵空母を3隻撃沈せよ!
\r
195 {212, new QuestEnemyType {Interval = Daily, Max = 5, EnemyType = new[] {15}}}, // 212: 敵輸送船団を叩け!
\r
196 {218, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {15}}}, // 218: 敵補給艦を3隻撃沈せよ!
\r
197 {226, new QuestSortie {Interval = Daily, Max = 5, Rank = "B", Maps = new[] {21, 22, 23, 24, 25}}}, // 226: 南西諸島海域の制海権を握れ!
\r
198 {230, new QuestEnemyType {Interval = Daily, Max = 6, EnemyType = new[] {13}}}, // 230: 敵潜水艦を制圧せよ!
\r
200 {213, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {15}}}, // 213: 海上通商破壊作戦
\r
201 {214, new QuestSpec {Interval = Weekly, MaxArray = new[] {36, 6, 24, 12}}}, // 214: あ号作戦
\r
202 {220, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {7, 11}}}, // 220: い号作戦
\r
203 {221, new QuestEnemyType {Interval = Weekly, Max = 50, EnemyType = new[] {15}}}, // 221: ろ号作戦
\r
204 {228, new QuestEnemyType {Interval = Weekly, Max = 15, EnemyType = new[] {13}}}, // 228: 海上護衛戦
\r
205 {229, new QuestSortie {Interval = Weekly, Max = 12, Rank = "B", Maps = new[] {41, 42, 43, 44, 45}}}, // 229: 敵東方艦隊を撃滅せよ!
\r
206 {241, new QuestSortie {Interval = Weekly, Max = 5, Rank = "B", Maps = new[] {33, 34, 35}}}, // 241: 敵北方艦隊主力を撃滅せよ!
\r
207 {242, new QuestSortie {Interval = Weekly, Max = 1, Rank = "B", Maps = new[] {44}}}, // 242: 敵東方中枢艦隊を撃破せよ!
\r
208 {243, new QuestSortie {Interval = Weekly, Max = 2, Rank = "B", Maps = new[] {52}}}, // 243: 南方海域珊瑚諸島沖の制空権を握れ!
\r
209 {256, new QuestSortie {Interval = Monthly, Max = 3, Rank = "S", Maps = new[] {61}}}, // 256: 「潜水艦隊」出撃せよ!
\r
210 {261, new QuestSortie {Interval = Weekly, Max = 3, Rank = "A", Maps = new[] {15}}}, // 261: 海上輸送路の安全確保に努めよ!
\r
211 {265, new QuestSortie {Interval = Monthly, Max = 10, Rank = "A", Maps = new[] {15}}}, // 265: 海上護衛強化月間
\r
213 {822, new QuestSortie {Interval = Quarterly, Max = 2, Rank = "S", Maps = new[] {24}}}, // 822: 沖ノ島海域迎撃戦
\r
214 {854, new QuestSpec {Interval = Quarterly, MaxArray = new[] {1, 1, 1, 1}}}, // 854: 戦果拡張任務!「Z作戦」前段作戦
\r
216 {303, new QuestPractice {Interval = Daily, Max = 3, Win = false}}, // 303: 「演習」で練度向上!
\r
217 {304, new QuestPractice {Interval = Daily, Max = 5, Win = true}}, // 304: 「演習」で他提督を圧倒せよ!
\r
218 {302, new QuestPractice {Interval = Weekly, Max = 20, Win = true}}, // 302: 大規模演習
\r
219 {311, new QuestPractice {Interval = Daily, Max = 7, Win = true}}, // 311: 精鋭艦隊演習
\r
221 {402, new QuestMission {Interval = Daily, Max = 3}}, // 402: 「遠征」を3回成功させよう!
\r
222 {403, new QuestMission {Interval = Daily, Max = 10}}, // 403: 「遠征」を10回成功させよう!
\r
223 {404, new QuestMission {Interval = Weekly, Max = 30}}, // 404: 大規模遠征作戦、発令!
\r
224 {410, new QuestMission {Interval = Weekly, Max = 1, Ids = new[] {37, 38}}}, // 410: 南方への輸送作戦を成功させよ!
\r
225 {411, new QuestMission {Interval = Weekly, Max = 6, Shift = 1, Ids = new[] {37, 38}}}, // 411: 南方への鼠輸送を継続実施せよ!
\r
226 {424, new QuestMission {Interval = Monthly, Max = 4, Shift = 1, Ids = new[] {5}}}, // 424: 輸送船団護衛を強化せよ!
\r
228 {503, new QuestSpec {Interval = Daily, Max = 5}}, // 503: 艦隊大整備!
\r
229 {504, new QuestSpec {Interval = Daily, Max = 15}}, // 504: 艦隊酒保祭り!
\r
231 {605, new QuestSpec {Interval = Daily, Max = 1}}, // 605: 新装備「開発」指令
\r
232 {606, new QuestSpec {Interval = Daily, Max = 1}}, // 606: 新造艦「建造」指令
\r
233 {607, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 607: 装備「開発」集中強化!
\r
234 {608, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 608: 艦娘「建造」艦隊強化!
\r
235 {609, new QuestSpec {Interval = Daily, Max = 2}}, // 609: 軍縮条約対応!
\r
236 {619, new QuestSpec {Interval = Daily, Max = 1}}, // 619: 装備の改修強化
\r
238 {613, new QuestSpec {Interval = Weekly, Max = 24}}, // 613: 資源の再利用
\r
239 {638, new QuestDestroyItem {Interval = Weekly, Max = 6, Items = new[] {21}}}, // 638: 対空機銃量産
\r
240 {673, new QuestDestroyItem {Interval = Daily, Max = 4, Items = new[] {1}, Shift = 1}}, // 673: 装備開発力の整備
\r
241 {674, new QuestDestroyItem {Interval = Daily, Max = 3, Items = new[] {21}, Shift = 2}}, // 674: 工廠環境の整備
\r
242 {675, new QuestSpec {Interval = Quarterly, MaxArray = new[] {6, 4}}}, // 675: 運用装備の統合整備
\r
243 {676, new QuestSpec {Interval = Weekly, MaxArray = new[] {3, 3, 1}}}, // 676: 装備開発力の集中整備
\r
244 {677, new QuestSpec {Interval = Weekly, MaxArray = new[] {4, 2, 3}}}, // 677: 継戦支援能力の整備
\r
246 {702, new QuestPowerup {Interval = Daily, Max = 2}}, // 702: 艦の「近代化改修」を実施せよ!
\r
247 {703, new QuestPowerup {Interval = Weekly, Max = 15}} // 703: 「近代化改修」を進め、戦備を整えよ!
\r
251 private readonly Dictionary<int, QuestCount> _countDict = new Dictionary<int, QuestCount>();
\r
253 public QuestCount GetCount(int id)
\r
255 if (_countDict.TryGetValue(id, out var value))
\r
257 if (QuestSpecs.TryGetValue(id, out var spec))
\r
259 var nowArray = spec.MaxArray?.Select(x => 0).ToArray();
\r
260 return _countDict[id] = new QuestCount
\r
264 NowArray = nowArray,
\r
268 return new QuestCount {Spec = new QuestSpec {AdjustCount = false}};
\r
271 public void Remove(int id)
\r
273 _countDict.Remove(id);
\r
276 public void Remove(QuestInterval interval)
\r
279 _countDict.Where(pair => pair.Value.Spec.Interval == interval).Select(pair => pair.Key).ToArray())
\r
281 _countDict.Remove(id);
\r
285 public IEnumerable<QuestCount> CountList
\r
287 get => _countDict.Values.Where(c => c.Now > 0 || (c.NowArray?.Any(n => n > 0) ?? false));
\r
292 foreach (var count in value)
\r
294 count.Spec = QuestSpecs[count.Id];
\r
295 _countDict[count.Id] = count;
\r
301 public class QuestInfo : IHaveState
\r
303 private readonly SortedDictionary<int, QuestStatus> _quests = new SortedDictionary<int, QuestStatus>();
\r
304 private readonly QuestCountList _countList = new QuestCountList();
\r
305 private readonly ItemInfo _itemInfo;
\r
306 private readonly BattleInfo _battleInfo;
\r
307 private readonly Func<DateTime> _nowFunc = () => DateTime.Now;
\r
308 private DateTime _lastReset;
\r
310 private readonly Color[] _color =
\r
312 Color.FromArgb(60, 141, 76), Color.FromArgb(232, 57, 41), Color.FromArgb(136, 204, 120),
\r
313 Color.FromArgb(52, 147, 185), Color.FromArgb(220, 198, 126), Color.FromArgb(168, 111, 76),
\r
314 Color.FromArgb(200, 148, 231), Color.FromArgb(232, 57, 41)
\r
317 public int AcceptMax { get; set; } = 5;
\r
319 public QuestStatus[] Quests => _quests.Values.ToArray();
\r
321 public QuestInfo(ItemInfo itemInfo, BattleInfo battleInfo, Func<DateTime> nowFunc = null)
\r
323 _itemInfo = itemInfo;
\r
324 _battleInfo = battleInfo;
\r
325 if (nowFunc != null)
\r
326 _nowFunc = nowFunc;
\r
329 public void InspectQuestList(dynamic json)
\r
332 if (json.api_list == null)
\r
334 for (var i = 0; i < 2; i++)
\r
336 foreach (var entry in json.api_list)
\r
338 if (entry is double) // -1の場合がある。
\r
341 var id = (int)entry.api_no;
\r
342 var state = (int)entry.api_state;
\r
343 var progress = (int)entry.api_progress_flag;
\r
344 var cat = (int)entry.api_category;
\r
345 var name = (string)entry.api_title;
\r
361 _quests.Remove(id);
\r
367 var count = _countList.GetCount(id);
\r
368 if (count.AdjustCount(progress))
\r
370 _quests[id] = new QuestStatus
\r
376 Progress = progress,
\r
377 Color = cat <= _color.Length ? _color[cat - 1] : Control.DefaultBackColor
\r
382 if (_quests.Count <= AcceptMax)
\r
385 * ほかのPCで任務を達成した場合、任務が消えずに受領した任務の数が_questCountを超えることがある。
\r
386 * その場合はいったん任務をクリアして、現在のページの任務だけを登録し直す。
\r
392 private void ResetQuests()
\r
394 var now = _nowFunc();
\r
395 var daily = now.Date.AddHours(5);
\r
396 if (!(_lastReset < daily && daily <= now))
\r
398 _quests.Clear(); // 前日に未消化のデイリーを消す。
\r
399 _countList.Remove(QuestInterval.Daily);
\r
400 var weekly = now.Date.AddDays(-((6 + (int)now.DayOfWeek) % 7)).AddHours(5);
\r
401 if (_lastReset < weekly && weekly <= now)
\r
402 _countList.Remove(QuestInterval.Weekly);
\r
403 var monthly = new DateTime(now.Year, now.Month, 1, 5, 0, 0);
\r
404 if (_lastReset < monthly && monthly <= now)
\r
405 _countList.Remove(QuestInterval.Monthly);
\r
406 var season = now.Month / 3;
\r
407 var quarterly = new DateTime(now.Year - (season == 0 ? 1 : 0), (season == 0 ? 12 : season * 3), 1, 5, 0, 0);
\r
408 if (_lastReset < quarterly && quarterly <= now)
\r
409 _countList.Remove(QuestInterval.Quarterly);
\r
413 private bool _boss;
\r
416 public void InspectMapStart(dynamic json)
\r
418 if (_quests.TryGetValue(214, out var ago)) // あ号
\r
419 ago.Count.NowArray[0]++;
\r
420 InspectMapNext(json);
\r
423 public void InspectMapNext(dynamic json)
\r
425 _map = (int)json.api_maparea_id * 10 + (int)json.api_mapinfo_no;
\r
426 _boss = (int)json.api_event_id == 5;
\r
429 public void InspectBattleResult(dynamic json)
\r
431 var rank = json.api_win_rank;
\r
432 foreach (var quest in _quests.Values)
\r
434 var count = quest.Count;
\r
435 switch (count.Spec)
\r
437 case QuestSortie sortie:
\r
438 if (count.Id == 216 && !_boss || sortie.Check(rank, _map, _boss))
\r
439 IncrementCount(count);
\r
441 case QuestEnemyType enemyType:
\r
442 var num = enemyType.CountResult(
\r
443 _battleInfo.Result.Enemy.Main.Concat(_battleInfo.Result.Enemy.Guard));
\r
445 AddCount(count, num);
\r
449 if (_quests.TryGetValue(214, out var ago))
\r
451 var array = ago.Count.NowArray;
\r
455 if (QuestSortie.CompareRank(rank, "B") <= 0)
\r
465 if (_quests.TryGetValue(854, out var opz) && _boss)
\r
467 var array = opz.Count.NowArray;
\r
470 case 24 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
474 case 61 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
478 case 63 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
482 case 64 when QuestSortie.CompareRank(rank, "S") <= 0:
\r
490 public void InspectPracticeResult(dynamic json)
\r
492 foreach (var quest in _quests.Values)
\r
494 var count = quest.Count;
\r
495 if (!(count.Spec is QuestPractice practice))
\r
497 if (practice.Check(json.api_win_rank))
\r
498 IncrementCount(count);
\r
502 private readonly int[] _missionId = new int[ShipInfo.FleetCount];
\r
504 public void InspectDeck(dynamic json)
\r
506 foreach (var entry in json)
\r
507 _missionId[(int)entry.api_id - 1] = (int)entry.api_mission[1];
\r
510 public void InspectMissionResult(string request, dynamic json)
\r
512 var values = HttpUtility.ParseQueryString(request);
\r
513 var deck = int.Parse(values["api_deck_id"]);
\r
514 if ((int)json.api_clear_result == 0)
\r
516 foreach (var quest in _quests.Values)
\r
518 var count = quest.Count;
\r
519 if (!(count.Spec is QuestMission mission))
\r
521 if (mission.Check(_missionId[deck - 1]))
\r
522 IncrementCount(count);
\r
526 private void IncrementCount(QuestCount count)
\r
532 private void AddCount(QuestCount count, int value)
\r
534 count.Now += value;
\r
538 private void IncrementCount(int id)
\r
543 private void AddCount(int id, int value)
\r
545 if (_quests.TryGetValue(id, out var quest))
\r
547 quest.Count.Now += value;
\r
552 public void CountNyukyo() => IncrementCount(503);
\r
554 public void CountCharge() => IncrementCount(504);
\r
556 public void CountCreateItem()
\r
558 IncrementCount(605);
\r
559 IncrementCount(607);
\r
562 public void CountCreateShip()
\r
564 IncrementCount(606);
\r
565 IncrementCount(608);
\r
568 public void InspectDestroyShip(string request)
\r
570 AddCount(609, HttpUtility.ParseQueryString(request)["api_ship_id"].Split(',').Length);
\r
573 public void CountRemodelSlot() => IncrementCount(619);
\r
575 public void InspectDestroyItem(string request, dynamic json)
\r
577 var values = HttpUtility.ParseQueryString(request);
\r
578 var items = values["api_slotitem_ids"].Split(',')
\r
579 .Select(id => _itemInfo.GetStatus(int.Parse(id)).Spec.Type).ToArray();
\r
580 IncrementCount(613); // 613: 資源の再利用
\r
581 foreach (var quest in _quests.Values)
\r
583 var count = quest.Count;
\r
584 if (!(count.Spec is QuestDestroyItem destroy))
\r
586 AddCount(count, items.Count(destroy.Check));
\r
588 if (_quests.TryGetValue(675, out var q675))
\r
590 q675.Count.NowArray[0] += items.Count(id => id == 6);
\r
591 q675.Count.NowArray[1] += items.Count(id => id == 21);
\r
594 if (_quests.TryGetValue(676, out var q676))
\r
596 q676.Count.NowArray[0] += items.Count(id => id == 2);
\r
597 q676.Count.NowArray[1] += items.Count(id => id == 4);
\r
598 q676.Count.NowArray[2] += items.Count(id => id == 30);
\r
601 if (_quests.TryGetValue(677, out var q677))
\r
603 q677.Count.NowArray[0] += items.Count(id => id == 3);
\r
604 q677.Count.NowArray[1] += items.Count(id => id == 10);
\r
605 q677.Count.NowArray[2] += items.Count(id => id == 5);
\r
610 public void InspectPowerup(dynamic json)
\r
612 if ((int)json.api_powerup_flag == 0)
\r
614 foreach (var quest in _quests.Values)
\r
616 var count = quest.Count;
\r
617 if (!(count.Spec is QuestPowerup))
\r
619 IncrementCount(count);
\r
623 public void InspectStop(string request)
\r
625 var values = HttpUtility.ParseQueryString(request);
\r
626 _quests.Remove(int.Parse(values["api_quest_id"]));
\r
629 public void InspectClearItemGet(string request)
\r
631 var values = HttpUtility.ParseQueryString(request);
\r
632 var id = int.Parse(values["api_quest_id"]);
\r
633 _countList.Remove(id);
\r
634 _quests.Remove(id);
\r
638 public bool NeedSave { get; private set; }
\r
640 public void SaveState(Status status)
\r
642 status.QuestLastReset = _lastReset;
\r
643 if (_countList == null)
\r
645 status.QuestCountList = _countList.CountList.ToArray();
\r
648 public void LoadState(Status status)
\r
650 _countList.CountList = status.QuestCountList;
\r
651 _lastReset = status.QuestLastReset;
\r