OSDN Git Service

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