OSDN Git Service

装備一覧に基地航空隊への配置状況を表示する
[kancollesniffer/KancolleSniffer.git] / KancolleSniffer / Sniffer.cs
1 // Copyright (C) 2013, 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.Linq;\r
18 \r
19 namespace KancolleSniffer\r
20 {\r
21     public class Sniffer\r
22     {\r
23         private bool _start;\r
24         private readonly ItemInfo _itemInfo = new ItemInfo();\r
25         private readonly MaterialInfo _materialInfo = new MaterialInfo();\r
26         private readonly QuestInfo _questInfo = new QuestInfo();\r
27         private readonly MissionInfo _missionInfo = new MissionInfo();\r
28         private readonly ShipInfo _shipInfo;\r
29         private readonly ConditionTimer _conditionTimer;\r
30         private readonly DockInfo _dockInfo;\r
31         private readonly AkashiTimer _akashiTimer;\r
32         private readonly Achievement _achievement = new Achievement();\r
33         private readonly BattleInfo _battleInfo;\r
34         private readonly Logger _logger;\r
35         private readonly ExMapInfo _exMapInfo = new ExMapInfo();\r
36         private readonly MiscTextInfo _miscTextInfo = new MiscTextInfo();\r
37         private readonly BaseAirCoprs _baseAirCoprs;\r
38         private readonly Status _status = new Status();\r
39         private bool _saveState;\r
40         private readonly List<IHaveState> _haveState;\r
41 \r
42         [Flags]\r
43         public enum Update\r
44         {\r
45             None = 0,\r
46             Error = 1 << 0,\r
47             Start = 1 << 1,\r
48             Item = 1 << 2,\r
49             Ship = 1 << 3,\r
50             Timer = 1 << 4,\r
51             NDock = 1 << 5,\r
52             Mission = 1 << 6,\r
53             QuestList = 1 << 7,\r
54             Battle = 1 << 8,\r
55             All = (1 << 9) - 1\r
56         }\r
57 \r
58         public Sniffer()\r
59         {\r
60             _shipInfo = new ShipInfo(_itemInfo);\r
61             _conditionTimer = new ConditionTimer(_shipInfo);\r
62             _dockInfo = new DockInfo(_shipInfo, _materialInfo);\r
63             _akashiTimer = new AkashiTimer(_shipInfo, _dockInfo);\r
64             _battleInfo = new BattleInfo(_shipInfo, _itemInfo);\r
65             _logger = new Logger(_shipInfo, _itemInfo, _battleInfo);\r
66             _baseAirCoprs = new BaseAirCoprs(_itemInfo);\r
67             _haveState = new List<IHaveState> {_achievement, _materialInfo, _conditionTimer, _exMapInfo};\r
68         }\r
69 \r
70         private void SaveState()\r
71         {\r
72             if (!_saveState)\r
73                 return;\r
74             if (!_haveState.Any(x => x.NeedSave))\r
75                 return;\r
76             foreach (var x in _haveState)\r
77                 x.SaveState(_status);\r
78             _status.Save();\r
79         }\r
80 \r
81         public void LoadState()\r
82         {\r
83             _status.Load();\r
84             foreach (var x in _haveState)\r
85                 x.LoadState(_status);\r
86             _saveState = true;\r
87         }\r
88 \r
89         public Update Sniff(string url, string request, dynamic json)\r
90         {\r
91             if (!json.api_result())\r
92                 return Update.Error;\r
93             if ((int)json.api_result != 1)\r
94                 return Update.None;\r
95             var data = json.api_data() ? json.api_data : new object();\r
96 \r
97             if (url.EndsWith("api_start2"))\r
98             {\r
99                 return ApiStart(data);\r
100             }\r
101             if (!_start)\r
102                 return Update.None;\r
103             if (url.EndsWith("api_port/port"))\r
104                 return ApiPort(data);\r
105             if (url.Contains("member"))\r
106                 return ApiMember(url, json);\r
107             if (url.Contains("kousyou"))\r
108                 return ApiKousyou(url, request, data);\r
109             if (url.Contains("battle"))\r
110                 return ApiBattle(url, request, data);\r
111             return ApiOthers(url, request, data);\r
112         }\r
113 \r
114         private Update ApiStart(dynamic data)\r
115         {\r
116             _shipInfo.InspectMaster(data);\r
117             _missionInfo.InspectMaster(data.api_mst_mission);\r
118             _itemInfo.InspectMaster(data);\r
119             _exMapInfo.ResetIfNeeded();\r
120             _start = true;\r
121             return Update.Start;\r
122         }\r
123 \r
124         private Update ApiPort(dynamic data)\r
125         {\r
126             _itemInfo.InspectBasic(data.api_basic);\r
127             _materialInfo.InspectMaterial(data.api_material, true);\r
128             _logger.InspectBasic(data.api_basic);\r
129             _logger.InspectMaterial(data.api_material);\r
130             _shipInfo.InspectShip(data);\r
131             _shipInfo.ClearBadlyDamagedShips();\r
132             _conditionTimer.CalcRegenTime();\r
133             _missionInfo.InspectDeck(data.api_deck_port);\r
134             _dockInfo.InspectNDock(data.api_ndock);\r
135             _akashiTimer.Port();\r
136             _achievement.InspectBasic(data.api_basic);\r
137             if (data.api_parallel_quest_count()) // 昔のログにはないので\r
138                 _questInfo.QuestCount = (int)data.api_parallel_quest_count;\r
139             if (data.api_event_object())\r
140                 _baseAirCoprs.InspectEventObject(data.api_event_object);\r
141             _battleInfo.CleanupResult();\r
142             _battleInfo.InBattle = false;\r
143             _shipInfo.ClearEscapedShips();\r
144             _miscTextInfo.ClearIfNeeded();\r
145             SaveState();\r
146             return Update.All;\r
147         }\r
148 \r
149         private Update ApiMember(string url, dynamic json)\r
150         {\r
151             var data = json.api_data() ? json.api_data : new object();\r
152 \r
153             if (url.EndsWith("api_get_member/require_info"))\r
154             {\r
155                 _itemInfo.InspectSlotItem(data.api_slot_item, true);\r
156                 _dockInfo.InspectKDock(data.api_kdock);\r
157                 return Update.Timer;\r
158             }\r
159             if (url.EndsWith("api_get_member/basic"))\r
160             {\r
161                 _itemInfo.InspectBasic(data);\r
162                 _logger.InspectBasic(data);\r
163                 return Update.None;\r
164             }\r
165             if (url.EndsWith("api_get_member/slot_item"))\r
166             {\r
167                 _itemInfo.InspectSlotItem(data, true);\r
168                 return Update.Item;\r
169             }\r
170             if (url.EndsWith("api_get_member/kdock"))\r
171             {\r
172                 _dockInfo.InspectKDock(data);\r
173                 _logger.InspectKDock(data);\r
174                 return Update.Timer;\r
175             }\r
176             if (url.EndsWith("api_get_member/ndock"))\r
177             {\r
178                 _dockInfo.InspectNDock(data);\r
179                 _conditionTimer.CheckCond();\r
180                 _akashiTimer.CheckFleet();\r
181                 return Update.NDock | Update.Timer | Update.Ship;\r
182             }\r
183             if (url.EndsWith("api_get_member/questlist"))\r
184             {\r
185                 _questInfo.InspectQuestList(data);\r
186                 return Update.QuestList;\r
187             }\r
188             if (url.EndsWith("api_get_member/deck"))\r
189             {\r
190                 _shipInfo.InspectDeck(data);\r
191                 _missionInfo.InspectDeck(data);\r
192                 _akashiTimer.CheckFleet();\r
193                 return Update.Mission | Update.Timer;\r
194             }\r
195             if (url.EndsWith("api_get_member/ship2"))\r
196             {\r
197                 // ここだけjsonなので注意\r
198                 _shipInfo.InspectShip(json);\r
199                 _akashiTimer.CheckFleet();\r
200                 _battleInfo.InBattle = false;\r
201                 return Update.Item | Update.Ship | Update.Battle;\r
202             }\r
203             if (url.EndsWith("api_get_member/ship_deck"))\r
204             {\r
205                 _shipInfo.InspectShip(data);\r
206                 _akashiTimer.CheckFleet();\r
207                 _battleInfo.InBattle = false;\r
208                 return Update.Ship | Update.Battle;\r
209             }\r
210             if (url.EndsWith("api_get_member/ship3"))\r
211             {\r
212                 _shipInfo.InspectShip(data);\r
213                 _akashiTimer.CheckFleet();\r
214                 _conditionTimer.CheckCond();\r
215                 return Update.Ship;\r
216             }\r
217             if (url.EndsWith("api_get_member/material"))\r
218             {\r
219                 _materialInfo.InspectMaterial(data);\r
220                 return Update.Item;\r
221             }\r
222             if (url.EndsWith("api_get_member/mapinfo"))\r
223             {\r
224                 _exMapInfo.InspectMapInfo(data);\r
225                 _miscTextInfo.InspectMapInfo(data);\r
226                 return Update.Item;\r
227             }\r
228             if (url.EndsWith("api_req_member/get_practice_enemyinfo"))\r
229             {\r
230                 _miscTextInfo.InspectPracticeEnemyInfo(data);\r
231                 return Update.Item;\r
232             }\r
233             if (url.EndsWith("api_get_member/preset_deck"))\r
234             {\r
235                 _shipInfo.InspectPresetDeck(data);\r
236                 return Update.None;\r
237             }\r
238             if (url.EndsWith("api_get_member/base_air_corps"))\r
239             {\r
240                 _baseAirCoprs.Inspect(data);\r
241                 return Update.Ship;\r
242             }\r
243             return Update.None;\r
244         }\r
245 \r
246         private Update ApiKousyou(string url, string request, dynamic data)\r
247         {\r
248             if (url.EndsWith("api_req_kousyou/createitem"))\r
249             {\r
250                 _itemInfo.InspectCreateItem(data);\r
251                 _materialInfo.InspectCreateIem(data);\r
252                 _logger.InspectCreateItem(request, data);\r
253                 return Update.Item;\r
254             }\r
255             if (url.EndsWith("api_req_kousyou/getship"))\r
256             {\r
257                 _itemInfo.InspectGetShip(data);\r
258                 _shipInfo.InspectShip(data);\r
259                 _dockInfo.InspectKDock(data.api_kdock);\r
260                 _conditionTimer.CheckCond();\r
261                 return Update.Item | Update.Timer;\r
262             }\r
263             if (url.EndsWith("api_req_kousyou/destroyship"))\r
264             {\r
265                 _shipInfo.InspectDestroyShip(request, data);\r
266                 _materialInfo.InspectDestroyShip(data);\r
267                 _conditionTimer.CheckCond();\r
268                 _akashiTimer.CheckFleet();\r
269                 return Update.Item | Update.Ship;\r
270             }\r
271             if (url.EndsWith("api_req_kousyou/destroyitem2"))\r
272             {\r
273                 _itemInfo.InspectDestroyItem(request, data);\r
274                 _materialInfo.InspectDestroyItem(data);\r
275                 return Update.Item;\r
276             }\r
277             if (url.EndsWith("api_req_kousyou/remodel_slot"))\r
278             {\r
279                 _logger.SetCurrentMaterial(_materialInfo.Current);\r
280                 _logger.InspectRemodelSlot(request, data); // 資材の差が必要なので_materialInfoより前\r
281                 _itemInfo.InspectRemodelSlot(data);\r
282                 _materialInfo.InspectRemodelSlot(data);\r
283                 return Update.Item;\r
284             }\r
285             if (url.EndsWith("api_req_kousyou/createship"))\r
286             {\r
287                 _logger.InspectCreateShip(request);\r
288                 return Update.None;\r
289             }\r
290             if (url.EndsWith("api_req_kousyou/createship_speedchange"))\r
291             {\r
292                 _dockInfo.InspectCreateShipSpeedChange(request);\r
293                 return Update.Timer;\r
294             }\r
295             return Update.None;\r
296         }\r
297 \r
298         private Update ApiBattle(string url, string request, dynamic data)\r
299         {\r
300             if (IsNormalBattleAPI(url))\r
301             {\r
302                 _battleInfo.InspectBattle(data, url);\r
303                 _logger.InspectBattle(data);\r
304                 return Update.Ship | Update.Battle;\r
305             }\r
306             if (url.EndsWith("api_req_practice/battle") || url.EndsWith("api_req_practice/midnight_battle"))\r
307             {\r
308                 if (url.EndsWith("/battle"))\r
309                 {\r
310                     _shipInfo.InspectMapStart(request); // 演習を出撃中とみなす\r
311                     _conditionTimer.InvalidateCond();\r
312                     _miscTextInfo.ClearFlag = true;\r
313                 }\r
314                 _battleInfo.InspectBattle(data, url);\r
315                 return Update.Ship | Update.Battle | Update.Timer;\r
316             }\r
317             if (url.EndsWith("api_req_sortie/battleresult"))\r
318             {\r
319                 _battleInfo.InspectBattleResult(data);\r
320                 _exMapInfo.InspectBattleResult(data);\r
321                 _logger.InspectBattleResult(data);\r
322                 return Update.Ship;\r
323             }\r
324             if (url.EndsWith("api_req_practice/battle_result"))\r
325             {\r
326                 _battleInfo.InspectPracticeResult(data);\r
327                 return Update.Ship;\r
328             }\r
329             if (IsCombinedBattleAPI(url))\r
330             {\r
331                 _battleInfo.InspectCombinedBattle(data, url);\r
332                 _logger.InspectBattle(data);\r
333                 return Update.Ship | Update.Battle;\r
334             }\r
335             if (url.EndsWith("api_req_combined_battle/battleresult"))\r
336             {\r
337                 _battleInfo.InspectCombinedBattleResult(data);\r
338                 _logger.InspectBattleResult(data);\r
339                 return Update.Ship;\r
340             }\r
341             if (url.EndsWith("api_req_combined_battle/goback_port"))\r
342             {\r
343                 _battleInfo.CauseCombinedBattleEscape();\r
344                 return Update.Ship;\r
345             }\r
346             return Update.None;\r
347         }\r
348 \r
349         private bool IsNormalBattleAPI(string url)\r
350         {\r
351             return url.EndsWith("api_req_sortie/battle") ||\r
352                    url.EndsWith("api_req_sortie/airbattle") ||\r
353                    url.EndsWith("api_req_sortie/ld_airbattle") ||\r
354                    url.EndsWith("api_req_battle_midnight/battle") ||\r
355                    url.EndsWith("api_req_battle_midnight/sp_midnight");\r
356         }\r
357 \r
358         private bool IsCombinedBattleAPI(string url)\r
359         {\r
360             return url.EndsWith("api_req_combined_battle/battle") ||\r
361                    url.EndsWith("api_req_combined_battle/airbattle") ||\r
362                    url.EndsWith("api_req_combined_battle/ld_airbattle") ||\r
363                    url.EndsWith("api_req_combined_battle/battle_water") ||\r
364                    url.EndsWith("api_req_combined_battle/midnight_battle") ||\r
365                    url.EndsWith("api_req_combined_battle/sp_midnight");\r
366         }\r
367 \r
368         private Update ApiOthers(string url, string request, dynamic data)\r
369         {\r
370             if (url.EndsWith("api_req_hensei/change"))\r
371             {\r
372                 _shipInfo.InspectChange(request);\r
373                 _akashiTimer.InspectChange(request);\r
374                 return Update.Ship;\r
375             }\r
376             if (url.EndsWith("api_req_hensei/preset_select"))\r
377             {\r
378                 _shipInfo.InspectDeck(new[] {data});\r
379                 _akashiTimer.CheckFleet();\r
380                 return Update.Ship;\r
381             }\r
382             if (url.EndsWith("api_req_hensei/preset_register"))\r
383             {\r
384                 _shipInfo.InspectPresetRegister(data);\r
385                 return Update.None;\r
386             }\r
387             if (url.EndsWith("api_req_hensei/preset_delete"))\r
388             {\r
389                 _shipInfo.InspectPresetDelete(request);\r
390                 return Update.Timer;\r
391             }\r
392             if (url.EndsWith("api_req_hensei/combined"))\r
393             {\r
394                 _shipInfo.InspectCombined(request);\r
395                 return Update.Ship;\r
396             }\r
397             if (url.EndsWith("api_req_hokyu/charge"))\r
398             {\r
399                 _shipInfo.InspectCharge(data);\r
400                 _materialInfo.InspectCharge(data);\r
401                 return Update.Item | Update.Ship;\r
402             }\r
403             if (url.EndsWith("api_req_kaisou/powerup"))\r
404             {\r
405                 _shipInfo.InspectPowerup(request, data);\r
406                 _conditionTimer.CheckCond();\r
407                 _akashiTimer.CheckFleet();\r
408                 return Update.Item | Update.Ship;\r
409             }\r
410             if (url.EndsWith("api_req_kaisou/slot_exchange_index"))\r
411             {\r
412                 _shipInfo.InspectSlotExchange(request, data);\r
413                 return Update.Ship;\r
414             }\r
415             if (url.EndsWith("api_req_kaisou/slot_deprive"))\r
416             {\r
417                 _shipInfo.InspectSlotDeprive(data);\r
418                 return Update.Ship;\r
419             }\r
420             if (url.EndsWith("api_req_nyukyo/start"))\r
421             {\r
422                 _dockInfo.InspectNyukyo(request);\r
423                 _conditionTimer.CheckCond();\r
424                 _akashiTimer.CheckFleet();\r
425                 return Update.Item | Update.Ship;\r
426             }\r
427             if (url.EndsWith("api_req_nyukyo/speedchange"))\r
428             {\r
429                 _dockInfo.InspectSpeedChange(request);\r
430                 _conditionTimer.CheckCond();\r
431                 return Update.NDock | Update.Timer | Update.Ship;\r
432             }\r
433             if (url.EndsWith("api_req_map/start"))\r
434             {\r
435                 _shipInfo.InspectMapStart(request); // 出撃中判定が必要なので_conditionTimerより前\r
436                 _conditionTimer.InvalidateCond();\r
437                 _exMapInfo.InspectMapStart(data);\r
438                 _logger.InspectMapStart(data);\r
439                 _miscTextInfo.ClearFlag = true;\r
440                 return Update.Timer | Update.Ship;\r
441             }\r
442             if (url.EndsWith("api_req_map/next"))\r
443             {\r
444                 _battleInfo.InspectMapNext(request);\r
445                 _exMapInfo.InspectMapNext(data);\r
446                 _logger.InspectMapNext(data);\r
447                 return Update.None;\r
448             }\r
449             if (url.EndsWith("api_req_mission/result"))\r
450             {\r
451                 _materialInfo.InspectMissionResult(data);\r
452                 _logger.InspectMissionResult(data);\r
453                 return Update.Item;\r
454             }\r
455             if (url.EndsWith("api_req_quest/stop"))\r
456             {\r
457                 _questInfo.InspectStop(request);\r
458                 return Update.QuestList;\r
459             }\r
460             if (url.EndsWith("api_req_quest/clearitemget"))\r
461             {\r
462                 _questInfo.InspectClearItemGet(request);\r
463                 return Update.QuestList;\r
464             }\r
465             if (url.EndsWith("api_req_air_corps/supply"))\r
466             {\r
467                 _materialInfo.InspectAirCorpsSupply(data);\r
468                 return Update.Item;\r
469             }\r
470             if (url.EndsWith("api_req_air_corps/set_plane"))\r
471             {\r
472                 _materialInfo.InspectAirCorpsSetPlane(data);\r
473                 _baseAirCoprs.InspectSetPlane(request, data);\r
474                 return Update.Item | Update.Ship;\r
475             }\r
476             if (url.EndsWith("api_req_air_corps/set_action"))\r
477             {\r
478                 _baseAirCoprs.InspectSetAction(request);\r
479                 return Update.Ship;\r
480             }\r
481             return Update.None;\r
482         }\r
483 \r
484         public NameAndTimer[] NDock => _dockInfo.NDock;\r
485 \r
486         public RingTimer[] KDock => _dockInfo.KDock;\r
487 \r
488         public ItemInfo Item => _itemInfo;\r
489 \r
490         public MaterialInfo Material => _materialInfo;\r
491 \r
492         public QuestStatus[] Quests => _questInfo.Quests;\r
493 \r
494         public NameAndTimer[] Missions => _missionInfo.Missions;\r
495 \r
496         public DateTime GetConditionTimer(int fleet) => _conditionTimer.GetTimer(fleet);\r
497 \r
498         public int[] GetConditionNotice() => _conditionTimer.GetNotice();\r
499 \r
500         public ShipStatus[] GetShipStatuses(int fleet) => _shipInfo.GetShipStatuses(fleet);\r
501 \r
502         public int[] GetDeck(int fleet) => _shipInfo.GetDeck(fleet);\r
503 \r
504         public int CombinedFleetType => _shipInfo.CombinedFleetType;\r
505 \r
506         public ChargeStatus[] ChargeStatuses => _shipInfo.ChargeStatuses;\r
507 \r
508         public int[] GetFighterPower(int fleet) => _shipInfo.GetFighterPower(fleet);\r
509 \r
510         public double GetContactTriggerRate(int fleet) => _shipInfo.GetContactTriggerRate(fleet);\r
511 \r
512         public double GetFleetLineOfSights(int fleet) => _shipInfo.GetLineOfSights(fleet);\r
513 \r
514         public ShipStatus[] RepairList => _shipInfo.GetRepairList(_dockInfo);\r
515 \r
516         public ShipStatus[] ShipList => _shipInfo.ShipList;\r
517 \r
518         public string[] BadlyDamagedShips => _shipInfo.BadlyDamagedShips;\r
519 \r
520         public ItemStatus[] ItemList\r
521         {\r
522             get\r
523             {\r
524                 _itemInfo.ClearHolder();\r
525                 _shipInfo.SetItemHolder();\r
526                 _baseAirCoprs.SetItemHolder();\r
527                 return _itemInfo.ItemList;\r
528             }\r
529         }\r
530 \r
531         public AkashiTimer AkashiTimer => _akashiTimer;\r
532 \r
533         public Achievement Achievement => _achievement;\r
534 \r
535         public BattleInfo Battle => _battleInfo;\r
536 \r
537         public ExMapInfo ExMap => _exMapInfo;\r
538 \r
539         public string MiscText => _miscTextInfo.Text;\r
540 \r
541         public BaseAirCoprs.AirCorpsInfo[] BaseAirCorps => _baseAirCoprs.AirCorps;\r
542 \r
543         public void SetLogWriter(Action<string, string, string> writer, Func<DateTime> nowFunc)\r
544         {\r
545             _logger.SetWriter(writer, nowFunc);\r
546         }\r
547 \r
548         public void SkipMaster()\r
549         {\r
550             _start = true;\r
551         }\r
552 \r
553         public void EnableLog(LogType type)\r
554         {\r
555             _logger.EnableLog(type);\r
556         }\r
557 \r
558         public int MaterialLogInterval\r
559         {\r
560             set { _logger.MaterialLogInterval = value; }\r
561         }\r
562 \r
563         public string LogOutputDir\r
564         {\r
565             set { _logger.OutputDir = value; }\r
566         }\r
567     }\r
568 \r
569     public class NameAndTimer\r
570     {\r
571         public string Name { get; set; }\r
572         public RingTimer Timer { get; set; }\r
573 \r
574         public NameAndTimer()\r
575         {\r
576             Timer = new RingTimer();\r
577         }\r
578     }\r
579 \r
580     public class RingTimer\r
581     {\r
582         private readonly TimeSpan _spare;\r
583 \r
584         public TimeSpan Rest { get; private set; }\r
585 \r
586         public bool IsFinished => EndTime != DateTime.MinValue && Rest <= _spare;\r
587 \r
588         public DateTime EndTime { get; private set; }\r
589 \r
590         public bool NeedRing { get; set; }\r
591 \r
592         public RingTimer(int spare = 60)\r
593         {\r
594             _spare = TimeSpan.FromSeconds(spare);\r
595         }\r
596 \r
597         public void SetEndTime(double time)\r
598         {\r
599             SetEndTime((int)time == 0\r
600                 ? DateTime.MinValue\r
601                 : new DateTime(1970, 1, 1).ToLocalTime().AddSeconds(time / 1000));\r
602         }\r
603 \r
604         public void SetEndTime(DateTime time)\r
605         {\r
606             EndTime = time;\r
607         }\r
608 \r
609         public void Update()\r
610         {\r
611             if (EndTime == DateTime.MinValue)\r
612             {\r
613                 Rest = TimeSpan.Zero;\r
614                 return;\r
615             }\r
616             var prev = Rest;\r
617             Rest = EndTime - DateTime.Now;\r
618             if (Rest < TimeSpan.Zero)\r
619                 Rest = TimeSpan.Zero;\r
620             if (prev > _spare && _spare >= Rest)\r
621                 NeedRing = true;\r
622         }\r
623 \r
624         public string ToString(bool finish = false)\r
625             => EndTime == DateTime.MinValue\r
626                 ? ""\r
627                 : finish ? EndTime.ToString(@"dd\ HH\:mm") : $@"{(int)Rest.TotalHours:d2}:{Rest:mm\:ss}";\r
628     }\r
629 }