OSDN Git Service

249151c500d7f6d990c33db4c486da89e758dd16
[automap/automap.git] / Automap / Subsystems / AutomapSystem.cs
1 using System;
2 using System.Collections;
3 using System.Collections.Concurrent;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Linq;
7 using System.Text;
8 using System.Text.RegularExpressions;
9 using System.Threading;
10
11 using Hjg.Pngcs;
12 using Mono.Collections.Generic;
13 using ProtoBuf;
14
15 using Vintagestory.API.Client;
16 using Vintagestory.API.Common;
17 using Vintagestory.API.Common.Entities;
18 using Vintagestory.API.Config;
19 using Vintagestory.API.Datastructures;
20 using Vintagestory.API.MathTools;
21 using Vintagestory.Common;
22
23 namespace Automap
24 {
25         public class AutomapSystem
26         {
27                 private Thread cartographer_thread;
28
29                 private Snapshotter snapshot;
30                 private ICoreClientAPI ClientAPI { get; set; }
31                 private ILogger Logger { get; set; }
32                 private AChunkRenderer ChunkRenderer { get; set; }
33                 private JsonGenerator JsonGenerator { get; set; }
34
35                 internal const string _mapPath = @"Maps";
36                 internal const string _chunkPath = @"Chunks";
37                 internal const uint editThreshold = 9;
38                 private const string _domain = @"automap";
39                 private const string chunkFile_filter = @"*_*.png";
40                 private const string poiFileName = @"poi_binary";
41                 private const string eoiFileName = @"eoi_binary";
42                 private const string pointsTsvFileName = @"points_of_interest.tsv";
43                 private const string plainMetadataFileName = @"map_metadata.txt";
44                 private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+)\.png", RegexOptions.Singleline);
45
46                 private ConcurrentDictionary<Vec2i, ColumnCounter> columnCounters = new ConcurrentDictionary<Vec2i, ColumnCounter>(3, 150);
47                 private Queue<Vec2i> revisitChunkList = new Queue<Vec2i>();
48                 private ColumnsMetadata chunkTopMetadata;
49                 internal PointsOfInterest POIs = new PointsOfInterest();
50                 internal EntitiesOfInterest EOIs = new EntitiesOfInterest();
51
52                 internal Dictionary<int, BlockDesignator> BlockID_Designators { get; private set; }
53                 internal Dictionary<AssetLocation, EntityDesignator> Entity_Designators { get; private set; }
54                 internal Dictionary<int, string> RockIdCodes { get; private set; }
55                 internal Dictionary<int, string> AiryIdCodes { get; private set; }
56
57                 internal CommandType CurrentState { get; set; }
58                 //Run status, Chunks processed, stats, center of map....
59                 private uint nullChunkCount, nullMapCount, updatedChunksTotal;
60                 private Vec2i startChunkColumn;
61
62                 private readonly int chunkSize;
63                 private string path;
64                 private IAsset staticMap;
65                 private PersistedConfiguration configuration;
66
67
68                 public static string AutomapStatusEventKey = @"AutomapStatus";
69                 public static string AutomapCommandEventKey = @"AutomapCommand";
70
71                 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger, PersistedConfiguration config)
72                 {
73                         this.ClientAPI = clientAPI;
74                         this.Logger = logger;
75                         chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
76
77                         configuration = config;
78                         ClientAPI.Event.LevelFinalize += EngageAutomap;
79
80                         this.ChunkRenderer = InstantiateChosenRenderer(config.RendererName);
81
82                         //Listen on bus for commands
83                         ClientAPI.Event.RegisterEventBusListener(CommandListener, 1.0, AutomapSystem.AutomapCommandEventKey);
84
85
86                         if (configuration.Autostart)
87                         {
88                                 CurrentState = CommandType.Run;
89                                 Logger.Notification("Autostart is Enabled.");
90                         }
91
92                 }
93
94
95                 #region Internals
96                 private void EngageAutomap()
97                 {
98                         path = ClientAPI.GetOrCreateDataPath(_mapPath);
99                         path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
100                         ClientAPI.GetOrCreateDataPath(Path.Combine(path, _chunkPath));
101                                                   
102                         JsonGenerator = new JsonGenerator(ClientAPI, Logger, path);
103
104                         string mapFilename = Path.Combine(path, "automap.html");
105                         StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite));
106
107                         staticMap = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap.html"));
108                         outputText.Write(staticMap.ToText());
109                         outputText.Flush();
110
111                         Prefill_POI_Designators();
112                         startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.Pos.AsBlockPos.X / chunkSize), (ClientAPI.World.Player.Entity.Pos.AsBlockPos.Z / chunkSize));
113                         chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
114                         Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
115                         Reload_Metadata();
116
117                         ClientAPI.Event.ChunkDirty += ChunkAChanging;
118
119                         cartographer_thread = new Thread(Cartographer)
120                         {
121                                 Name = "Cartographer",
122                                 Priority = ThreadPriority.Lowest,
123                                 IsBackground = true
124                         };
125
126                         ClientAPI.Event.RegisterGameTickListener(ThreadDecider, 6000);
127                 }
128
129                 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
130                 {
131                 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);              
132                 bool newOrEdit = (reason == EnumChunkDirtyReason.NewlyCreated || reason == EnumChunkDirtyReason.NewlyLoaded);
133                 
134                 columnCounters.AddOrUpdate(topPosition, 
135                                               new ColumnCounter(chunkSize, newOrEdit, chunkCoord), 
136                                               (chkPos, chkChng) => chkChng.Update(chunkCoord, chunkSize, newOrEdit)
137                                              );         
138                 }
139
140                 /// <summary>
141                 /// Cartographer Thread 'decider'
142                 /// </summary>
143                 /// <param name="delayed">called delay offset</param>
144                 private void ThreadDecider(float delayed)
145                 {
146
147                         if (CurrentState == CommandType.Run && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true))
148                         {
149                                 #if DEBUG
150                                 Logger.VerboseDebug("ThreadDecider re-trigger from [{0}]", cartographer_thread.ThreadState);
151                                 #endif
152
153                                 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted))
154                                 {
155                                         cartographer_thread.Start();
156                                 }
157                                 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin))
158                                 {
159                                         //Time to (re)write chunk shards
160                                         cartographer_thread.Interrupt();
161                                 }
162                                 //#if DEBUG
163                                 //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})");
164                                 //#endif
165                         }
166                         else if (CurrentState == CommandType.Snapshot)
167                         {
168                         //Prepare for taking a snopshot
169                         if (snapshot == null) {         
170                                 snapshot = new Snapshotter(path, chunkTopMetadata, chunkSize, ClientAPI.World.Seed);
171                                 #if DEBUG
172                                 Logger.VerboseDebug("Starting new Snapshot: {0} Wx{1} Hx{2}", snapshot.fileName, snapshot.Width, snapshot.Height);
173                                 #endif
174                                 snapshot.Take( );
175                                 }
176                         else if (snapshot != null && snapshot.Finished) {
177                                 #if DEBUG
178                                 Logger.VerboseDebug("COMPLETED Snapshot: {0} Wx{1} Hx{2}, taking {3}", snapshot.fileName, snapshot.Width, snapshot.Height, snapshot.Timer.Elapsed);
179                                 #endif
180                                 snapshot = null;
181                                 CurrentState = CommandType.Run;
182                                 }
183                         }
184                 }
185
186
187                 private void Cartographer()
188                 {
189                         wake:
190                         Logger.VerboseDebug("Cartographer thread awoken");
191
192                         try
193                         {
194                                 ColumnCounter ejectedItem ;
195                                 uint updatedChunks = 0;
196                                 uint updatedPixels = 0;
197
198
199                 //Revisit failed chunks first;
200                 while (revisitChunkList.Count > 0) 
201                 {
202                 var revisitCoord = revisitChunkList.Dequeue( );
203                 var rv_mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(revisitCoord);
204
205                 if (rv_mapChunk == null) continue;
206
207                 ColumnMeta rv_chunkMeta;
208                 if (chunkTopMetadata.Contains(revisitCoord)) 
209                 {
210                 rv_chunkMeta = chunkTopMetadata[revisitCoord];
211                 #if DEBUG
212                 Logger.VerboseDebug("(re)Loaded meta-chunk {0}", revisitCoord);
213                 #endif
214                 }
215                 else                                    
216                 {
217                 rv_chunkMeta = CreateColumnMetadata(revisitCoord, rv_mapChunk);
218                 #if DEBUG
219                 Logger.VerboseDebug("(re)Created meta-chunk {0}", revisitCoord);
220                 #endif
221                 }
222                 //TODO: Double validation of....?
223                 ProcessChunkBlocks(revisitCoord, rv_mapChunk, ref rv_chunkMeta);
224
225                 ChunkRenderer.SetupPngImage(revisitCoord, path, _chunkPath, ref rv_chunkMeta);
226                 ChunkRenderer.GenerateChunkPngShard(revisitCoord, rv_mapChunk, rv_chunkMeta, ref chunkTopMetadata, out updatedPixels);
227
228                 if (updatedPixels > 0) 
229                         {
230                         #if DEBUG
231                         Logger.VerboseDebug("(re)Wrote top-chunk shard: ({0}), Pixels#:{2}", revisitCoord, updatedPixels);
232                         #endif
233                         updatedChunks++;
234                         chunkTopMetadata.Update(rv_chunkMeta);
235                         }
236                 }//*********** REVISIT'd ******************
237                 
238                 if (!columnCounters.IsEmpty)
239                                 {//Resulting snapshot keys mabey NON-UNIQUE!!! a *FEATURE* of ConcurrentDict !
240                                         var columnsSnapshot = columnCounters.Where(cks => cks.Value.WeightedSum > editThreshold).OrderByDescending(kvp => kvp.Value.WeightedSum).ToArray( );
241
242                                         UpdateEntityMetadata();
243
244                                         foreach (var mostActiveCol in columnsSnapshot)
245                                         {
246                                                 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
247
248                                                 if (mapChunk == null)
249                                                 {                                                       
250                                                         #if DEBUG
251                                                         Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
252                                                         #endif
253                                                         nullMapCount++;
254                                                         columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
255                                                         revisitChunkList.Enqueue(mostActiveCol.Key);
256                                                         continue;
257                                                 }
258
259                                                 ColumnMeta chunkMeta;
260                                                 if (chunkTopMetadata.Contains(mostActiveCol.Key))
261                                                 {
262                                                         chunkMeta = chunkTopMetadata[mostActiveCol.Key];
263                                                         #if DEBUG
264                                                         Logger.VerboseDebug("Loaded meta-chunk {0}", mostActiveCol.Key);
265                                                         #endif
266                                                 }
267                                                 else
268                                                 {
269                                                         chunkMeta = CreateColumnMetadata(mostActiveCol, mapChunk);
270                                                         #if DEBUG
271                                                         Logger.VerboseDebug("Created meta-chunk {0}", mostActiveCol.Key);
272                                                         #endif
273                                                 }
274
275                                                 /********* Special Interlock with C.D. *************/
276                                                 if (columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem)) {
277                                                 ProcessChunkBlocks(mostActiveCol.Key, mapChunk, ref chunkMeta);
278                                                 ejectedItem.SetCutoff(chunkMeta.YMax / chunkSize);
279
280                                                 ChunkRenderer.SetupPngImage(mostActiveCol.Key, path, _chunkPath, ref chunkMeta);
281                                                 ChunkRenderer.GenerateChunkPngShard(mostActiveCol.Key, mapChunk, chunkMeta, ref chunkTopMetadata, out updatedPixels);
282                                                 }
283                                                 else {
284                                                 #if DEBUG
285                                                 Logger.Warning("Prevented duplicate processing of: {0}",mostActiveCol.Key );
286                                                 #endif
287                                                 }
288
289                                                 if (updatedPixels > 0)
290                                                 {
291                                                         #if DEBUG
292                                                         Logger.VerboseDebug("Wrote top-chunk shard: ({0}) - Weight:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
293                                                         #endif
294                                                         updatedChunks++;
295                                                         chunkTopMetadata.Update(chunkMeta);
296                                                         //columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
297                                                 }
298                                                 else
299                                                 {
300                                                         //columnCounters.TryRemove(mostActiveCol.Key, out ejectedItem);
301                                                         #if DEBUG
302                                                         Logger.VerboseDebug("Un-painted chunk shard: ({0}) ", mostActiveCol.Key);
303                                                         #endif
304                                                         revisitChunkList.Enqueue(mostActiveCol.Key);
305                                                 }
306                                         }
307                                 }
308
309                                 UpdateStatus(this.updatedChunksTotal, this.nullChunkCount, updatedChunks);
310
311                                 if (updatedChunks > 0)
312                                 {
313                                         //What about chunk updates themselves; a update bitmap isn't kept...
314                                         updatedChunksTotal += updatedChunks;
315                                         JsonGenerator.GenerateJSONMetadata(chunkTopMetadata, startChunkColumn, POIs, EOIs, RockIdCodes);
316                                         updatedChunks = 0;
317
318                                         //Cleanup in-memory Metadata...
319                                         chunkTopMetadata.ClearMetadata( );
320                                 }
321
322                                 #if DEBUG
323                                 Logger.VerboseDebug("Clearing Column Counters of: {0} non-written shards", columnCounters.Count);
324                                 #endif
325
326                                 columnCounters.Clear( );
327
328                                 //Then sleep until interupted again, and repeat
329                                 #if DEBUG
330                                 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
331                                 #endif
332                                 Thread.Sleep(Timeout.Infinite);
333
334                         }
335                         catch (ThreadInterruptedException)
336                         {
337
338                         #if DEBUG
339                                 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
340                         #endif
341                                 goto wake;
342
343                         }
344                         catch (ThreadAbortException)
345                         {
346                         #if DEBUG
347                                 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
348                         #endif
349                         }
350                         finally
351                         {
352                         #if DEBUG
353                                 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
354                         #endif
355                                 PersistPointsData();
356                                 Write_PlainMetadata( );
357                         }
358                 }
359
360                 private void Snap()
361                 {
362                         snapshotTake:
363                         #if DEBUG
364                         Logger.VerboseDebug("Snapshot started");
365                         #endif
366                         try
367                         {
368                                 snapshot.Take();
369                                 #if DEBUG
370                                 Logger.VerboseDebug("Snapshot sleeping");
371                                 #endif
372                                 CurrentState = CommandType.Run;
373                                 Thread.Sleep(Timeout.Infinite);
374                         }
375                         catch (ThreadInterruptedException)
376                         {
377                                 #if DEBUG
378                                 Logger.VerboseDebug("Snapshot intertupted");
379                                 #endif
380                                 goto snapshotTake;
381                         }
382                 }
383
384                 private void UpdateStatus(uint totalUpdates, uint voidChunks, uint delta)
385                 {
386                         StatusData updateData = new StatusData(totalUpdates, voidChunks, delta, CommandType.Run);
387
388                         this.ClientAPI.Event.PushEvent(AutomapStatusEventKey, updateData);
389                 }
390
391                 private void Prefill_POI_Designators()
392                 {
393
394                         this.BlockID_Designators = new Dictionary<int, BlockDesignator>();
395                         this.Entity_Designators = new Dictionary<AssetLocation, EntityDesignator>();
396                         this.RockIdCodes = Helpers.ArbitrarytBlockIdHunter(ClientAPI, new AssetLocation(GlobalConstants.DefaultDomain, "rock-"), EnumBlockMaterial.Stone);
397
398                         var airBlocksQuery = from airyBlock in ClientAPI.World.Blocks
399                                                          where airyBlock.MatterState == EnumMatterState.Solid
400                                                          where airyBlock.BlockMaterial == EnumBlockMaterial.Plant || airyBlock.BlockMaterial == EnumBlockMaterial.Leaves 
401                                                          where airyBlock.CollisionBoxes == null || airyBlock.CollisionBoxes.Length == 0 || airyBlock.RainPermeable == true                                       
402                                                                 select airyBlock;                       
403                         //^^ 'Solid' phase - 'Plant' Blocks without any bounding-box; OR 'Invisible' shapes...
404                         var invisibleBlocksQuery = from novisBlock in ClientAPI.World.Blocks                                                                       
405                                                                            where novisBlock.Shape == null || novisBlock.Shape.Base.EndsWith(GlobalConstants.DefaultDomain, @"invisible")   //Whaat! [ base: "block/basic/invisible" ]
406                                                                                 select novisBlock;                      
407                         this.AiryIdCodes = airBlocksQuery.Union(invisibleBlocksQuery).ToDictionary(aBlk => aBlk.BlockId, aBlk => aBlk.Code.Path);
408
409                         #if DEBUG
410                         foreach (var fluffBlock in AiryIdCodes) {
411                         Logger.VerboseDebug("ID#\t{0}:\t{1} IGNORED", fluffBlock.Key, fluffBlock.Value);
412                         }
413                         Logger.VerboseDebug("Ignoring {0} blocks", AiryIdCodes.Count);
414                         #endif
415
416                 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
417                 Reload_POI_Designators();
418                 }
419
420                 private void Reload_POI_Designators()
421                 {
422                 uint poisSetup =0, eoiSetup = 0;
423                         foreach (var designator in configuration.BlockDesignators)
424                         {
425                                 if (designator.Enabled == false) continue;
426                                 var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
427                                 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString(), blockIDs.Count); }
428                                 foreach (var entry in blockIDs)
429                                 {
430                                         BlockID_Designators.Add(entry.Key, designator);
431                                         poisSetup++;
432                                 }
433                         }
434                         this.ChunkRenderer.BlockID_Designators = BlockID_Designators;
435                         Logger.VerboseDebug("Connected {0} IDs from {1} Block-Designators", poisSetup, configuration.BlockDesignators.Count );
436
437
438                         foreach (var designator in configuration.EntityDesignators)
439                         {
440                                 if (designator.Enabled == false) continue;
441                                 //Get Variants first, from EntityTypes...better be populated!
442                                 var matched = ClientAPI.World.EntityTypes.FindAll(entp => entp.Code.BeginsWith(designator.Pattern.Domain, designator.Pattern.Path));
443
444                                 foreach (var match in matched)
445                                 {                                       
446                                         Logger.VerboseDebug("Linked Entity: {0} Designator: {1}", match.Code, designator);
447                                         this.Entity_Designators.Add(match.Code, designator);
448                                         eoiSetup++;
449                                 }
450                         }
451                         Logger.VerboseDebug("Connected {0} IDs from {1} Entity-Designators", eoiSetup, configuration.EntityDesignators.Count);
452
453                 }
454
455
456
457                 /// <summary>
458                 /// Store Points/Entity of Interest
459                 /// </summary>
460                 private void PersistPointsData()
461                 {
462                         //POI and EOI raw dump files ~ WRITE em!
463                         //var poiRawFile = File.
464                         string poiPath = Path.Combine(path, poiFileName);
465                         string eoiPath = Path.Combine(path, eoiFileName);
466
467                         if (this.POIs.Count > 0)
468                         {
469                                 using (var poiFile = File.Open(poiPath, FileMode.Create, FileAccess.Write, FileShare.None))
470                                 {
471                                         Serializer.Serialize<PointsOfInterest>(poiFile, this.POIs);
472                                         poiFile.Flush(true);
473                                 }
474                         }
475
476                         if (this.EOIs.Count > 0)
477                         {
478                                 using (var eoiFile = File.Open(eoiPath, FileMode.Create, FileAccess.Write, FileShare.None))
479                                 {
480                                         Serializer.Serialize<EntitiesOfInterest>(eoiFile, this.EOIs);
481                                         eoiFile.Flush(true);
482                                 }
483                         }
484
485                         //Create Easy to Parse TSV file for tool/human use....
486                         string pointsTsvPath = Path.Combine(path, pointsTsvFileName);
487
488                         using (var tsvWriter = new StreamWriter(pointsTsvPath, false, Encoding.UTF8))
489                         {
490                                 tsvWriter.WriteLine("Name\tDescription\tLocation\tTime\tDestination\tEntity_UID");
491                                 foreach (var point in this.POIs)
492                                 {
493                                         tsvWriter.Write(point.Name + "\t");
494                                         var notes = point.Notes
495                                                 .Replace('\n', '\x001f')
496                                                 .Replace("\t", "\\t")
497                                                 .Replace("\\", "\\\\");
498                                         tsvWriter.Write(notes + "\t");
499                                         tsvWriter.Write(point.Location.PrettyCoords(ClientAPI) + "\t");
500                                         tsvWriter.Write(point.Timestamp.ToString("u") + "\t");
501                                         tsvWriter.Write((point.Destination != null ? point.Destination.PrettyCoords(ClientAPI) : "---") +"\t");
502                                         tsvWriter.Write("null\t");
503                                         tsvWriter.WriteLine();
504                                 }
505                                 foreach (var entity in this.EOIs)
506                                 {
507                                         tsvWriter.Write(entity.Name + "\t");
508                                         var notes = entity.Notes
509                                                 .Replace('\n', '\x001f')
510                                                 .Replace("\t", "\\t")
511                                                 .Replace("\\", "\\\\");
512                                         tsvWriter.Write(notes + "\t");
513                                         tsvWriter.Write(entity.Location.PrettyCoords(ClientAPI) + "\t");
514                                         tsvWriter.Write(entity.Timestamp.ToString("u") + "\t");
515                                         tsvWriter.Write("---\t");
516                                         tsvWriter.Write(entity.EntityId.ToString("D"));
517                                         tsvWriter.WriteLine();
518                                 }
519                                 tsvWriter.WriteLine();
520                                 tsvWriter.Flush();
521                         }
522
523                 }
524
525                 private void Write_PlainMetadata( )
526                 { 
527                 string metaPath = Path.Combine(path, plainMetadataFileName);
528
529                 using (var metaDataFile = File.Open(metaPath,FileMode.Create)) {
530                 using (var mdWriter = new StreamWriter(metaDataFile, Encoding.ASCII)) 
531                         {
532                                 mdWriter.WriteLine("WorldSeed {0}", ClientAPI.World.Seed);
533                                 mdWriter.WriteLine("PlayerChunkCoords {0:D} {1:D}", startChunkColumn.X, startChunkColumn.Y);
534                                 mdWriter.WriteLine("DefaultSpawnPos {0:D} {1:D} {2:D}", ClientAPI.World.DefaultSpawnPosition.AsBlockPos.X,ClientAPI.World.DefaultSpawnPosition.AsBlockPos.Y,ClientAPI.World.DefaultSpawnPosition.AsBlockPos.Z);
535                                 //mdWriter.WriteLine("CurrentPlayerSpawn", ClientAPI.World.Player.WorldData.EntityPlayer.);
536                                 mdWriter.WriteLine("ChunkSize {0}", chunkSize);
537                                 mdWriter.WriteLine("SeaLevel {0:D}", ClientAPI.World.SeaLevel);
538                                 mdWriter.WriteLine("WorldSize {0:D} {1:D} {2:D}", ClientAPI.World.BulkBlockAccessor.MapSizeX, ClientAPI.World.BulkBlockAccessor.MapSizeY,ClientAPI.World.BulkBlockAccessor.MapSizeZ);
539                                 mdWriter.WriteLine("RegionSize {0:D}", ClientAPI.World.BulkBlockAccessor.RegionSize);
540                                 mdWriter.WriteLine("AMVersion '{0}'", ClientAPI.Self().Info.Version);
541                                 mdWriter.WriteLine("PlayTime {0:F1}", ClientAPI.InWorldEllapsedMilliseconds / 1000);
542                                 mdWriter.WriteLine("GameDate {0}", ClientAPI.World.Calendar.PrettyDate());
543                                 mdWriter.WriteLine("Chunks {0:D}", chunkTopMetadata.Count);
544                                 mdWriter.WriteLine("Chunks Updated {0:D}", updatedChunksTotal);
545                                 mdWriter.WriteLine("Null Chunks {0:D}", nullChunkCount);        
546                                 mdWriter.Flush( );
547                         }
548                 }
549                 }
550                         
551
552                 private ColumnMeta CreateColumnMetadata(KeyValuePair<Vec2i, ColumnCounter> mostActiveCol, IMapChunk mapChunk)
553                 {
554                         ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy(), ClientAPI, (byte) chunkSize, (ClientAPI.World.BlockAccessor.MapSizeY / chunkSize));
555                         BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * chunkSize,
556                                                                                         mapChunk.YMax,
557                                                                                         mostActiveCol.Key.Y * chunkSize);
558
559                         var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
560                         data.UpdateFieldsFrom(climate, mapChunk, TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
561
562                         return data;
563                 }
564
565         private ColumnMeta CreateColumnMetadata(Vec2i coords, IMapChunk mapChunk)
566                 {
567                 ColumnMeta data = new ColumnMeta(coords, ClientAPI, ( byte )chunkSize, (ClientAPI.World.BlockAccessor.MapSizeY / chunkSize));
568                 BlockPos equivBP = new BlockPos(coords.X * chunkSize,
569                                                                                 mapChunk.YMax,
570                                                                                 coords.Y * chunkSize);
571
572                 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
573                 data.UpdateFieldsFrom(climate, mapChunk, TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
574
575                 return data;
576                 }
577
578                 /// <summary>
579                 /// Reload chunk bounds from chunk shards
580                 /// </summary>
581                 /// <returns>The metadata.</returns>
582                 private void Reload_Metadata()
583                 {
584                         var shardsDir = new DirectoryInfo( Path.Combine(path, _chunkPath) );
585
586                         if (!shardsDir.Exists)
587                         {
588                                 #if DEBUG
589                                 Logger.VerboseDebug("Could not open world map (shards) directory");
590                                 #endif
591                                 return;
592                         }
593                         var shardFiles = shardsDir.GetFiles(chunkFile_filter);
594
595                         if (shardFiles.Length > 0)
596                         {
597                                 #if DEBUG
598                                 Logger.VerboseDebug("Metadata reloading from {0} shards", shardFiles.Length);
599                                 #endif
600
601                                 foreach (var shardFile in shardFiles)
602                                 {
603
604                                         if (shardFile.Length < 1024) continue;
605                                         var result = chunkShardRegex.Match(shardFile.Name);
606                                         if (!result.Success) continue;
607
608                                         int X_chunk_pos = int.Parse(result.Groups["X"].Value);
609                                         int Z_chunk_pos = int.Parse(result.Groups["Z"].Value);
610
611                                         try
612                                         {
613                                                 using (var fileStream = shardFile.OpenRead())
614                                                 {
615
616                                                         PngReader pngRead = new PngReader(fileStream);
617                                                         pngRead.ReadSkippingAllRows();
618                                                         pngRead.End();
619                                                         //Parse PNG chunks for METADATA in shard
620                                                         PngMetadataChunk metadataFromPng = pngRead.GetChunksList().GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
621                                                         var column = metadataFromPng.ChunkMetadata;
622                                                         if (column.PrettyLocation == null)
623                                                                 column = column.Reload(ClientAPI);
624                                                         chunkTopMetadata.Add(column);
625                                                 }
626
627                                         }
628                                         catch (PngjException someEx)
629                                         {
630                                                 Logger.Error("PNG Corruption file '{0}' - Reason: {1}", shardFile.Name, someEx);
631                                                 continue;
632                                         }
633                                         catch (ProtoException protoEx) 
634                                         {
635                                                 Logger.Error("ProtoBuf invalid! file:'{0}' - Reason: {1}", shardFile.Name, protoEx);
636                                                 continue;
637                                         }
638                                 }
639                         }
640
641                         //POI and EOI raw dump files ~ reload em!
642                         //var poiRawFile = File.
643                         string poiPath = Path.Combine(path, poiFileName);
644                         string eoiPath = Path.Combine(path, eoiFileName);
645
646                         if (File.Exists(poiPath))
647                         {
648                                 using (var poiFile = File.OpenRead(poiPath))
649                                 {
650                                         this.POIs = Serializer.Deserialize<PointsOfInterest>(poiFile);
651                                         Logger.VerboseDebug("Reloaded {0} POIs from file.", this.POIs.Count);
652                                 }
653                         }
654
655                         if (File.Exists(eoiPath))
656                         {
657                                 using (var eoiFile = File.OpenRead(eoiPath))
658                                 {
659                                         this.EOIs = Serializer.Deserialize<EntitiesOfInterest>(eoiFile);
660                                         Logger.VerboseDebug("Reloaded {0} EOIs from file.", this.EOIs.Count);
661                                 }
662                         }
663
664                 }
665
666
667
668                 /// <summary>
669                 /// Does the heavy lifting of Scanning columns of chunks - scans for BlockEntity, creates Heightmap and stats...
670                 /// </summary>
671                 /// <param name="key">Chunk Coordinate</param>
672                 /// <param name="mapChunk">Map chunk.</param>
673                 /// <param name="chunkMeta">Chunk metadata</param>
674                 private void ProcessChunkBlocks(Vec2i key, IMapChunk mapChunk, ref ColumnMeta chunkMeta)
675                 {
676                         int targetChunkY = mapChunk.YMax / chunkSize;//Surface ish... 
677                         byte chunkTally = 0;
678
679                 #if DEBUG
680                 Logger.VerboseDebug("Start col @ X{0} Y{1} Z{2} !", key.X, targetChunkY, key.Y);
681                 #endif
682
683                 chunkMeta.ResetMetadata(ClientAPI.World.BlockAccessor.MapSizeY);
684
685                 for (; targetChunkY > 0; targetChunkY--)
686                         {
687                                 WorldChunk worldChunk = ClientAPI.World.BlockAccessor.GetChunk(key.X, targetChunkY, key.Y) as WorldChunk;
688
689                                 if (worldChunk == null || worldChunk.BlockEntities == null || worldChunk.Disposed)
690                                 {
691                                         #if DEBUG
692                                         Logger.VerboseDebug("WORLD chunk, null/disposed OR B.E. null: X{0} Y{1} Z{2} !", key.X, targetChunkY, key.Y);
693                                         #endif
694                                         nullChunkCount++;
695                                         continue;
696                                 }
697
698                                 if (worldChunk.IsPacked()) 
699                                 {
700                                 #if DEBUG
701                                 Logger.VerboseDebug("WORLD chunk: Compressed: X{0} Y{1} Z{2}", key.X, targetChunkY, key.Y);
702                                 #endif
703                                 //VALIDATE: check Read-only applicable
704                                 if (worldChunk.Unpack_ReadOnly( ) == false) 
705                                         { 
706                                         Logger.Warning("Failed to unpack chunk: X{0} Y{1} Z{2}", key.X, targetChunkY, key.Y);
707                                         nullChunkCount++;
708                                         continue;
709                                         };
710                                 }
711
712                                 /*************** Chunk Entities Scanning *********************/
713                                 if (worldChunk.BlockEntities != null && worldChunk.BlockEntities.Count > 0)
714                                 {
715                                         #if DEBUG
716                                         Logger.VerboseDebug("Scan in Chunk:({0}, Y {2}):  found {1} BlockEntities", key, worldChunk.BlockEntities.Count,targetChunkY);
717                                         #endif
718
719                                         foreach (var blockEnt in worldChunk.BlockEntities)
720                                         {
721                                         var blockEntityPos = blockEnt.Key;
722                                         var blockEntity = blockEnt.Value;
723                                         if (blockEntityPos == null || blockEntity == null ) continue;
724                                         if (blockEntity.Block != null && !blockEntity.Block.IsMissing &&  BlockID_Designators.ContainsKey(blockEntity.Block.BlockId))
725                                                 {
726                                                         var designator = BlockID_Designators[blockEnt.Value.Block.BlockId];
727                                                         if (designator != null && designator.SpecialAction != null) designator.SpecialAction(ClientAPI, POIs, blockEnt.Value.Pos.Copy(), blockEnt.Value.Block);
728                                                 }
729                                         }
730                                 }
731
732                                 /********************* Chunk/Column BLOCKs scanning ****************/
733                                 //Heightmap, Stats, block tally
734
735                                 int X_index, Y_index, Z_index;
736
737                                 //First Chance fail-safe;
738                                 if (worldChunk.MaybeBlocks == null || worldChunk.MaybeBlocks.Length <= 0) {
739                                 #if DEBUG
740                                 Logger.VerboseDebug("WORLD chunk; Missing block DATA⁈ X{0} Y{1} Z{2} ⁈", key.X, targetChunkY, key.Y);
741                                 #endif
742                                 nullChunkCount++;
743                                 continue;
744                                 }               
745
746                                 chunkMeta.ColumnPresense[targetChunkY] = true;
747                                 chunkTally++;
748                                 for (Y_index = 0; Y_index < chunkSize; Y_index++)
749                                 {
750                                         for (Z_index = 0; Z_index < chunkSize; Z_index++)
751                                         {
752                                                 for (X_index = 0; X_index < chunkSize; X_index++) 
753                                                 {
754                                                 var indicie = MapUtil.Index3d(X_index, Y_index, Z_index, chunkSize, chunkSize);
755
756                                                 //'Last' Chance fail-safe;
757                                                 if (worldChunk.MaybeBlocks == null || worldChunk.MaybeBlocks.Length <= 0) {
758                                                 #if DEBUG
759                                                 Logger.VerboseDebug("Processing Block: Missing block DATA⁈ X{0} Y{1} Z{2} ⁈", X_index, Y_index, Z_index);
760                                                 #endif
761                                                 nullChunkCount++;
762                                                 goto loop_bustout; 
763                                                 }
764
765                                                 int aBlockId = worldChunk.MaybeBlocks[indicie];
766
767                                                 if (aBlockId == 0 || AiryIdCodes.ContainsKey(aBlockId)) {//Airy blocks,,,
768                                                 chunkMeta.AirBlocks++;
769                                                 continue;
770                                                 }
771
772                                                 if (RockIdCodes.ContainsKey(aBlockId)) {
773                                                 if (chunkMeta.RockRatio.ContainsKey(aBlockId))
774                                                         chunkMeta.RockRatio[aBlockId]++;
775                                                 else
776                                                         chunkMeta.RockRatio.Add(aBlockId, 1);
777                                                 }
778
779                                                 chunkMeta.NonAirBlocks++;
780
781                                                 ushort localHeight = ( ushort )(Y_index + (targetChunkY * chunkSize));
782                                                 //Heightmap - Need to ignore Grass & Snow
783                                                 if (localHeight > chunkMeta.HeightMap[X_index, Z_index]) 
784                                                         {
785                                                         chunkMeta.HeightMap[X_index, Z_index] = localHeight;
786                                                         if (localHeight > chunkMeta.YMax) chunkMeta.YMax = localHeight;
787                                                         }
788                                                 }
789                                         }
790                                 }
791                                 loop_bustout:;
792                         }
793                         #if DEBUG
794                         Logger.VerboseDebug("COLUMN X{0} Z{1}: {2}, processed.", key.X , key.Y, chunkTally + 1);
795                         #endif
796                 }
797
798                 private void UpdateEntityMetadata()
799                 {
800                 #if DEBUG
801                 Logger.Debug("Presently {0} Entities", ClientAPI.World.LoadedEntities.Count);
802                 #endif
803
804                 var keyList = new long[ClientAPI.World.LoadedEntities.Keys.Count];
805                 ClientAPI.World.LoadedEntities.Keys.CopyTo(keyList, 0);
806
807              //'ElementAt'; worse! instead; walk fixed list...
808                 Entity loadedEntity;
809                 foreach (var key in keyList)
810                         {
811                         if (ClientAPI.World.LoadedEntities.TryGetValue(key, out loadedEntity))
812                                 {               
813                                 #if DEBUG
814                                 //Logger.VerboseDebug($"ENTITY: ({loadedEntity.Value.Code}) = #{loadedEntity.Value.EntityId} {loadedEntity.Value.State} {loadedEntity.Value.LocalPos}    <<<<<<<<<<<<");
815                                 #endif
816
817                                 var dMatch = Entity_Designators.SingleOrDefault(se => se.Key.Equals(loadedEntity.Code));
818                                 if (dMatch.Value != null) 
819                                         {
820                                         dMatch.Value.SpecialAction(ClientAPI, this.EOIs, loadedEntity.Pos.AsBlockPos.Copy( ), loadedEntity);
821                                         }
822                                 }                               
823                         }
824                 }
825
826                 private void AddNote(string notation)
827                 {
828                         var playerNodePoi = new PointOfInterest()
829                         {
830                                 Name = "Note",
831                                 Location = ClientAPI.World.Player.Entity.Pos.AsBlockPos.Copy(),
832                                 Notes = notation,
833                                 Timestamp = DateTime.UtcNow,
834                         };
835
836                         this.POIs.AddReplace(playerNodePoi);
837                 }
838
839
840
841                 private void CommandListener(string eventName, ref EnumHandling handling, IAttribute data)
842                 {
843                         //Logger.VerboseDebug("MsgBus RX: AutomapCommandMsg: {0}", data.ToJsonToken());
844
845                         CommandData cmdData = data as CommandData;
846
847                         switch (cmdData.State)
848                         {
849                                 case CommandType.Run:
850                                 case CommandType.Stop:
851                                 case CommandType.Snapshot:
852                                         if (CurrentState != cmdData.State)
853                                         {
854                                                 CurrentState = cmdData.State;
855                                                 ThreadDecider(0.0f);
856                                         }
857                                         break;
858
859                                 case CommandType.Notation:
860                                         //Add to POI list where player location
861                                         AddNote(cmdData.Notation);
862                                         break;
863                         }
864
865                         ClientAPI.TriggerChatMessage($"Automap commanded to: {cmdData.State} ");
866
867                 }
868 #endregion
869
870                 private AChunkRenderer InstantiateChosenRenderer(string rendererName )
871                 {
872                 Logger.VerboseDebug("Using '{0}' style Shard Renderer", rendererName);
873                 switch (rendererName) 
874                 {                               
875                 case StandardRenderer.Name:
876                         return new StandardRenderer(ClientAPI, Logger, this.configuration.SeasonalColors);
877                 
878                 case AlternateRenderer.Name:
879                         return new AlternateRenderer(ClientAPI, Logger, this.configuration.SeasonalColors);
880         
881                 case FlatRenderer.Name:
882                         return new FlatRenderer(ClientAPI, Logger, this.configuration.SeasonalColors);  
883
884                 default:
885                         throw new ArgumentOutOfRangeException("rendererName",rendererName,"That value isn't supported or known...");
886                 }
887
888                 return null;
889                 }
890         }
891
892 }