OSDN Git Service

Split Renderers (W.I.P.)
[automap/automap.git] / Automap / Subsystems / AutomapSystem.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5
6 using System.IO;
7 using System.Linq;
8 using System.Text;
9 using System.Text.RegularExpressions;
10 using System.Threading;
11 using System.Web.UI;
12
13 using Hjg.Pngcs;
14 using Hjg.Pngcs.Chunks;
15
16 using Vintagestory.API.Client;
17 using Vintagestory.API.Common;
18 using Vintagestory.API.MathTools;
19
20
21
22 namespace Automap
23 {
24         public class AutomapSystem
25         {
26                 private Thread cartographer_thread;
27                 private ICoreClientAPI ClientAPI { get; set; }
28                 private ILogger Logger { get; set; }
29                 private IChunkRenderer ChunkRenderer { get; set; }
30
31                 private const string _mapPath = @"Maps";
32                 private const string _chunkPath = @"Chunks";
33                 private const string _domain = @"automap";
34                 private const string chunkFile_filter = @"*_*.png";
35                 private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+).png", RegexOptions.Singleline);
36
37                 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150 );
38                 private ColumnsMetadata chunkTopMetadata;
39                 private PointsOfInterest POIs;
40
41                 internal Dictionary<int, Designator> BlockID_Designators { get; private set;}
42                 internal bool Enabled { get; set; }
43                 //Run status, Chunks processed, stats, center of map....
44                 internal uint nullChunkCount;
45                 internal uint updatedChunksTotal;
46                 internal Vec2i startChunkColumn;
47
48                 private readonly int chunkSize;
49                 private string path;
50                 private IAsset stylesFile;
51
52
53                 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger)
54                 {
55                 this.ClientAPI = clientAPI;
56                 this.Logger = logger;
57                 chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
58                 ClientAPI.Event.LevelFinalize += EngageAutomap;
59
60                 //TODO:Choose which one from GUI 
61                 this.ChunkRenderer = new StandardRenderer(clientAPI, logger);
62                 }
63
64
65                 #region Internals
66                 private void EngageAutomap( )
67                 {
68                 path = ClientAPI.GetOrCreateDataPath(_mapPath);
69                 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
70
71                 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
72                 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
73
74                 Prefill_POI_Designators( );
75                 startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / chunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z / chunkSize));
76                 chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
77
78                 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
79                 Reload_Metadata( );
80
81                 ClientAPI.Event.ChunkDirty += ChunkAChanging;
82
83                 cartographer_thread = new Thread(Cartographer);
84                 cartographer_thread.Name = "Cartographer";
85                 cartographer_thread.Priority = ThreadPriority.Lowest;
86                 cartographer_thread.IsBackground = true;
87
88                 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
89                 }
90
91                 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
92                 {                       
93                 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
94
95                         columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
96                 }
97
98                 private void AwakenCartographer(float delayed)
99                 {
100
101                 if (Enabled && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true)) {
102                 #if DEBUG
103                 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
104                 #endif
105
106                 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
107                 cartographer_thread.Start( );
108                 }
109                 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
110                 //Time to (re)write chunk shards
111                 cartographer_thread.Interrupt( );
112                 }
113                 ClientAPI.TriggerChatMessage($"Automap {updatedChunksTotal} Updates - MAX (N:{chunkTopMetadata.North_mostChunk},S:{chunkTopMetadata.South_mostChunk},E:{chunkTopMetadata.East_mostChunk}, W:{chunkTopMetadata.West_mostChunk} - TOTAL: {chunkTopMetadata.Count})");
114                 }
115
116                 }
117
118
119                 private void Cartographer( )
120                 {
121         wake:
122                 Logger.VerboseDebug("Cartographer thread awoken");
123
124                 try {
125                 uint ejectedItem = 0;
126                 uint updatedChunks = 0;
127
128                 //-- Should dodge enumerator changing underfoot....at a cost.
129                 if (!columnCounter.IsEmpty) {
130                 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
131                 foreach (var mostActiveCol in tempSet) {
132
133                 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
134
135                 if (mapChunk == null) {
136                 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
137                 nullChunkCount++;
138                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem );
139                 continue;
140                 }
141                 
142                 ColumnMeta chunkMeta = CreateColumnMetadata(mostActiveCol,mapChunk);
143                 PngWriter pngWriter = SetupPngImage(mostActiveCol.Key, chunkMeta);
144                 ProcessChunkBlocks(mostActiveCol.Key, mapChunk, chunkMeta);
145
146                 uint updatedPixels = 0;
147
148                 ChunkRenderer.GenerateChunkPngShard(mostActiveCol.Key, mapChunk, chunkMeta, pngWriter , out updatedPixels);
149                 
150                 if (updatedPixels > 0) {                
151                 
152                 #if DEBUG
153                 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
154                 #endif
155                 updatedChunks++;
156                 chunkTopMetadata.Update(chunkMeta);
157                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
158                 }
159                 else {
160                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
161                 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
162                 }
163
164                 }
165                 }
166
167                 if (updatedChunks > 0) {
168                 //What about chunk updates themselves; a update bitmap isn't kept...
169                 updatedChunksTotal += updatedChunks;
170                 GenerateMapHTML( );
171                 updatedChunks = 0;
172                 }
173
174                 //Then sleep until interupted again, and repeat
175
176                 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
177
178                 Thread.Sleep(Timeout.Infinite);
179
180                 } catch (ThreadInterruptedException) {
181
182                 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
183                 goto wake;
184
185                 } catch (ThreadAbortException) {
186                 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
187
188                 } finally {
189                 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
190                 }
191                 }
192
193                 private void Prefill_POI_Designators( )
194                 {
195                 this.POIs = new PointsOfInterest( );
196                 this.BlockID_Designators = new Dictionary<int, Designator>( );
197
198                 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
199
200                 var standardDesignators = new List<Designator>{
201                                 DefaultDesignators.Roads,
202                 DefaultDesignators.GroundSigns,
203                 DefaultDesignators.WallSigns,
204                 DefaultDesignators.PostSigns,
205                                 };
206
207                 Install_POI_Designators(standardDesignators);
208                 }
209
210                 private void Install_POI_Designators(ICollection<Designator> designators)
211                 {
212                 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
213                 foreach (var designator in designators) {                               
214                         var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
215                                 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
216                         foreach (var entry in blockIDs) {
217                         BlockID_Designators.Add(entry.Key, designator);
218                         }
219                 }
220                 this.ChunkRenderer.BlockID_Designators = BlockID_Designators;
221                 }
222
223                 //TODO: Convert to RAZOR model
224                 private void GenerateMapHTML( )
225                 {
226                 string mapFilename = Path.Combine(path, "Automap.html");
227
228                 int TopNorth = chunkTopMetadata.North_mostChunk;
229                 int TopSouth = chunkTopMetadata.South_mostChunk;
230                 int TopEast = chunkTopMetadata.East_mostChunk;
231                 int TopWest = chunkTopMetadata.West_mostChunk;
232
233                 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))) {
234                 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
235                 tableWriter.BeginRender( );
236                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
237
238                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
239                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
240                 tableWriter.WriteEncodedText("Generated Automap");
241                 tableWriter.RenderEndTag( );
242                 //CSS  style  here
243                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
244                 tableWriter.Write(stylesFile.ToText( ));
245                 tableWriter.RenderEndTag( );//</style>
246
247                 //## JSON map-state data ######################
248                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
249                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Script);
250
251                 tableWriter.Write("var available_images = [");
252
253                 foreach (var shard in this.chunkTopMetadata) {
254                 tableWriter.Write("{{X:{0},Y:{1} }}, ", shard.Location.X, shard.Location.Y);
255                 }
256
257                 tableWriter.Write(" ];\n");
258
259                 tableWriter.RenderEndTag( );
260
261                 tableWriter.RenderEndTag( );
262
263                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
264                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
265                 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
266                 tableWriter.RenderEndTag( );
267                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
268                 tableWriter.WriteEncodedText($"W:{TopWest}, E: {TopEast}, N:{TopNorth}, S:{TopSouth} ");
269                 tableWriter.RenderEndTag( );
270                 tableWriter.WriteLine( );
271                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
272                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
273                 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");             
274                 tableWriter.RenderEndTag( );
275
276                 //################ X-Axis <thead> #######################
277                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
278                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
279
280                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
281                 tableWriter.Write("N, W");
282                 tableWriter.RenderEndTag( );
283
284                 for (int xAxisT = TopWest; xAxisT <= TopEast; xAxisT++) {
285                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
286                 tableWriter.Write(xAxisT);
287                 tableWriter.RenderEndTag( );
288                 }
289
290                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
291                 tableWriter.Write("N, E");
292                 tableWriter.RenderEndTag( );
293                 
294                 tableWriter.RenderEndTag( );
295                 tableWriter.RenderEndTag( );
296                 //###### </thead> ################################
297
298                 //###### <tbody> - Chunk rows & Y-axis cols
299                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
300
301                 //######## <tr> for every vertical row
302                 for (int yAxis = TopNorth; yAxis <= TopSouth; yAxis++) {
303                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
304                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
305                 tableWriter.Write(yAxis);//legend: Y-axis
306                 tableWriter.RenderEndTag( );
307
308                 for (int xAxis = TopWest; xAxis <= TopEast; xAxis++) {
309                 //###### <td>  #### for chunk shard 
310                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
311                 var colLoc = new Vec2i(xAxis, yAxis);
312                 if (chunkTopMetadata.Contains( colLoc)){
313                 ColumnMeta meta = chunkTopMetadata[colLoc];
314                 //Tooltip first                                 
315                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltip");
316                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Div);
317
318                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");          
319                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
320                 tableWriter.RenderEndTag( );
321                 // <span class="tooltiptext">Tooltip text
322                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltiptext");
323                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Span);
324
325                 StringBuilder tooltipText = new StringBuilder( );
326                 tooltipText.Append($"{meta.Location.PrettyCoords(ClientAPI)} ");
327                 tooltipText.Append($" Max-Height: {meta.YMax}, Temp: {meta.Temperature.ToString("F1")} " );
328                 tooltipText.Append($" Rainfall: {meta.Rainfall.ToString("F1")}, ");
329                 tooltipText.Append($" Shrubs: {meta.ShrubDensity.ToString("F1")}, ");
330                 tooltipText.Append($" Forest: {meta.ForestDensity.ToString("F1")}, ");
331                 tooltipText.Append($" Fertility: {meta.Fertility.ToString("F1")}, ");
332
333                 if (meta.RockRatio != null) {
334                 foreach (KeyValuePair<int, uint> blockID in meta.RockRatio) {
335                 var block = ClientAPI.World.GetBlock(blockID.Key);
336                 tooltipText.AppendFormat(" {0} × {1},\t", block.Code.GetName( ), meta.RockRatio[blockID.Key]);
337                 }
338                 }
339
340                 tableWriter.WriteEncodedText(tooltipText.ToString() );
341                 
342                 tableWriter.RenderEndTag( );//</span>
343                                                                                 
344
345                 tableWriter.RenderEndTag( );//</div> --tooltip enclosure
346                 }
347                 else {
348                 tableWriter.Write("?");
349                 }       
350
351                 tableWriter.RenderEndTag( );
352                 }//############ </td> ###########
353
354                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
355                 tableWriter.Write(yAxis);//legend: Y-axis
356                 tableWriter.RenderEndTag( );
357
358                 tableWriter.RenderEndTag( );
359                 
360                 }
361                 tableWriter.RenderEndTag( );
362
363                 //################ X-Axis <tfoot> #######################
364                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
365                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
366
367                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
368                 tableWriter.Write("S, W");
369                 tableWriter.RenderEndTag( );
370
371                 for (int xAxisB = TopWest; xAxisB <= TopEast; xAxisB++) {
372                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
373                 tableWriter.Write(xAxisB);
374                 tableWriter.RenderEndTag( );
375                 }
376
377                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
378                 tableWriter.Write("S, E");
379                 tableWriter.RenderEndTag( );
380
381                 tableWriter.RenderEndTag( );
382                 tableWriter.RenderEndTag( );
383                 //###### </tfoot> ################################
384
385
386                 tableWriter.RenderEndTag( );//</table>
387                 
388                 //############## POI list #####################
389                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
390                 foreach (var poi in this.POIs) {
391                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
392                 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
393                 tableWriter.WriteEncodedText(poi.Notes);
394                 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
395                 tableWriter.RenderEndTag( );
396                 }
397
398                 tableWriter.RenderEndTag( );
399
400                 
401
402
403                 tableWriter.RenderEndTag( );//### </BODY> ###
404                                                         
405                 tableWriter.EndRender( );
406                 tableWriter.Flush( );
407                 }
408                 outputText.Flush( );            
409                 }
410
411                 Logger.VerboseDebug("Generated HTML map");
412                 }
413
414
415
416                 private ColumnMeta CreateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
417                 {
418                 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy(), chunkSize);
419                 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * chunkSize,
420                                                                                 mapChunk.YMax,
421                                                                                 mostActiveCol.Key.Y * chunkSize);
422
423                 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
424                 data.UpdateFieldsFrom(climate, mapChunk,TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
425                                         
426                 
427
428
429                 return data;
430                 }
431
432                 /// <summary>
433                 /// Reload chunk bounds from chunk shards
434                 /// </summary>
435                 /// <returns>The metadata.</returns>
436                 private void Reload_Metadata( )
437                 {       
438                 var worldmapDir = new DirectoryInfo(path);
439
440                 if (worldmapDir.Exists) {
441
442                 var files = worldmapDir.GetFiles(chunkFile_filter);
443
444                 if (files.Length > 0) {
445                 #if DEBUG
446                 Logger.VerboseDebug("{0} Existing world chunk shards", files.Length);
447                 #endif
448
449                 
450
451                 foreach (var shardFile in files) {
452                 var result = chunkShardRegex.Match(shardFile.Name);
453                 if (result.Success) {
454                 int X_chunk_pos = int.Parse(result.Groups["X"].Value );
455                 int Z_chunk_pos = int.Parse(result.Groups["Z"].Value );
456                 
457                 //Parse PNG chunks for METADATA in shard
458                 using (var fileStream = shardFile.OpenRead( ))
459                 {
460                 PngReader pngRead = new PngReader(fileStream );
461                 pngRead.ReadSkippingAllRows( );
462                 pngRead.End( );
463
464                 PngMetadataChunk metadataFromPng = pngRead.GetChunksList( ).GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
465
466                 chunkTopMetadata.Add(metadataFromPng.ChunkMetadata);
467                 }
468                 
469                 }
470                 }
471
472                 }
473                 }
474                 else {
475                 #if DEBUG
476                 Logger.VerboseDebug("Could not open world map directory");
477                 #endif
478                 }
479
480
481
482                 }
483
484                 private PngWriter SetupPngImage(Vec2i coord, ColumnMeta metadata)
485                 {
486                 ImageInfo imageInf = new ImageInfo(chunkSize, chunkSize, 8, false);
487                 
488                 string filename = $"{coord.X}_{coord.Y}.png";
489                 filename = Path.Combine(path, filename);
490
491                 PngWriter pngWriter = FileHelper.CreatePngWriter(filename, imageInf, true);
492                 PngMetadata meta = pngWriter.GetMetadata( );
493                 meta.SetTimeNow( );
494                 meta.SetText("Chunk_X", coord.X.ToString("D"));
495                 meta.SetText("Chunk_Y", coord.Y.ToString("D"));
496                 //Setup specialized meta-data PNG chunks here...
497                 PngMetadataChunk pngChunkMeta = new PngMetadataChunk(pngWriter.ImgInfo);
498                 pngChunkMeta.ChunkMetadata = metadata;          
499                 pngWriter.GetChunksList( ).Queue(pngChunkMeta);
500
501                 return pngWriter;
502                 }
503
504                 /// <summary>
505                 /// Does the heavy lifting of Scanning the whole (surface) chunk - creates Heightmap and Processes POIs, and stats...
506                 /// </summary>
507                 /// <param name="key">Chunk Coordinate</param>
508                 /// <param name="mapChunk">Map chunk.</param>
509                 /// <param name="chunkMeta">Chunk metadata</param>
510                 private void ProcessChunkBlocks(Vec2i key, IMapChunk mapChunk, ColumnMeta chunkMeta)
511                 {
512
513                 }
514
515
516
517
518                 #endregion
519
520
521         }
522
523 }