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
108 public int Max => Spec.Max;
\r
111 public QuestSpec Spec { get; set; }
\r
113 public bool AdjustCount(int progress)
\r
115 if (!Spec.AdjustCount)
\r
117 if (NowArray != null)
\r
119 if (progress != 100)
\r
121 NowArray = NowArray.Zip(Spec.MaxArray, (n, m) => Max(n, m)).ToArray();
\r
140 var now = Now + Spec.Shift;
\r
141 var max = Max + Spec.Shift;
\r
142 var low = (int)Ceiling(max * progress / 100.0);
\r
143 var high = (int)Ceiling(max * next / 100.0);
\r
146 Now = low - Spec.Shift;
\r
151 Now = high - 1 - Spec.Shift;
\r
159 public class QuestCountList
\r
161 private const QuestInterval Daily = QuestInterval.Daily;
\r
162 private const QuestInterval Weekly = QuestInterval.Weekly;
\r
163 private const QuestInterval Monthly = QuestInterval.Monthly;
\r
164 private const QuestInterval Quarterly = QuestInterval.Quarterly;
\r
167 /// このテーブルは七四式電子観測儀を参考に作成した。
\r
168 /// https://github.com/andanteyk/ElectronicObserver/blob/develop/ElectronicObserver/Data/Quest/QuestProgressManager.cs
\r
170 private static readonly Dictionary<int, QuestSpec> QuestSpecs = new Dictionary<int, QuestSpec>
\r
172 {201, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 201: 敵艦隊を撃滅せよ!
\r
173 {216, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 216: 敵艦隊主力を撃滅せよ!
\r
174 {210, new QuestSortie {Interval = Daily, Max = 10}}, // 210: 敵艦隊を10回邀撃せよ!
\r
175 {211, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {7, 11}}}, // 211: 敵空母を3隻撃沈せよ!
\r
176 {212, new QuestEnemyType {Interval = Daily, Max = 5, EnemyType = new[] {15}}}, // 212: 敵輸送船団を叩け!
\r
177 {218, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {15}}}, // 218: 敵補給艦を3隻撃沈せよ!
\r
178 {226, new QuestSortie {Interval = Daily, Max = 5, Rank = "B", Maps = new[] {21, 22, 23, 24, 25}}}, // 226: 南西諸島海域の制海権を握れ!
\r
179 {230, new QuestEnemyType {Interval = Daily, Max = 6, EnemyType = new[] {13}}}, // 230: 敵潜水艦を制圧せよ!
\r
181 {213, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {15}}}, // 213: 海上通商破壊作戦
\r
182 {214, new QuestSpec {Interval = Weekly, MaxArray = new[] {36, 6, 24, 12}}}, // 214: あ号作戦
\r
183 {220, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {7, 11}}}, // 220: い号作戦
\r
184 {221, new QuestEnemyType {Interval = Weekly, Max = 50, EnemyType = new[] {15}}}, // 221: ろ号作戦
\r
185 {228, new QuestEnemyType {Interval = Weekly, Max = 15, EnemyType = new[] {13}}}, // 228: 海上護衛戦
\r
186 {229, new QuestSortie {Interval = Weekly, Max = 12, Rank = "B", Maps = new[] {41, 42, 43, 44, 45}}}, // 229: 敵東方艦隊を撃滅せよ!
\r
187 {241, new QuestSortie {Interval = Weekly, Max = 5, Rank = "B", Maps = new[] {33, 34, 35}}}, // 241: 敵北方艦隊主力を撃滅せよ!
\r
188 {242, new QuestSortie {Interval = Weekly, Max = 1, Rank = "B", Maps = new[] {44}}}, // 242: 敵東方中枢艦隊を撃破せよ!
\r
189 {243, new QuestSortie {Interval = Weekly, Max = 2, Rank = "B", Maps = new[] {52}}}, // 243: 南方海域珊瑚諸島沖の制空権を握れ!
\r
190 {256, new QuestSortie {Interval = Monthly, Max = 3, Rank = "S", Maps = new[] {61}}}, // 256: 「潜水艦隊」出撃せよ!
\r
191 {261, new QuestSortie {Interval = Weekly, Max = 3, Rank = "A", Maps = new[] {15}}}, // 261: 海上輸送路の安全確保に努めよ!
\r
192 {265, new QuestSortie {Interval = Monthly, Max = 10, Rank = "A", Maps = new[] {15}}}, // 265: 海上護衛強化月間
\r
194 {822, new QuestSortie {Interval = Quarterly, Max = 2, Rank = "S", Maps = new[] {24}}}, // 822: 沖ノ島海域迎撃戦
\r
195 {854, new QuestSpec {Interval = Quarterly, MaxArray = new[] {1, 1, 1, 1}}}, // 854: 戦果拡張任務!「Z作戦」前段作戦
\r
197 {303, new QuestPractice {Interval = Daily, Max = 3, Win = false}}, // 303: 「演習」で練度向上!
\r
198 {304, new QuestPractice {Interval = Daily, Max = 5, Win = true}}, // 304: 「演習」で他提督を圧倒せよ!
\r
199 {302, new QuestPractice {Interval = Weekly, Max = 20, Win = true}}, // 302: 大規模演習
\r
200 {311, new QuestPractice {Interval = Daily, Max = 7, Win = true}}, // 311: 精鋭艦隊演習
\r
202 {402, new QuestMission {Interval = Daily, Max = 3}}, // 402: 「遠征」を3回成功させよう!
\r
203 {403, new QuestMission {Interval = Daily, Max = 10}}, // 403: 「遠征」を10回成功させよう!
\r
204 {404, new QuestMission {Interval = Weekly, Max = 30}}, // 404: 大規模遠征作戦、発令!
\r
205 {410, new QuestMission {Interval = Weekly, Max = 1, Ids = new[] {37, 38}}}, // 410: 南方への輸送作戦を成功させよ!
\r
206 {411, new QuestMission {Interval = Weekly, Max = 6, Shift = 1, Ids = new[] {37, 38}}}, // 411: 南方への鼠輸送を継続実施せよ!
\r
207 {424, new QuestMission {Interval = Monthly, Max = 4, Shift = 1, Ids = new[] {5}}}, // 424: 輸送船団護衛を強化せよ!
\r
209 {503, new QuestSpec {Interval = Daily, Max = 5}}, // 503: 艦隊大整備!
\r
210 {504, new QuestSpec {Interval = Daily, Max = 15}}, // 504: 艦隊酒保祭り!
\r
212 {605, new QuestSpec {Interval = Daily, Max = 1}}, // 605: 新装備「開発」指令
\r
213 {606, new QuestSpec {Interval = Daily, Max = 1}}, // 606: 新造艦「建造」指令
\r
214 {607, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 607: 装備「開発」集中強化!
\r
215 {608, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 608: 艦娘「建造」艦隊強化!
\r
216 {609, new QuestSpec {Interval = Daily, Max = 2}}, // 609: 軍縮条約対応!
\r
217 {619, new QuestSpec {Interval = Daily, Max = 1}}, // 619: 装備の改修強化
\r
219 {613, new QuestSpec {Interval = Weekly, Max = 24}}, // 613: 資源の再利用
\r
220 {638, new QuestDestroyItem {Interval = Weekly, Max = 6, Items = new[] {21}}}, // 638: 対空機銃量産
\r
221 {673, new QuestDestroyItem {Interval = Daily, Max = 4, Items = new[] {1}, Shift = 1}}, // 673: 装備開発力の整備
\r
222 {674, new QuestDestroyItem {Interval = Daily, Max = 3, Items = new[] {21}, Shift = 2}}, // 674: 工廠環境の整備
\r
223 {675, new QuestSpec {Interval = Quarterly, MaxArray = new[] {6, 4}}}, // 675: 運用装備の統合整備
\r
224 {676, new QuestSpec {Interval = Weekly, MaxArray = new[] {3, 3, 1}}}, // 676: 装備開発力の集中整備
\r
226 {702, new QuestPowerup {Interval = Daily, Max = 2}}, // 702: 艦の「近代化改修」を実施せよ!
\r
227 {703, new QuestPowerup {Interval = Weekly, Max = 15}} // 703: 「近代化改修」を進め、戦備を整えよ!
\r
231 private readonly Dictionary<int, QuestCount> _countDict = new Dictionary<int, QuestCount>();
\r
233 public QuestCount GetCount(int id)
\r
235 if (_countDict.TryGetValue(id, out var value))
\r
237 if (QuestSpecs.TryGetValue(id, out var spec))
\r
239 var nowArray = spec.MaxArray?.Select(x => 0).ToArray();
\r
240 return _countDict[id] = new QuestCount
\r
244 NowArray = nowArray,
\r
248 return new QuestCount {Spec = new QuestSpec {AdjustCount = false}};
\r
251 public void Remove(int id)
\r
253 _countDict.Remove(id);
\r
256 public void Remove(QuestInterval interval)
\r
259 _countDict.Where(pair => pair.Value.Spec.Interval == interval).Select(pair => pair.Key).ToArray())
\r
261 _countDict.Remove(id);
\r
265 public IEnumerable<QuestCount> CountList
\r
267 get => _countDict.Values.Where(c => c.Now > 0 || (c.NowArray?.Any(n => n > 0) ?? false));
\r
272 foreach (var count in value)
\r
274 count.Spec = QuestSpecs[count.Id];
\r
275 _countDict[count.Id] = count;
\r
281 public class QuestInfo : IHaveState
\r
283 private readonly SortedDictionary<int, QuestStatus> _quests = new SortedDictionary<int, QuestStatus>();
\r
284 private readonly QuestCountList _countList = new QuestCountList();
\r
285 private readonly ItemInfo _itemInfo;
\r
286 private readonly BattleInfo _battleInfo;
\r
287 private readonly Func<DateTime> _nowFunc = () => DateTime.Now;
\r
288 private DateTime _lastReset;
\r
290 private readonly Color[] _color =
\r
292 Color.FromArgb(60, 141, 76), Color.FromArgb(232, 57, 41), Color.FromArgb(136, 204, 120),
\r
293 Color.FromArgb(52, 147, 185), Color.FromArgb(220, 198, 126), Color.FromArgb(168, 111, 76),
\r
294 Color.FromArgb(200, 148, 231), Color.FromArgb(232, 57, 41)
\r
297 public int AcceptMax { get; set; } = 5;
\r
299 public QuestStatus[] Quests => _quests.Values.ToArray();
\r
301 public QuestInfo(ItemInfo itemInfo, BattleInfo battleInfo, Func<DateTime> nowFunc = null)
\r
303 _itemInfo = itemInfo;
\r
304 _battleInfo = battleInfo;
\r
305 if (nowFunc != null)
\r
306 _nowFunc = nowFunc;
\r
309 public void InspectQuestList(dynamic json)
\r
312 if (json.api_list == null)
\r
314 for (var i = 0; i < 2; i++)
\r
316 foreach (var entry in json.api_list)
\r
318 if (entry is double) // -1の場合がある。
\r
321 var id = (int)entry.api_no;
\r
322 var state = (int)entry.api_state;
\r
323 var progress = (int)entry.api_progress_flag;
\r
324 var cat = (int)entry.api_category;
\r
325 var name = (string)entry.api_title;
\r
341 _quests.Remove(id);
\r
347 var count = _countList.GetCount(id);
\r
348 if (count.AdjustCount(progress))
\r
350 _quests[id] = new QuestStatus
\r
356 Progress = progress,
\r
357 Color = cat <= _color.Length ? _color[cat - 1] : Control.DefaultBackColor
\r
362 if (_quests.Count <= AcceptMax)
\r
365 * ほかのPCで任務を達成した場合、任務が消えずに受領した任務の数が_questCountを超えることがある。
\r
366 * その場合はいったん任務をクリアして、現在のページの任務だけを登録し直す。
\r
372 private void ResetQuests()
\r
374 var now = _nowFunc();
\r
375 var daily = now.Date.AddHours(5);
\r
376 if (!(_lastReset < daily && daily <= now))
\r
378 _quests.Clear(); // 前日に未消化のデイリーを消す。
\r
379 _countList.Remove(QuestInterval.Daily);
\r
380 var weekly = now.Date.AddDays(-((6 + (int)now.DayOfWeek) % 7)).AddHours(5);
\r
381 if (_lastReset < weekly && weekly <= now)
\r
382 _countList.Remove(QuestInterval.Weekly);
\r
383 var monthly = new DateTime(now.Year, now.Month, 1, 5, 0, 0);
\r
384 if (_lastReset < monthly && monthly <= now)
\r
385 _countList.Remove(QuestInterval.Monthly);
\r
386 var season = now.Month / 3;
\r
387 var quarterly = new DateTime(now.Year - (season == 0 ? 1 : 0), (season == 0 ? 12 : season * 3), 1, 5, 0, 0);
\r
388 if (_lastReset < quarterly && quarterly <= now)
\r
389 _countList.Remove(QuestInterval.Quarterly);
\r
393 private bool _boss;
\r
396 public void InspectMapStart(dynamic json)
\r
398 if (_quests.TryGetValue(214, out var ago)) // あ号
\r
399 ago.Count.NowArray[0]++;
\r
400 InspectMapNext(json);
\r
403 public void InspectMapNext(dynamic json)
\r
405 _map = (int)json.api_maparea_id * 10 + (int)json.api_mapinfo_no;
\r
406 _boss = (int)json.api_event_id == 5;
\r
409 public void InspectBattleResult(dynamic json)
\r
411 var rank = json.api_win_rank;
\r
412 foreach (var quest in _quests.Values)
\r
414 var count = quest.Count;
\r
415 switch (count.Spec)
\r
417 case QuestSortie sortie:
\r
418 if (count.Id == 216 && !_boss || sortie.Check(rank, _map, _boss))
\r
419 IncrementCount(count);
\r
421 case QuestEnemyType enemyType:
\r
422 var num = enemyType.CountResult(
\r
423 _battleInfo.Result.Enemy.Main.Concat(_battleInfo.Result.Enemy.Guard));
\r
425 AddCount(count, num);
\r
429 if (_quests.TryGetValue(214, out var ago))
\r
431 var array = ago.Count.NowArray;
\r
435 if (QuestSortie.CompareRank(rank, "B") <= 0)
\r
445 if (_quests.TryGetValue(854, out var opz) && _boss)
\r
447 var array = opz.Count.NowArray;
\r
450 case 24 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
454 case 61 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
458 case 63 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
462 case 64 when QuestSortie.CompareRank(rank, "S") <= 0:
\r
470 public void InspectPracticeResult(dynamic json)
\r
472 foreach (var quest in _quests.Values)
\r
474 var count = quest.Count;
\r
475 if (!(count.Spec is QuestPractice practice))
\r
477 if (practice.Check(json.api_win_rank))
\r
478 IncrementCount(count);
\r
482 private readonly int[] _missionId = new int[ShipInfo.FleetCount];
\r
484 public void InspectDeck(dynamic json)
\r
486 foreach (var entry in json)
\r
487 _missionId[(int)entry.api_id - 1] = (int)entry.api_mission[1];
\r
490 public void InspectMissionResult(string request, dynamic json)
\r
492 var values = HttpUtility.ParseQueryString(request);
\r
493 var deck = int.Parse(values["api_deck_id"]);
\r
494 if ((int)json.api_clear_result == 0)
\r
496 foreach (var quest in _quests.Values)
\r
498 var count = quest.Count;
\r
499 if (!(count.Spec is QuestMission mission))
\r
501 if (mission.Check(_missionId[deck - 1]))
\r
502 IncrementCount(count);
\r
506 private void IncrementCount(QuestCount count)
\r
512 private void AddCount(QuestCount count, int value)
\r
514 count.Now += value;
\r
518 private void IncrementCount(int id)
\r
523 private void AddCount(int id, int value)
\r
525 if (_quests.TryGetValue(id, out var quest))
\r
527 quest.Count.Now += value;
\r
532 public void CountNyukyo() => IncrementCount(503);
\r
534 public void CountCharge() => IncrementCount(504);
\r
536 public void CountCreateItem()
\r
538 IncrementCount(605);
\r
539 IncrementCount(607);
\r
542 public void CountCreateShip()
\r
544 IncrementCount(606);
\r
545 IncrementCount(608);
\r
548 public void InspectDestroyShip(string request)
\r
550 AddCount(609, HttpUtility.ParseQueryString(request)["api_ship_id"].Split(',').Length);
\r
553 public void CountRemodelSlot() => IncrementCount(619);
\r
555 public void InspectDestroyItem(string request, dynamic json)
\r
557 var values = HttpUtility.ParseQueryString(request);
\r
558 var items = values["api_slotitem_ids"].Split(',')
\r
559 .Select(id => _itemInfo.GetStatus(int.Parse(id)).Spec.Type).ToArray();
\r
560 IncrementCount(613); // 613: 資源の再利用
\r
561 foreach (var quest in _quests.Values)
\r
563 var count = quest.Count;
\r
564 if (!(count.Spec is QuestDestroyItem destroy))
\r
566 AddCount(count, items.Count(destroy.Check));
\r
568 if (_quests.TryGetValue(675, out var q675))
\r
570 q675.Count.NowArray[0] += items.Count(id => id == 6);
\r
571 q675.Count.NowArray[1] += items.Count(id => id == 21);
\r
574 if (_quests.TryGetValue(676, out var q676))
\r
576 q676.Count.NowArray[0] += items.Count(id => id == 2);
\r
577 q676.Count.NowArray[1] += items.Count(id => id == 4);
\r
578 q676.Count.NowArray[2] += items.Count(id => id == 30);
\r
583 public void InspectPowerup(dynamic json)
\r
585 if ((int)json.api_powerup_flag == 0)
\r
587 foreach (var quest in _quests.Values)
\r
589 var count = quest.Count;
\r
590 if (!(count.Spec is QuestPowerup))
\r
592 IncrementCount(count);
\r
596 public void InspectStop(string request)
\r
598 var values = HttpUtility.ParseQueryString(request);
\r
599 _quests.Remove(int.Parse(values["api_quest_id"]));
\r
602 public void InspectClearItemGet(string request)
\r
604 var values = HttpUtility.ParseQueryString(request);
\r
605 var id = int.Parse(values["api_quest_id"]);
\r
606 _countList.Remove(id);
\r
607 _quests.Remove(id);
\r
611 public bool NeedSave { get; private set; }
\r
613 public void SaveState(Status status)
\r
615 status.QuestLastReset = _lastReset;
\r
616 if (_countList == null)
\r
618 status.QuestCountList = _countList.CountList.ToArray();
\r
621 public void LoadState(Status status)
\r
623 _countList.CountList = status.QuestCountList;
\r
624 _lastReset = status.QuestLastReset;
\r