2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
9 using System.Text.RegularExpressions;
10 using System.Threading;
14 using Hjg.Pngcs.Chunks;
16 using Vintagestory.API.Client;
17 using Vintagestory.API.Common;
18 using Vintagestory.API.MathTools;
24 public class AutomapSystem
26 private Thread cartographer_thread;
27 private ICoreClientAPI ClientAPI { get; set; }
28 private ILogger Logger { get; set; }
29 private IChunkRenderer ChunkRenderer { get; set; }
31 private const string _mapPath = @"Maps";
32 private const string _chunkPath = @"Chunks";
33 private const string _domain = @"automap";
34 private const string chunkFile_filter = @"*_*.png";
35 private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+).png", RegexOptions.Singleline);
37 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150 );
38 private ColumnsMetadata chunkTopMetadata;
39 private PointsOfInterest POIs;
41 internal Dictionary<int, Designator> BlockID_Designators { get; private set;}
42 internal bool Enabled { get; set; }
43 //Run status, Chunks processed, stats, center of map....
44 internal uint nullChunkCount;
45 internal uint updatedChunksTotal;
46 internal Vec2i startChunkColumn;
48 private readonly int chunkSize;
50 private IAsset stylesFile;
53 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger)
55 this.ClientAPI = clientAPI;
57 chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
58 ClientAPI.Event.LevelFinalize += EngageAutomap;
60 //TODO:Choose which one from GUI
61 this.ChunkRenderer = new StandardRenderer(clientAPI, logger);
66 private void EngageAutomap( )
68 path = ClientAPI.GetOrCreateDataPath(_mapPath);
69 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
71 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
72 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
74 Prefill_POI_Designators( );
75 startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / chunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z / chunkSize));
76 chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
78 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
81 ClientAPI.Event.ChunkDirty += ChunkAChanging;
83 cartographer_thread = new Thread(Cartographer);
84 cartographer_thread.Name = "Cartographer";
85 cartographer_thread.Priority = ThreadPriority.Lowest;
86 cartographer_thread.IsBackground = true;
88 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
91 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
93 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
95 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
98 private void AwakenCartographer(float delayed)
101 if (Enabled && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true)) {
103 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
106 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
107 cartographer_thread.Start( );
109 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
110 //Time to (re)write chunk shards
111 cartographer_thread.Interrupt( );
113 ClientAPI.TriggerChatMessage($"Automap {updatedChunksTotal} Updates - MAX (N:{chunkTopMetadata.North_mostChunk},S:{chunkTopMetadata.South_mostChunk},E:{chunkTopMetadata.East_mostChunk}, W:{chunkTopMetadata.West_mostChunk} - TOTAL: {chunkTopMetadata.Count})");
119 private void Cartographer( )
122 Logger.VerboseDebug("Cartographer thread awoken");
125 uint ejectedItem = 0;
126 uint updatedChunks = 0;
128 //-- Should dodge enumerator changing underfoot....at a cost.
129 if (!columnCounter.IsEmpty) {
130 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
131 foreach (var mostActiveCol in tempSet) {
133 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
135 if (mapChunk == null) {
136 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
138 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem );
142 ColumnMeta chunkMeta = CreateColumnMetadata(mostActiveCol,mapChunk);
143 PngWriter pngWriter = SetupPngImage(mostActiveCol.Key, chunkMeta);
144 ProcessChunkBlocks(mostActiveCol.Key, mapChunk, chunkMeta);
146 uint updatedPixels = 0;
148 ChunkRenderer.GenerateChunkPngShard(mostActiveCol.Key, mapChunk, chunkMeta, pngWriter , out updatedPixels);
150 if (updatedPixels > 0) {
153 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
156 chunkTopMetadata.Update(chunkMeta);
157 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
160 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
161 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
167 if (updatedChunks > 0) {
168 //What about chunk updates themselves; a update bitmap isn't kept...
169 updatedChunksTotal += updatedChunks;
174 //Then sleep until interupted again, and repeat
176 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
178 Thread.Sleep(Timeout.Infinite);
180 } catch (ThreadInterruptedException) {
182 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
185 } catch (ThreadAbortException) {
186 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
189 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
193 private void Prefill_POI_Designators( )
195 this.POIs = new PointsOfInterest( );
196 this.BlockID_Designators = new Dictionary<int, Designator>( );
198 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
200 var standardDesignators = new List<Designator>{
201 DefaultDesignators.Roads,
202 DefaultDesignators.GroundSigns,
203 DefaultDesignators.WallSigns,
204 DefaultDesignators.PostSigns,
207 Install_POI_Designators(standardDesignators);
210 private void Install_POI_Designators(ICollection<Designator> designators)
212 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
213 foreach (var designator in designators) {
214 var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
215 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
216 foreach (var entry in blockIDs) {
217 BlockID_Designators.Add(entry.Key, designator);
220 this.ChunkRenderer.BlockID_Designators = BlockID_Designators;
223 //TODO: Convert to RAZOR model
224 private void GenerateMapHTML( )
226 string mapFilename = Path.Combine(path, "Automap.html");
228 int TopNorth = chunkTopMetadata.North_mostChunk;
229 int TopSouth = chunkTopMetadata.South_mostChunk;
230 int TopEast = chunkTopMetadata.East_mostChunk;
231 int TopWest = chunkTopMetadata.West_mostChunk;
233 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))) {
234 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
235 tableWriter.BeginRender( );
236 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
238 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
239 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
240 tableWriter.WriteEncodedText("Generated Automap");
241 tableWriter.RenderEndTag( );
243 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
244 tableWriter.Write(stylesFile.ToText( ));
245 tableWriter.RenderEndTag( );//</style>
247 //## JSON map-state data ######################
248 tableWriter.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
249 tableWriter.RenderBeginTag(HtmlTextWriterTag.Script);
251 tableWriter.Write("var available_images = [");
253 foreach (var shard in this.chunkTopMetadata) {
254 tableWriter.Write("{{X:{0},Y:{1} }}, ", shard.Location.X, shard.Location.Y);
257 tableWriter.Write(" ];\n");
259 tableWriter.RenderEndTag( );
261 tableWriter.RenderEndTag( );
263 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
264 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
265 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
266 tableWriter.RenderEndTag( );
267 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
268 tableWriter.WriteEncodedText($"W:{TopWest}, E: {TopEast}, N:{TopNorth}, S:{TopSouth} ");
269 tableWriter.RenderEndTag( );
270 tableWriter.WriteLine( );
271 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
272 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
273 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");
274 tableWriter.RenderEndTag( );
276 //################ X-Axis <thead> #######################
277 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
278 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
280 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
281 tableWriter.Write("N, W");
282 tableWriter.RenderEndTag( );
284 for (int xAxisT = TopWest; xAxisT <= TopEast; xAxisT++) {
285 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
286 tableWriter.Write(xAxisT);
287 tableWriter.RenderEndTag( );
290 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
291 tableWriter.Write("N, E");
292 tableWriter.RenderEndTag( );
294 tableWriter.RenderEndTag( );
295 tableWriter.RenderEndTag( );
296 //###### </thead> ################################
298 //###### <tbody> - Chunk rows & Y-axis cols
299 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
301 //######## <tr> for every vertical row
302 for (int yAxis = TopNorth; yAxis <= TopSouth; yAxis++) {
303 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
304 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
305 tableWriter.Write(yAxis);//legend: Y-axis
306 tableWriter.RenderEndTag( );
308 for (int xAxis = TopWest; xAxis <= TopEast; xAxis++) {
309 //###### <td> #### for chunk shard
310 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
311 var colLoc = new Vec2i(xAxis, yAxis);
312 if (chunkTopMetadata.Contains( colLoc)){
313 ColumnMeta meta = chunkTopMetadata[colLoc];
315 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltip");
316 tableWriter.RenderBeginTag(HtmlTextWriterTag.Div);
318 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");
319 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
320 tableWriter.RenderEndTag( );
321 // <span class="tooltiptext">Tooltip text
322 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltiptext");
323 tableWriter.RenderBeginTag(HtmlTextWriterTag.Span);
325 StringBuilder tooltipText = new StringBuilder( );
326 tooltipText.Append($"{meta.Location.PrettyCoords(ClientAPI)} ");
327 tooltipText.Append($" Max-Height: {meta.YMax}, Temp: {meta.Temperature.ToString("F1")} " );
328 tooltipText.Append($" Rainfall: {meta.Rainfall.ToString("F1")}, ");
329 tooltipText.Append($" Shrubs: {meta.ShrubDensity.ToString("F1")}, ");
330 tooltipText.Append($" Forest: {meta.ForestDensity.ToString("F1")}, ");
331 tooltipText.Append($" Fertility: {meta.Fertility.ToString("F1")}, ");
333 if (meta.RockRatio != null) {
334 foreach (KeyValuePair<int, uint> blockID in meta.RockRatio) {
335 var block = ClientAPI.World.GetBlock(blockID.Key);
336 tooltipText.AppendFormat(" {0} × {1},\t", block.Code.GetName( ), meta.RockRatio[blockID.Key]);
340 tableWriter.WriteEncodedText(tooltipText.ToString() );
342 tableWriter.RenderEndTag( );//</span>
345 tableWriter.RenderEndTag( );//</div> --tooltip enclosure
348 tableWriter.Write("?");
351 tableWriter.RenderEndTag( );
352 }//############ </td> ###########
354 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
355 tableWriter.Write(yAxis);//legend: Y-axis
356 tableWriter.RenderEndTag( );
358 tableWriter.RenderEndTag( );
361 tableWriter.RenderEndTag( );
363 //################ X-Axis <tfoot> #######################
364 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
365 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
367 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
368 tableWriter.Write("S, W");
369 tableWriter.RenderEndTag( );
371 for (int xAxisB = TopWest; xAxisB <= TopEast; xAxisB++) {
372 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
373 tableWriter.Write(xAxisB);
374 tableWriter.RenderEndTag( );
377 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
378 tableWriter.Write("S, E");
379 tableWriter.RenderEndTag( );
381 tableWriter.RenderEndTag( );
382 tableWriter.RenderEndTag( );
383 //###### </tfoot> ################################
386 tableWriter.RenderEndTag( );//</table>
388 //############## POI list #####################
389 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
390 foreach (var poi in this.POIs) {
391 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
392 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
393 tableWriter.WriteEncodedText(poi.Notes);
394 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
395 tableWriter.RenderEndTag( );
398 tableWriter.RenderEndTag( );
403 tableWriter.RenderEndTag( );//### </BODY> ###
405 tableWriter.EndRender( );
406 tableWriter.Flush( );
411 Logger.VerboseDebug("Generated HTML map");
416 private ColumnMeta CreateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
418 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy(), chunkSize);
419 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * chunkSize,
421 mostActiveCol.Key.Y * chunkSize);
423 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
424 data.UpdateFieldsFrom(climate, mapChunk,TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours));
433 /// Reload chunk bounds from chunk shards
435 /// <returns>The metadata.</returns>
436 private void Reload_Metadata( )
438 var worldmapDir = new DirectoryInfo(path);
440 if (worldmapDir.Exists) {
442 var files = worldmapDir.GetFiles(chunkFile_filter);
444 if (files.Length > 0) {
446 Logger.VerboseDebug("{0} Existing world chunk shards", files.Length);
451 foreach (var shardFile in files) {
452 var result = chunkShardRegex.Match(shardFile.Name);
453 if (result.Success) {
454 int X_chunk_pos = int.Parse(result.Groups["X"].Value );
455 int Z_chunk_pos = int.Parse(result.Groups["Z"].Value );
457 //Parse PNG chunks for METADATA in shard
458 using (var fileStream = shardFile.OpenRead( ))
460 PngReader pngRead = new PngReader(fileStream );
461 pngRead.ReadSkippingAllRows( );
464 PngMetadataChunk metadataFromPng = pngRead.GetChunksList( ).GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
466 chunkTopMetadata.Add(metadataFromPng.ChunkMetadata);
476 Logger.VerboseDebug("Could not open world map directory");
484 private PngWriter SetupPngImage(Vec2i coord, ColumnMeta metadata)
486 ImageInfo imageInf = new ImageInfo(chunkSize, chunkSize, 8, false);
488 string filename = $"{coord.X}_{coord.Y}.png";
489 filename = Path.Combine(path, filename);
491 PngWriter pngWriter = FileHelper.CreatePngWriter(filename, imageInf, true);
492 PngMetadata meta = pngWriter.GetMetadata( );
494 meta.SetText("Chunk_X", coord.X.ToString("D"));
495 meta.SetText("Chunk_Y", coord.Y.ToString("D"));
496 //Setup specialized meta-data PNG chunks here...
497 PngMetadataChunk pngChunkMeta = new PngMetadataChunk(pngWriter.ImgInfo);
498 pngChunkMeta.ChunkMetadata = metadata;
499 pngWriter.GetChunksList( ).Queue(pngChunkMeta);
505 /// Does the heavy lifting of Scanning the whole (surface) chunk - creates Heightmap and Processes POIs, and stats...
507 /// <param name="key">Chunk Coordinate</param>
508 /// <param name="mapChunk">Map chunk.</param>
509 /// <param name="chunkMeta">Chunk metadata</param>
510 private void ProcessChunkBlocks(Vec2i key, IMapChunk mapChunk, ColumnMeta chunkMeta)