2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
6 using System.Reflection;
8 using System.Text.RegularExpressions;
9 using System.Threading;
12 using Hjg.Pngcs.Chunks;
14 using Newtonsoft.Json;
18 using Vintagestory.API.Client;
19 using Vintagestory.API.Common;
20 using Vintagestory.API.Config;
21 using Vintagestory.API.Datastructures;
22 using Vintagestory.API.MathTools;
23 using Vintagestory.Common;
27 public class AutomapSystem
29 private Thread cartographer_thread;
30 private Thread snapshotThread;
31 private Snapshotter snapshot;
32 private ICoreClientAPI ClientAPI { get; set; }
33 private ILogger Logger { get; set; }
34 private IChunkRenderer ChunkRenderer { get; set; }
35 private JsonGenerator JsonGenerator { get; set; }
37 internal const string _mapPath = @"Maps";
38 internal const string _chunkPath = @"Chunks";
39 private const string _domain = @"automap";
40 private const string chunkFile_filter = @"*_*.png";
41 private const string poiFileName = @"poi_binary";
42 private const string eoiFileName = @"eoi_binary";
43 private const string pointsTsvFileName = @"points_of_interest.tsv";
44 private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+)\.png", RegexOptions.Singleline);
46 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150);
47 private ColumnsMetadata chunkTopMetadata;
48 private PointsOfInterest POIs = new PointsOfInterest();
49 private EntitiesOfInterest EOIs = new EntitiesOfInterest();
51 internal Dictionary<int, BlockDesignator> BlockID_Designators { get; private set; }
52 internal Dictionary<AssetLocation, EntityDesignator> Entity_Designators { get; private set; }
53 internal Dictionary<int, string> RockIdCodes { get; private set; }
55 internal CommandType CurrentState { get; set; }
56 //Run status, Chunks processed, stats, center of map....
57 private uint nullChunkCount, nullMapCount, updatedChunksTotal;
58 private Vec2i startChunkColumn;
60 private readonly int chunkSize;
62 private IAsset staticMap;
63 private PersistedConfiguration configuration;
66 public static string AutomapStatusEventKey = @"AutomapStatus";
67 public static string AutomapCommandEventKey = @"AutomapCommand";
69 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger, PersistedConfiguration config)
71 this.ClientAPI = clientAPI;
73 chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
74 ClientAPI.Event.LevelFinalize += EngageAutomap;
75 configuration = config;
77 //TODO:Choose which one from GUI
78 this.ChunkRenderer = new StandardRenderer(clientAPI, logger);
80 //Listen on bus for commands
81 ClientAPI.Event.RegisterEventBusListener(CommandListener, 1.0, AutomapSystem.AutomapCommandEventKey);
82 //TODO: recreate as GUI button!
83 ClientAPI.RegisterCommand("snapshot", "", "", (id, args) => CurrentState = CommandType.Snapshot);
85 if (configuration.Autostart)
87 CurrentState = CommandType.Run;
88 Logger.Debug("Autostart is Enabled.");
95 private void EngageAutomap()
97 path = ClientAPI.GetOrCreateDataPath(_mapPath);
98 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
99 ClientAPI.GetOrCreateDataPath(Path.Combine(path, _chunkPath));
101 JsonGenerator = new JsonGenerator(ClientAPI, Logger, path);
103 string mapFilename = Path.Combine(path, "automap.html");
104 StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite));
106 staticMap = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap.html"));
107 outputText.Write(staticMap.ToText());
110 Prefill_POI_Designators();
111 startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / chunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z / chunkSize));
112 chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
113 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
116 ClientAPI.Event.ChunkDirty += ChunkAChanging;
118 cartographer_thread = new Thread(Cartographer)
120 Name = "Cartographer",
121 Priority = ThreadPriority.Lowest,
125 snapshot = new Snapshotter(path, chunkTopMetadata, chunkSize,ClientAPI.World.Seed );
126 snapshotThread = new Thread(Snap)
129 Priority = ThreadPriority.Lowest,
133 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
136 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
138 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
140 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
143 private void AwakenCartographer(float delayed)
146 if (CurrentState == CommandType.Run && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true))
149 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
152 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted))
154 cartographer_thread.Start();
156 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin))
158 //Time to (re)write chunk shards
159 cartographer_thread.Interrupt();
162 //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})");
165 else if (CurrentState == CommandType.Snapshot)
167 if (snapshotThread.ThreadState.HasFlag(ThreadState.Unstarted))
169 snapshotThread.Start();
170 } else if (snapshotThread.ThreadState.HasFlag(ThreadState.WaitSleepJoin))
172 snapshotThread.Interrupt();
179 private void Cartographer()
182 Logger.VerboseDebug("Cartographer thread awoken");
186 uint ejectedItem = 0;
187 uint updatedChunks = 0;
188 uint updatedPixels = 0;
190 //-- Should dodge enumerator changing underfoot....at a cost.
191 if (!columnCounter.IsEmpty)
193 var tempSet = columnCounter.ToArray().OrderByDescending(kvp => kvp.Value);
194 UpdateEntityMetadata();
196 foreach (var mostActiveCol in tempSet)
198 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
200 if (mapChunk == null)
202 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
204 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
208 ColumnMeta chunkMeta;
209 if (chunkTopMetadata.Contains(mostActiveCol.Key))
211 chunkMeta = chunkTopMetadata[mostActiveCol.Key];
213 Logger.VerboseDebug("Loaded chunk {0}", mostActiveCol.Key);
218 chunkMeta = CreateColumnMetadata(mostActiveCol, mapChunk);
220 Logger.VerboseDebug("Created chunk {0}", mostActiveCol.Key);
223 ProcessChunkBlocks(mostActiveCol.Key, mapChunk, ref chunkMeta);
225 PngWriter pngWriter = SetupPngImage(mostActiveCol.Key, ref chunkMeta);
226 ChunkRenderer.GenerateChunkPngShard(mostActiveCol.Key, mapChunk, chunkMeta, pngWriter, out updatedPixels);
228 if (updatedPixels > 0)
231 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
234 chunkTopMetadata.Update(chunkMeta);
235 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
239 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
241 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
245 //Cleanup persisted Metadata...
246 chunkTopMetadata.ClearMetadata();
249 UpdateStatus(this.updatedChunksTotal, this.nullChunkCount, updatedChunks);
251 if (updatedChunks > 0)
253 //What about chunk updates themselves; a update bitmap isn't kept...
254 updatedChunksTotal += updatedChunks;
255 JsonGenerator.GenerateJSONMetadata(chunkTopMetadata, startChunkColumn, POIs, EOIs, RockIdCodes);
259 //Then sleep until interupted again, and repeat
261 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
263 Thread.Sleep(Timeout.Infinite);
266 catch (ThreadInterruptedException)
270 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
275 catch (ThreadAbortException)
278 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
284 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
294 Logger.VerboseDebug("Snapshot started");
300 Logger.VerboseDebug("Snapshot sleeping");
302 CurrentState = CommandType.Run;
303 Thread.Sleep(Timeout.Infinite);
305 catch (ThreadInterruptedException)
308 Logger.VerboseDebug("Snapshot intertupted");
314 private void UpdateStatus(uint totalUpdates, uint voidChunks, uint delta)
316 StatusData updateData = new StatusData(totalUpdates, voidChunks, delta, CommandType.Run);
318 this.ClientAPI.Event.PushEvent(AutomapStatusEventKey, updateData);
321 private void Prefill_POI_Designators()
324 this.BlockID_Designators = new Dictionary<int, BlockDesignator>();
325 this.Entity_Designators = new Dictionary<AssetLocation, EntityDesignator>();
326 this.RockIdCodes = Helpers.ArbitrarytBlockIdHunter(ClientAPI, new AssetLocation(GlobalConstants.DefaultDomain, "rock-"), EnumBlockMaterial.Stone);
328 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
330 Reload_POI_Designators();
333 private void Reload_POI_Designators()
335 Logger.VerboseDebug("Connecting {0} Configured Block-Designators", configuration.BlockDesignators.Count);
336 foreach (var designator in configuration.BlockDesignators)
338 var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
339 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString(), blockIDs.Count); }
340 foreach (var entry in blockIDs)
342 BlockID_Designators.Add(entry.Key, designator);
345 this.ChunkRenderer.BlockID_Designators = BlockID_Designators;
348 Logger.VerboseDebug("Connecting {0} Configured Entity-Designators", configuration.EntityDesignators.Count);
349 foreach (var designator in configuration.EntityDesignators)
351 //Get Variants first, from EntityTypes...better be populated!
352 var matched = ClientAPI.World.EntityTypes.FindAll(entp => entp.Code.BeginsWith(designator.Pattern.Domain, designator.Pattern.Path));
354 foreach (var match in matched)
356 Logger.VerboseDebug("Linked Entity: {0} Designator: {1}", match.Code, designator);
357 this.Entity_Designators.Add(match.Code, designator);
360 //EntityProperties props = ClientAPI.World.GetEntityType(designator.Pattern);
369 /// Store Points/Entity of Interest
371 private void PersistPointsData()
373 //POI and EOI raw dump files ~ WRITE em!
374 //var poiRawFile = File.
375 string poiPath = Path.Combine(path, poiFileName);
376 string eoiPath = Path.Combine(path, eoiFileName);
378 if (this.POIs.Count > 0)
380 using (var poiFile = File.OpenWrite(poiPath))
382 Serializer.Serialize<PointsOfInterest>(poiFile, this.POIs);
386 if (this.EOIs.Count > 0)
388 using (var eoiFile = File.OpenWrite(eoiPath))
390 Serializer.Serialize<EntitiesOfInterest>(eoiFile, this.EOIs);
394 //Create Easy to Parse TSV file for tool/human use....
395 string pointsTsvPath = Path.Combine(path, pointsTsvFileName);
397 using (var tsvWriter = new StreamWriter(pointsTsvPath, false, Encoding.UTF8))
399 tsvWriter.WriteLine("Name\tDescription\tLocation\tTime\t");
400 foreach (var point in this.POIs)
402 tsvWriter.Write(point.Name + "\t");
403 var notes = point.Notes
404 .Replace("\n", "\\n")
405 .Replace("\t", "\\t")
406 .Replace("\\", "\\\\");
407 tsvWriter.Write(notes + "\t");
408 tsvWriter.Write(point.Location.PrettyCoords(ClientAPI) + "\t");
409 tsvWriter.Write(point.Timestamp.ToString("u") + "\t");
410 tsvWriter.WriteLine();
412 foreach (var entity in this.EOIs)
414 tsvWriter.Write(entity.Name + "\t");
415 var notes = entity.Notes
416 .Replace("\n", "\\n")
417 .Replace("\t", "\\t")
418 .Replace("\\", "\\\\");
419 tsvWriter.Write(notes + "\t");
420 tsvWriter.Write(entity.Location.PrettyCoords(ClientAPI) + "\t");
421 tsvWriter.Write(entity.Timestamp.ToString("u") + "\t");
422 tsvWriter.WriteLine();
424 tsvWriter.WriteLine();
430 private ColumnMeta CreateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
432 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy(), ClientAPI, (byte) chunkSize);
433 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * chunkSize,
435 mostActiveCol.Key.Y * chunkSize);
437 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
438 data.UpdateFieldsFrom(climate, mapChunk, TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
444 /// Reload chunk bounds from chunk shards
446 /// <returns>The metadata.</returns>
447 private void Reload_Metadata()
449 var shardsDir = new DirectoryInfo( Path.Combine(path, _chunkPath) );
451 if (!shardsDir.Exists)
454 Logger.VerboseDebug("Could not open world map (shards) directory");
458 var shardFiles = shardsDir.GetFiles(chunkFile_filter);
460 if (shardFiles.Length > 0)
463 Logger.VerboseDebug("Metadata reloading from {0} shards", shardFiles.Length);
466 foreach (var shardFile in shardFiles)
469 if (shardFile.Length < 1024) continue;
470 var result = chunkShardRegex.Match(shardFile.Name);
471 if (!result.Success) continue;
473 int X_chunk_pos = int.Parse(result.Groups["X"].Value);
474 int Z_chunk_pos = int.Parse(result.Groups["Z"].Value);
478 using (var fileStream = shardFile.OpenRead())
481 PngReader pngRead = new PngReader(fileStream);
482 pngRead.ReadSkippingAllRows();
484 //Parse PNG chunks for METADATA in shard
485 PngMetadataChunk metadataFromPng = pngRead.GetChunksList().GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
486 var column = metadataFromPng.ChunkMetadata;
487 if (column.PrettyLocation == null)
488 column = column.Reload(ClientAPI);
489 chunkTopMetadata.Add(column);
493 catch (PngjException someEx)
495 Logger.Error("PNG Corruption file '{0}' - Reason: {1}", shardFile.Name, someEx);
501 //POI and EOI raw dump files ~ reload em!
502 //var poiRawFile = File.
503 string poiPath = Path.Combine(path, poiFileName);
504 string eoiPath = Path.Combine(path, eoiFileName);
506 if (File.Exists(poiPath))
508 using (var poiFile = File.OpenRead(poiPath))
510 this.POIs = Serializer.Deserialize<PointsOfInterest>(poiFile);
511 Logger.VerboseDebug("Reloaded {0} POIs from file.", this.POIs.Count);
515 if (File.Exists(eoiPath))
517 using (var eoiFile = File.OpenRead(eoiPath))
519 this.EOIs = Serializer.Deserialize<EntitiesOfInterest>(eoiFile);
520 Logger.VerboseDebug("Reloaded {0} EOIs from file.", this.EOIs.Count);
526 private PngWriter SetupPngImage(Vec2i coord, ref ColumnMeta metadata)
528 ImageInfo imageInf = new ImageInfo(chunkSize, chunkSize, 8, false);
530 string filename = $"{coord.X}_{coord.Y}.png";
531 filename = Path.Combine(path, _chunkPath ,filename);
533 PngWriter pngWriter = FileHelper.CreatePngWriter(filename, imageInf, true);
534 PngMetadata meta = pngWriter.GetMetadata();
536 meta.SetText("Chunk_X", coord.X.ToString("D"));
537 meta.SetText("Chunk_Y", coord.Y.ToString("D"));
538 //Setup specialized meta-data PNG chunks here...
539 PngMetadataChunk pngChunkMeta = new PngMetadataChunk(pngWriter.ImgInfo)
541 ChunkMetadata = metadata
543 pngWriter.GetChunksList().Queue(pngChunkMeta);
544 pngWriter.CompLevel = 5;// 9 is the maximum compression but thats too high for the small benefit it gives
545 pngWriter.CompressionStrategy = Hjg.Pngcs.Zlib.EDeflateCompressStrategy.Huffman;
551 /// Does the heavy lifting of Scanning columns of chunks - scans for BlockEntity, creates Heightmap and stats...
553 /// <param name="key">Chunk Coordinate</param>
554 /// <param name="mapChunk">Map chunk.</param>
555 /// <param name="chunkMeta">Chunk metadata</param>
556 private void ProcessChunkBlocks(Vec2i key, IMapChunk mapChunk, ref ColumnMeta chunkMeta)
559 int targetChunkY = mapChunk.YMax / chunkSize;//Surface ...
560 for (; targetChunkY > 0; targetChunkY--)
562 WorldChunk chunkData = ClientAPI.World.BlockAccessor.GetChunk(key.X, targetChunkY, key.Y) as WorldChunk;
564 if (chunkData == null || chunkData.BlockEntities == null)
567 Logger.VerboseDebug("Chunk null or empty X{0} Y{1} Z{2}", key.X, targetChunkY, key.Y);
573 /*************** Chunk Entities Scanning *********************/
574 if (chunkData.BlockEntities != null && chunkData.BlockEntities.Length > 0)
577 Logger.VerboseDebug("Surface@ {0} = BlockEntities: {1}", key, chunkData.BlockEntities.Length);
580 foreach (var blockEnt in chunkData.BlockEntities)
582 if (blockEnt != null && blockEnt.Block != null && BlockID_Designators.ContainsKey(blockEnt.Block.BlockId))
584 var designator = BlockID_Designators[blockEnt.Block.BlockId];
585 designator.SpecialAction(ClientAPI, POIs, blockEnt.Pos.Copy(), blockEnt.Block);
589 /********************* Chunk/Column BLOCKs scanning ****************/
590 //Heightmap, Stats, block tally
593 //int X_index, Y_index, Z_index;
595 //Ensure ChunkData Metadata fields arn't null...due to being tossed out
596 //if (chunkMeta.HeightMap == null) { chunkMeta.HeightMap = new ushort[chunkSize, chunkSize]; }
597 //if (chunkMeta.RockRatio == null) { chunkMeta.RockRatio = new Dictionary<int, uint>(10); }
599 //for (Y_index = 0; Y_index < chunkSize - 1; Y_index++)
601 // for (Z_index = 0; Z_index < chunkSize - 1; Z_index++)
603 // for (X_index = 0; X_index < chunkSize - 1; X_index++)
605 // /* Encode packed indicie
606 // (y * chunksize + z) * chunksize + x
608 // var indicie = Helpers.ChunkBlockIndicie16(X_index, Y_index, Z_index);
609 // int aBlockId = chunkData.Blocks[indicie];
611 // if (aBlockId == 0)
613 // chunkMeta.AirBlocks++;
617 // if (RockIdCodes.ContainsKey(aBlockId))
619 // if (chunkMeta.RockRatio.ContainsKey(aBlockId))
620 // chunkMeta.RockRatio[aBlockId]++;
622 // chunkMeta.RockRatio.Add(aBlockId, 1);
625 // chunkMeta.NonAirBlocks++;
628 // //if (chunkMeta.HeightMap[X_index, Z_index] == 0)
631 // // chunkMeta.HeightMap[X_index, Z_index] = (ushort) (Y_index + (targetChunkY * chunkSize));
641 private void UpdateEntityMetadata()
643 Logger.Debug("Presently {0} Entities", ClientAPI.World.LoadedEntities.Count);
644 //Mabey scan only for 'new' entities by tracking ID in set?
645 foreach (var loadedEntity in ClientAPI.World.LoadedEntities.ToArray())
649 //Logger.VerboseDebug($"ENTITY: ({loadedEntity.Value.Code}) = #{loadedEntity.Value.EntityId} {loadedEntity.Value.State} {loadedEntity.Value.LocalPos} <<<<<<<<<<<<");
652 var dMatch = Entity_Designators.SingleOrDefault(se => se.Key.Equals(loadedEntity.Value.Code));
653 if (dMatch.Value != null)
655 dMatch.Value.SpecialAction(ClientAPI, this.EOIs, loadedEntity.Value.LocalPos.AsBlockPos.Copy(), loadedEntity.Value);
663 private void AddNote(string notation)
665 var playerNodePoi = new PointOfInterest()
668 Location = ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Copy(),
670 Timestamp = DateTime.UtcNow,
673 this.POIs.AddReplace(playerNodePoi);
678 private void CommandListener(string eventName, ref EnumHandling handling, IAttribute data)
680 //Logger.VerboseDebug("MsgBus RX: AutomapCommandMsg: {0}", data.ToJsonToken());
682 CommandData cmdData = data as CommandData;
684 switch (cmdData.State)
686 case CommandType.Run:
687 case CommandType.Stop:
688 case CommandType.Snapshot:
689 if (CurrentState != cmdData.State)
691 CurrentState = cmdData.State;
692 AwakenCartographer(0.0f);
696 case CommandType.Notation:
697 //Add to POI list where player location
698 AddNote(cmdData.Notation);
702 ClientAPI.TriggerChatMessage($"Automap commanded to: {cmdData.State} ");