OSDN Git Service

戦況にツールチップで味方の装備を表示する
[kancollesniffer/KancolleSniffer.git] / KancolleSniffer / LogServer.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.Globalization;\r
18 using System.IO;\r
19 using System.Linq;\r
20 using System.Net.Sockets;\r
21 using System.Text;\r
22 using System.Text.RegularExpressions;\r
23 \r
24 namespace KancolleSniffer\r
25 {\r
26     public class LogServer\r
27     {\r
28         private static readonly string IndexDir = AppDomain.CurrentDomain.BaseDirectory;\r
29         private static string _outputDir = AppDomain.CurrentDomain.BaseDirectory;\r
30 \r
31         public static string OutputDir\r
32         {\r
33             set => _outputDir = value;\r
34         }\r
35 \r
36         public static MaterialCount[] MaterialHistory { private get; set; }\r
37 \r
38         public static void Process(Socket client, string requestLine)\r
39         {\r
40             var from = DateTime.MinValue;\r
41             var to = DateTime.MaxValue;\r
42             var timestamp = false;\r
43 \r
44             var request = requestLine.Split(' ');\r
45             if (request.Length != 3)\r
46             {\r
47                 SendError(client, "400 Bad Request");\r
48                 return;\r
49             }\r
50             if (!request[0].StartsWith("GET", StringComparison.OrdinalIgnoreCase))\r
51             {\r
52                 SendError(client, "501 Not Implemented");\r
53                 return;\r
54             }\r
55             var tmp = request[1].Split('?');\r
56             var path = HttpUtility.UrlDecode(tmp[0]);\r
57             if (path == null || !path.StartsWith("/"))\r
58             {\r
59                 SendError(client, "400 Bad Request");\r
60                 return;\r
61             }\r
62             if (tmp.Length == 2)\r
63             {\r
64                 var query = HttpUtility.ParseQueryString(tmp[1]);\r
65                 if (query["from"] != null)\r
66                 {\r
67                     double.TryParse(query["from"], out var tick);\r
68                     from = new DateTime(1970, 1, 1).ToLocalTime().AddSeconds(tick / 1000);\r
69                 }\r
70                 if (query["to"] != null)\r
71                 {\r
72                     double.TryParse(query["to"], out var tick);\r
73                     to = new DateTime(1970, 1, 1).ToLocalTime().AddSeconds(tick / 1000);\r
74                 }\r
75                 if (query["number"] != null)\r
76                     timestamp = query["number"] == "true";\r
77             }\r
78 \r
79             path = path == "/" ? "index.html" : path.Substring(1);\r
80             var full = Path.Combine(IndexDir, path);\r
81             var csv = Path.Combine(_outputDir, path);\r
82             if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && File.Exists(full))\r
83             {\r
84                 SendFile(client, full, "text/html");\r
85                 return;\r
86             }\r
87             if (path.EndsWith(".csv", StringComparison.OrdinalIgnoreCase) && File.Exists(csv))\r
88             {\r
89                 SendFile(client, csv, "text/csv; charset=Shift_JIS");\r
90                 return;\r
91             }\r
92             if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))\r
93             {\r
94                 SendJsonData(client, csv, from, to, timestamp);\r
95                 return;\r
96             }\r
97             if (path.EndsWith(".js", StringComparison.OrdinalIgnoreCase) && File.Exists(full))\r
98             {\r
99                 SendFile(client, full, "application/javascript");\r
100                 return;\r
101             }\r
102             if (path.EndsWith(".pac"))\r
103             {\r
104                 SendProxyPac(client, HttpProxy.LocalPort);\r
105                 return;\r
106             }\r
107             SendError(client, "404 Not Found");\r
108         }\r
109 \r
110         private static void SendError(Socket client, string error)\r
111         {\r
112             using (var writer = new StreamWriter(new MemoryStream(), Encoding.ASCII))\r
113             {\r
114                 writer.Write("HTTP/1.1 {0}\r\n", error);\r
115                 writer.Write("Server: KancolleSniffer\r\n");\r
116                 writer.Write("Date: {0:R}\r\n", DateTime.Now);\r
117                 writer.Write("Connection: close\r\n\r\n");\r
118                 writer.Write("<html><head><title>{0}</title></head>\r\n", error);\r
119                 writer.Write("<body><h4>{0}</h4></body></html>\r\n\r\n", error);\r
120                 writer.Flush();\r
121                 client.Send(((MemoryStream)writer.BaseStream).ToArray());\r
122             }\r
123         }\r
124 \r
125         private static void SendJsonData(Socket client, string path, DateTime from, DateTime to, bool number)\r
126         {\r
127             using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))\r
128             {\r
129                 header.Write("HTTP/1.1 200 OK\r\n");\r
130                 header.Write("Server: KancolleSniffer\r\n");\r
131                 header.Write("Date: {0:R}\r\n", DateTime.Now);\r
132                 header.Write("Content-Type: {0}\r\n", "application/json; charset=Shift_JIS");\r
133                 header.Write("Connection: close\r\n\r\n");\r
134                 header.Flush();\r
135                 client.Send(((MemoryStream)header.BaseStream).ToArray());\r
136             }\r
137             var csv = path.Replace(".json", ".csv");\r
138             var encoding = Encoding.GetEncoding("Shift_JIS");\r
139             client.Send(encoding.GetBytes("{ \"data\": [\n"));\r
140             var battle = false;\r
141             var material = false;\r
142             try\r
143             {\r
144                 if (!File.Exists(csv))\r
145                     return;\r
146                 var records = 0;\r
147                 if (path.EndsWith("遠征報告書.json"))\r
148                 {\r
149                     records = 10;\r
150                 }\r
151                 else if (path.EndsWith("改修報告書.json"))\r
152                 {\r
153                     records = 15;\r
154                 }\r
155                 else if (path.EndsWith("海戦・ドロップ報告書.json"))\r
156                 {\r
157                     records = 39;\r
158                     battle = true;\r
159                 }\r
160                 else if (path.EndsWith("開発報告書.json"))\r
161                 {\r
162                     records = 9;\r
163                 }\r
164                 else if (path.EndsWith("建造報告書.json"))\r
165                 {\r
166                     records = 12;\r
167                 }\r
168                 else if (path.EndsWith("資材ログ.json"))\r
169                 {\r
170                     records = 9;\r
171                     material = true;\r
172                 }\r
173                 else if (path.EndsWith("戦果.json"))\r
174                 {\r
175                     records = 3;\r
176                 }\r
177                 var delimiter = "";\r
178                 foreach (var line in File.ReadLines(csv, encoding).Skip(1))\r
179                 {\r
180                     var data = line.Split(',');\r
181                     if (!DateTime.TryParseExact(data[0], Logger.DateTimeFormat, CultureInfo.InvariantCulture,\r
182                         DateTimeStyles.AssumeLocal, out DateTime date))\r
183                     {\r
184                         // システムが和暦に設定されていて和暦が出力されてしまったケースを救う\r
185                         var wareki = CultureInfo.CreateSpecificCulture("ja-JP");\r
186                         wareki.DateTimeFormat.Calendar = new JapaneseCalendar();\r
187                         if (DateTime.TryParseExact(data[0], Logger.DateTimeFormat, wareki,\r
188                             DateTimeStyles.AssumeLocal, out date))\r
189                         {\r
190                             data[0] = Logger.FormatDateTime(date);\r
191                         }\r
192                         else if (DateTime.TryParse(data[0], CultureInfo.CurrentCulture,\r
193                             DateTimeStyles.AssumeLocal, out date))\r
194                         {\r
195                             data[0] = Logger.FormatDateTime(date);\r
196                         }\r
197                         else\r
198                         {\r
199                             continue;\r
200                         }\r
201                     }\r
202                     if (date < from || to < date)\r
203                         continue;\r
204                     IEnumerable<string> entries = data;\r
205                     if (material)\r
206                         entries = data.Take(9);\r
207                     if (battle)\r
208                         entries = BattleLogProcessor.Process(data);\r
209                     if (entries.Count() != records)\r
210                         continue;\r
211                     if (number)\r
212                     {\r
213                         var stamp = ((date.ToUniversalTime().Ticks -\r
214                                       new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks) /\r
215                                      TimeSpan.TicksPerMillisecond).ToString();\r
216                         client.Send(encoding.GetBytes(delimiter + "[" + stamp + "," +\r
217                                                       string.Join(",", entries.Skip(1)) + "]"));\r
218                     }\r
219                     else\r
220                     {\r
221                         client.Send(encoding.GetBytes(delimiter + "[\"" +\r
222                                                       string.Join("\",\"", entries) + "\"]"));\r
223                     }\r
224                     delimiter = ",\n";\r
225                 }\r
226                 if (material && !number)\r
227                 {\r
228                     client.Send(encoding.GetBytes(delimiter + "[\"" +\r
229                                                   string.Join("\",\"", GetCurrentMaterialRecord()) + "\"]"));\r
230                 }\r
231             }\r
232             finally\r
233             {\r
234                 client.Send(encoding.GetBytes("]}\n"));\r
235             }\r
236         }\r
237 \r
238         private static IEnumerable<string> GetCurrentMaterialRecord()\r
239         {\r
240             return new[] {Logger.FormatDateTime(DateTime.Now)}.\r
241                 Concat(MaterialHistory.Select(c => c.Now.ToString()));\r
242         }\r
243 \r
244         private static void SendFile(Socket client, string path, string mime)\r
245         {\r
246             using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))\r
247             {\r
248                 header.Write("HTTP/1.1 200 OK\r\n");\r
249                 header.Write("Server: KancolleSniffer\r\n");\r
250                 header.Write("Date: {0:R}\r\n", DateTime.Now);\r
251                 header.Write("Content-Length: {0}\r\n", new FileInfo(path).Length);\r
252                 header.Write("Content-Type: {0}\r\n", mime);\r
253                 header.Write("Connection: close\r\n\r\n");\r
254                 header.Flush();\r
255                 client.SendFile(path, ((MemoryStream)header.BaseStream).ToArray(), null,\r
256                     TransmitFileOptions.UseDefaultWorkerThread);\r
257             }\r
258         }\r
259 \r
260         private static void SendProxyPac(Socket client, int port)\r
261         {\r
262             using (var header = new StreamWriter(new MemoryStream(), Encoding.ASCII))\r
263             {\r
264                 header.Write("HTTP/1.1 200 OK\r\n");\r
265                 header.Write("Server: KancolleSniffer\r\n");\r
266                 header.Write("Date: {0:R}\r\n", DateTime.Now);\r
267                 header.Write("Content-Type: application/x-ns-proxy-autoconfig\r\n");\r
268                 header.Write("Connection: close\r\n\r\n");\r
269                 header.Flush();\r
270                 client.Send(((MemoryStream)header.BaseStream).ToArray());\r
271             }\r
272             string pacFile;\r
273             try\r
274             {\r
275                 pacFile = File.ReadAllText("proxy.pac").Replace("8080", port.ToString());\r
276             }\r
277             catch\r
278             {\r
279                 pacFile = "";\r
280             }\r
281             client.Send(Encoding.ASCII.GetBytes(pacFile));\r
282         }\r
283     }\r
284 \r
285     public static class BattleLogProcessor\r
286     {\r
287         public static IEnumerable<string> Process(string[] data)\r
288         {\r
289             if (data.Length == 35)\r
290                 data = data.Concat(Enumerable.Repeat("", 3)).ToArray();\r
291             if (data.Length == 40)\r
292             {\r
293                 data = data.Take(21).Concat(new[] {data[21] + "・" + data[23], data[22] + "・" + data[24]})\r
294                     .Concat(data.Skip(25)).ToArray();\r
295             }\r
296             else if (data.Length != 38)\r
297             {\r
298                 return data;\r
299             }\r
300             if (data[5] == "T字戦(有利)")\r
301                 data[5] = "T字有利";\r
302             if (data[5] == "T字戦(不利)")\r
303                 data[5] = "T字不利";\r
304             if (data[6].EndsWith("航行序列"))\r
305                 data[6] = data[6].Substring(0, 4);\r
306             if (data[7].EndsWith("航行序列"))\r
307                 data[7] = data[7].Substring(0, 4);\r
308             data[37] = ShortenAirBattleResult(data[37]);\r
309             return AddDamagedShip(data);\r
310         }\r
311 \r
312         private static string ShortenAirBattleResult(string result)\r
313         {\r
314             switch (result)\r
315             {\r
316                 case "制空均衡":\r
317                     return "均衡";\r
318                 case "制空権確保":\r
319                     return "確保";\r
320                 case "航空優勢":\r
321                     return "優勢";\r
322                 case "航空劣勢":\r
323                     return "劣勢";\r
324                 case "制空権喪失":\r
325                     return "喪失";\r
326                 default:\r
327                     return "";\r
328             }\r
329         }\r
330 \r
331         private static IEnumerable<string> AddDamagedShip(string[] data)\r
332         {\r
333             var damaged = new List<string>();\r
334             for (var i = 11; i < 11 + 12; i += 2)\r
335             {\r
336                 if (data[i] == "")\r
337                     continue;\r
338                 var ship = data[i] = StripKana(data[i]);\r
339                 var hp = data[i + 1];\r
340                 try\r
341                 {\r
342                     if (ship.Contains("・"))\r
343                     {\r
344                         var ships = ship.Split('・');\r
345                         var hps = hp.Split('・');\r
346                         var nowMax = hps[0].Split('/').Select(int.Parse).ToArray();\r
347                         if (ShipStatus.CalcDamage(nowMax[0], nowMax[1]) == ShipStatus.Damage.Badly)\r
348                             damaged.Add(ships[0]);\r
349                         nowMax = hps[1].Split('/').Select(int.Parse).ToArray();\r
350                         if (ShipStatus.CalcDamage(nowMax[0], nowMax[1]) == ShipStatus.Damage.Badly)\r
351                             damaged.Add(ships[1]);\r
352                     }\r
353                     else\r
354                     {\r
355                         var nowMax = hp.Split('/').Select(int.Parse).ToArray();\r
356                         if (ShipStatus.CalcDamage(nowMax[0], nowMax[1]) == ShipStatus.Damage.Badly)\r
357                             damaged.Add(ship);\r
358                     }\r
359                 }\r
360                 catch (FormatException)\r
361                 {\r
362                     return data;\r
363                 }\r
364             }\r
365             return data.Take(23).Concat(new[] { string.Join("・", damaged) }).Concat(data.Skip(23));\r
366         }\r
367 \r
368         private static readonly Regex Kana = new Regex(@"\([^)]+\)\(", RegexOptions.Compiled);\r
369 \r
370         private static string StripKana(string name)\r
371         {\r
372             return Kana.Replace(name, "(");\r
373         }\r
374     }\r
375 }