2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
6 using System.Drawing.Imaging;
10 using System.Text.RegularExpressions;
11 using System.Threading;
15 using Vintagestory.API.Common;
16 using Vintagestory.API.MathTools;
22 public partial class AutomapMod
24 private Thread cartographer_thread;
26 private const string _mapPath = @"Maps";
27 private const string _chunkPath = @"Chunks";
28 private const string _domain = @"automap";
30 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150 );
31 private ColumnsMetadata chunkTopMetadata;
33 private PointsOfInterest POIs;
34 private Dictionary<int, Designator> BlockID_Designators;
37 private Vec2i startChunkColumn;
38 private uint lastUpdate;
41 private IAsset stylesFile;
44 private void StartAutomap( )
46 path = ClientAPI.GetOrCreateDataPath(_mapPath);
47 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
49 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
50 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
52 Prefill_POI_Designators( );
53 startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / ClientAPI.World.BlockAccessor.ChunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z / ClientAPI.World.BlockAccessor.ChunkSize));
54 chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
56 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
59 ClientAPI.Event.ChunkDirty += ChunkAChanging;
61 cartographer_thread = new Thread(Cartographer);
62 cartographer_thread.Name = "Cartographer";
63 cartographer_thread.Priority = ThreadPriority.Lowest;
64 cartographer_thread.IsBackground = true;
66 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
69 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
71 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
73 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
76 private void AwakenCartographer(float delayed)
79 if (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true) {
81 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
84 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
85 cartographer_thread.Start( );
87 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
88 //Time to (re)write chunk shards
89 cartographer_thread.Interrupt( );
91 ClientAPI.TriggerChatMessage($"Automap {lastUpdate} changes - MAX (N:{chunkTopMetadata.North_mostChunk},S:{chunkTopMetadata.South_mostChunk},E:{chunkTopMetadata.East_mostChunk}, W:{chunkTopMetadata.West_mostChunk} - TOTAL: {chunkTopMetadata.Count})");
97 private void Cartographer( )
100 Logger.VerboseDebug("Cartographer thread awoken");
103 uint ejectedItem = 0;
104 uint updatedChunks = 0;
107 //-- Should dodge enumerator changing underfoot....at a cost.
108 if (!columnCounter.IsEmpty) {
109 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
110 foreach (var mostActiveCol in tempSet) {
112 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
114 if (mapChunk == null) {
115 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
117 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem );
121 string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
122 filename = Path.Combine(path, filename);
124 var chunkMeta = UpdateColumnMetadata(mostActiveCol,mapChunk);
127 var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk, out pixels);
130 chkImg.Save(filename, ImageFormat.Png);
132 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, pixels);
135 chunkTopMetadata.Update(chunkMeta);
136 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
139 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
140 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
146 if (updatedChunks > 0) {
147 //TODO: ONLY update if chunk bounds have changed!
148 lastUpdate = updatedChunks;
153 //Then sleep until interupted again, and repeat
155 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
157 Thread.Sleep(Timeout.Infinite);
159 } catch (ThreadInterruptedException) {
161 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
164 } catch (ThreadAbortException) {
165 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
168 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
175 private void Prefill_POI_Designators( )
177 this.POIs = new PointsOfInterest( );
178 this.BlockID_Designators = new Dictionary<int, Designator>( );
180 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
182 var theDesignators = new List<Designator>{
183 DefaultDesignators.Roads,
184 DefaultDesignators.GroundSigns,
185 DefaultDesignators.WallSigns,
186 DefaultDesignators.PostSigns,
189 Install_POI_Designators(theDesignators);
192 private void Install_POI_Designators(ICollection<Designator> designators)
194 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
195 foreach (var designator in designators) {
196 var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
197 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
198 foreach (var entry in blockIDs) {
199 BlockID_Designators.Add(entry.Key, designator);
208 //TODO: rewrite - with vertical ray caster, down to bottom-most chunk (for object detection...)
209 //A slightly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
210 internal Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, out uint pixelCount)
213 BlockPos tmpPos = new BlockPos( );
214 Vec2i localpos = new Vec2i( );
215 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
216 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
217 Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb);
218 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
219 //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
220 //Grab a chunk COLUMN... Topmost Y down...
221 for (int chunkY = 0; chunkY <= topChunkY; chunkY++) {
222 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
223 //What to do if chunk is a void? invalid?
226 // Prefetch map chunks, in pattern
227 IMapChunk[ ] mapChunks = new IMapChunk[ ]
229 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
230 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
231 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
235 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
236 int mapY = mc.RainHeightMap[posIndex];
237 int localChunkY = mapY / chunkSize;
238 if (localChunkY >= (chunksColumn.Length)) continue;//Out of range!
240 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
241 int localX = localpos.X;
242 int localZ = localpos.Y;
245 int leftTop, rightTop, leftBot;
247 IMapChunk leftTopMapChunk = mc;
248 IMapChunk rightTopMapChunk = mc;
249 IMapChunk leftBotMapChunk = mc;
251 int topX = localX - 1;
253 int leftZ = localZ - 1;
256 if (topX < 0 && leftZ < 0) {
257 leftTopMapChunk = mapChunks[0];
258 rightTopMapChunk = mapChunks[1];
259 leftBotMapChunk = mapChunks[2];
263 leftTopMapChunk = mapChunks[1];
264 rightTopMapChunk = mapChunks[1];
267 leftTopMapChunk = mapChunks[2];
268 leftBotMapChunk = mapChunks[2];
272 topX = GameMath.Mod(topX, chunkSize);
273 leftZ = GameMath.Mod(leftZ, chunkSize);
275 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
276 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
277 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
279 float slopeness = (leftTop + rightTop + leftBot);
281 if (slopeness > 0) b = 1.2f;
282 if (slopeness < 0) b = 0.8f;
284 b -= 0.15f; //Slope boost value
286 if (chunksColumn[localChunkY] == null) {
291 chunksColumn[localChunkY].Unpack( );
292 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
294 Block block = ClientAPI.World.Blocks[blockId];
296 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
298 int avgCol = block.GetColor(ClientAPI, tmpPos);
299 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
300 //This is still, an abnormal color - the tint is too blue
301 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
302 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b);
304 Color pixelColor = Color.FromArgb(ColorUtil.ColorB(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorR(packedFormat));
306 //============ POI Population =================
307 if (BlockID_Designators.ContainsKey(blockId)) {
308 var desig = BlockID_Designators[blockId];
309 pixelColor = desig.OverwriteColor;
311 if (desig.SpecialAction != null) {
312 desig.SpecialAction(ClientAPI, this.POIs, tmpPos, block);
316 chunkImage.SetPixel(localX, localZ, pixelColor);
326 private void GenerateMapHTML( )
328 string mapFilename = Path.Combine(path, "Automap.html");
330 int TopNorth = chunkTopMetadata.North_mostChunk;
331 int TopSouth = chunkTopMetadata.South_mostChunk;
332 int TopEast = chunkTopMetadata.East_mostChunk;
333 int TopWest = chunkTopMetadata.West_mostChunk;
335 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))) {
336 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
337 tableWriter.BeginRender( );
338 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
340 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
341 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
342 tableWriter.WriteEncodedText("Generated Automap");
343 tableWriter.RenderEndTag( );
345 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
346 tableWriter.Write(stylesFile.ToText( ));
347 tableWriter.RenderEndTag( );//</style>
349 tableWriter.RenderEndTag( );
351 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
352 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
353 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
354 tableWriter.RenderEndTag( );
355 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
356 tableWriter.WriteEncodedText($"W:{TopWest}, E: {TopEast}, N:{TopNorth}, S:{TopSouth} ");
357 tableWriter.RenderEndTag( );
358 tableWriter.WriteLine( );
359 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
360 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
361 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");
362 tableWriter.RenderEndTag( );
364 //################ X-Axis <thead> #######################
365 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
366 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
368 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
369 tableWriter.Write("N, W");
370 tableWriter.RenderEndTag( );
372 for (int xAxisT = TopWest; xAxisT <= TopEast; xAxisT++) {
373 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
374 tableWriter.Write(xAxisT);
375 tableWriter.RenderEndTag( );
378 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
379 tableWriter.Write("N, E");
380 tableWriter.RenderEndTag( );
382 tableWriter.RenderEndTag( );
383 tableWriter.RenderEndTag( );
384 //###### </thead> ################################
386 //###### <tbody> - Chunk rows & Y-axis cols
387 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
389 //######## <tr> for every vertical row
390 for (int yAxis = TopNorth; yAxis <= TopSouth; yAxis++) {
391 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
392 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
393 tableWriter.Write(yAxis);//legend: Y-axis
394 tableWriter.RenderEndTag( );
396 for (int xAxis = TopWest; xAxis <= TopEast; xAxis++) {
397 //###### <td> #### for chunk shard
398 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
399 var colLoc = new Vec2i(xAxis, yAxis);
400 if (chunkTopMetadata.Contains( colLoc)){
401 ColumnMeta meta = chunkTopMetadata[colLoc];
403 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltip");
404 tableWriter.RenderBeginTag(HtmlTextWriterTag.Div);
406 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");
407 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
408 tableWriter.RenderEndTag( );
409 // <span class="tooltiptext">Tooltip text
410 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltiptext");
411 tableWriter.RenderBeginTag(HtmlTextWriterTag.Span);
413 StringBuilder tooltipText = new StringBuilder( );
414 tooltipText.Append($"{meta.Location.PrettyCoords(ClientAPI)} ");
415 tooltipText.Append($" Max-Height: {meta.YMax}, Temp: {meta.Temperature.ToString("F1")} " );
416 tooltipText.Append($" Rainfall: {meta.Rainfall.ToString("F1")}, ");
417 tooltipText.Append($" Shrubs: {meta.ShrubDensity.ToString("F1")}, ");
418 tooltipText.Append($" Forest: {meta.ForestDensity.ToString("F1")}, ");
419 tooltipText.Append($" Fertility: {meta.Fertility.ToString("F1")}, ");
421 if (meta.RockRatio != null) {
422 foreach (KeyValuePair<int, uint> blockID in meta.RockRatio) {
423 var block = ClientAPI.World.GetBlock(blockID.Key);
424 tooltipText.AppendFormat(" {0} × {1},\t", block.Code.GetName( ), meta.RockRatio[blockID.Key]);
428 tableWriter.WriteEncodedText(tooltipText.ToString() );
430 tableWriter.RenderEndTag( );//</span>
433 tableWriter.RenderEndTag( );//</div> --tooltip enclosure
436 tableWriter.Write("?");
439 tableWriter.RenderEndTag( );
440 }//############ </td> ###########
442 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
443 tableWriter.Write(yAxis);//legend: Y-axis
444 tableWriter.RenderEndTag( );
446 tableWriter.RenderEndTag( );
449 tableWriter.RenderEndTag( );
451 //################ X-Axis <tfoot> #######################
452 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
453 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
455 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
456 tableWriter.Write("S, W");
457 tableWriter.RenderEndTag( );
459 for (int xAxisB = TopWest; xAxisB <= TopEast; xAxisB++) {
460 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
461 tableWriter.Write(xAxisB);
462 tableWriter.RenderEndTag( );
465 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
466 tableWriter.Write("S, E");
467 tableWriter.RenderEndTag( );
469 tableWriter.RenderEndTag( );
470 tableWriter.RenderEndTag( );
471 //###### </tfoot> ################################
474 tableWriter.RenderEndTag( );//</table>
476 //############## POI list #####################
477 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
478 foreach (var poi in this.POIs) {
479 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
480 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
481 tableWriter.WriteEncodedText(poi.Notes);
482 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
483 tableWriter.RenderEndTag( );
486 tableWriter.RenderEndTag( );
487 tableWriter.RenderEndTag( );
490 tableWriter.EndRender( );
491 tableWriter.Flush( );
496 Logger.VerboseDebug("Generated HTML map");
501 private ColumnMeta UpdateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
503 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy());
504 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * ClientAPI.World.BlockAccessor.ChunkSize,
506 mostActiveCol.Key.Y * ClientAPI.World.BlockAccessor.ChunkSize);
508 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
509 data.Temperature = climate.Temperature;
510 data.Fertility = climate.Fertility;
511 data.ForestDensity = climate.ForestDensity;
512 data.Rainfall = climate.Rainfall;
513 data.ShrubDensity = climate.ShrubDensity;
515 data.YMax = mapChunk.YMax;
518 /* Only present on server....
519 if (mapChunk.TopRockIdMap != null) {
520 foreach (var topRockId in mapChunk.TopRockIdMap) {
522 if (data.RockRatio.ContainsKey(topRockId)) { data.RockRatio[topRockId]++; }
523 else { data.RockRatio.Add(topRockId, 1); }
532 /// Reload chunk bounds from chunk shards
534 /// <returns>The metadata.</returns>
535 private void Reload_Metadata( )
537 string chunkFile_filter = @"*_*.png";
538 Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+).png", RegexOptions.Singleline);
540 var worldmapDir = new DirectoryInfo(path);
542 if (worldmapDir.Exists) {
544 var files = worldmapDir.GetFiles(chunkFile_filter);
546 if (files.Length > 0) {
548 Logger.VerboseDebug("{0} Existing world chunk shards", files.Length);
551 foreach (var shardFile in files) {
552 var result = chunkShardRegex.Match(shardFile.Name);
553 if (result.Success) {
554 int X_chunk_pos = int.Parse(result.Groups["X"].Value );
555 int Z_chunk_pos = int.Parse(result.Groups["Z"].Value );
556 //TODO: METADATA from shard
557 chunkTopMetadata.Add(new ColumnMeta(new Vec2i(X_chunk_pos, Z_chunk_pos)));
565 Logger.VerboseDebug("Could not open world map directory");