2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
5 using System.Drawing.Imaging;
8 using System.Threading;
12 using Vintagestory.API.Common;
13 using Vintagestory.API.MathTools;
19 public partial class AutomapMod
21 private Thread cartographer_thread;
23 private const string _mapPath = @"Maps";
24 private const string _chunkPath = @"Chunks";
25 private const string _domain = @"automap";
27 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>( );
28 private HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>( );
30 private List<PointOfInterest> POIs;
31 private Dictionary<int, Designator> BlockID_Designators;
33 private int North_mostChunk;
34 private int East_mostChunk;
35 private int West_mostChunk;
36 private int South_mostChunk;
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));
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);
53 Prefill_POI_Designators( );
54 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));
55 North_mostChunk = startChunkColumn.Y;
56 South_mostChunk = startChunkColumn.Y;
57 East_mostChunk = startChunkColumn.X;
58 West_mostChunk = startChunkColumn.X;
60 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
62 ClientAPI.Event.ChunkDirty += ChunkAChanging;
64 cartographer_thread = new Thread(Cartographer);
65 cartographer_thread.Name = "Cartographer";
66 cartographer_thread.Priority = ThreadPriority.Lowest;
67 cartographer_thread.IsBackground = true;
69 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
72 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
74 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
76 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
79 private void AwakenCartographer(float delayed)
82 if (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true) {
84 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
87 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
88 cartographer_thread.Start( );
90 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
91 //Time to (re)write chunk shards
92 cartographer_thread.Interrupt( );
94 ClientAPI.TriggerChatMessage($"Automap {lastUpdate} changes - MAX (N:{North_mostChunk},S:{South_mostChunk},E:{East_mostChunk}, W:{West_mostChunk} - TOTAL: {knownChunkTops.Count})");
100 private void Cartographer( )
103 Logger.VerboseDebug("Cartographer thread awoken");
106 uint ejectedItem = 0;
107 uint updatedChunks = 0;
109 while (columnCounter.Count > 0) {
110 var mostActiveCol = columnCounter.OrderByDescending(kvp => kvp.Value).First( );
111 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);
125 var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk, out pixels);
128 chkImg.Save(filename, ImageFormat.Png);
130 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, pixels);
133 knownChunkTops.Add(mostActiveCol.Key);
134 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
137 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
138 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
143 if (updatedChunks > 0) {
144 lastUpdate = updatedChunks;
149 //Then sleep until interupted again, and repeat
151 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
153 Thread.Sleep(Timeout.Infinite);
155 } catch (ThreadInterruptedException) {
157 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
160 } catch (ThreadAbortException) {
161 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
164 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
173 private void Prefill_POI_Designators( )
175 this.POIs = new List<PointOfInterest>( );
176 this.BlockID_Designators = new Dictionary<int, Designator>( );
178 //Add special marker types for BlockID's of "Interest", plus a special overwrite colour for them
180 var roadIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, new AssetLocation("game", "stonepath"), EnumBlockMaterial.Gravel);
181 var roadDesignator = new Designator
186 foreach (var entry in roadIDs) {
187 BlockID_Designators.Add(entry.Key, roadDesignator);
197 //TODO: rewrite - with alternate algo.
198 //A slightly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
199 internal Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, out uint pixelCount)
202 BlockPos tmpPos = new BlockPos( );
203 Vec2i localpos = new Vec2i( );
204 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
205 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
206 Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb);
207 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
208 //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
209 //Grab a chunk COLUMN... Topmost Y down...
210 for (int chunkY = 0; chunkY <= topChunkY; chunkY++) {
211 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
212 //What to do if chunk is a void? invalid?
215 // Prefetch map chunks, in pattern
216 IMapChunk[ ] mapChunks = new IMapChunk[ ]
218 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
219 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
220 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
224 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
225 int mapY = mc.RainHeightMap[posIndex];
226 int localChunkY = mapY / chunkSize;
227 if (localChunkY >= (chunksColumn.Length)) continue;//Out of range!
229 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
230 int localX = localpos.X;
231 int localZ = localpos.Y;
234 int leftTop, rightTop, leftBot;
236 IMapChunk leftTopMapChunk = mc;
237 IMapChunk rightTopMapChunk = mc;
238 IMapChunk leftBotMapChunk = mc;
240 int topX = localX - 1;
242 int leftZ = localZ - 1;
245 if (topX < 0 && leftZ < 0) {
246 leftTopMapChunk = mapChunks[0];
247 rightTopMapChunk = mapChunks[1];
248 leftBotMapChunk = mapChunks[2];
252 leftTopMapChunk = mapChunks[1];
253 rightTopMapChunk = mapChunks[1];
256 leftTopMapChunk = mapChunks[2];
257 leftBotMapChunk = mapChunks[2];
261 topX = GameMath.Mod(topX, chunkSize);
262 leftZ = GameMath.Mod(leftZ, chunkSize);
264 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
265 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
266 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
268 float slopeness = (leftTop + rightTop + leftBot);
270 if (slopeness > 0) b = 1.2f;
271 if (slopeness < 0) b = 0.8f;
273 b -= 0.15f; //Slope boost value
275 if (chunksColumn[localChunkY] == null) {
280 chunksColumn[localChunkY].Unpack( );
281 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
283 Block block = ClientAPI.World.Blocks[blockId];
285 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
287 int avgCol = block.GetColor(ClientAPI, tmpPos);
288 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
289 //This is still, an abnormal color - the tint is too blue
290 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
291 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b);
293 Color pixelColor = Color.FromArgb(ColorUtil.ColorR(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorB(packedFormat));
295 //============ POI Population =================
296 if (BlockID_Designators.ContainsKey(blockId)) {
297 var desig = BlockID_Designators[blockId];
298 pixelColor = desig.OverwriteColor;
300 if (desig.SpecialAction != null) {
301 desig.SpecialAction.Invoke(tmpPos, block);
305 chunkImage.SetPixel(localX, localZ, pixelColor);
315 private void GenerateMapHTML( )
317 string mapFilename = Path.Combine(path, "Automap.html");
319 North_mostChunk = knownChunkTops.Min(tc => tc.Y);
320 South_mostChunk = knownChunkTops.Max(tc => tc.Y);
321 East_mostChunk = knownChunkTops.Max(tc => tc.X);
322 West_mostChunk = knownChunkTops.Min(tc => tc.X);
325 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write))) {
326 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
327 tableWriter.BeginRender( );
328 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
330 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
331 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
332 tableWriter.WriteEncodedText("Generated Automap");
333 tableWriter.RenderEndTag( );
335 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
336 tableWriter.Write(stylesFile.ToText( ));
337 tableWriter.RenderEndTag( );//</style>
339 tableWriter.RenderEndTag( );
341 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
342 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
343 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
344 tableWriter.RenderEndTag( );
345 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
346 tableWriter.WriteEncodedText($"W:{West_mostChunk}, E: {East_mostChunk}, N:{North_mostChunk}, S:{South_mostChunk} ");
347 tableWriter.RenderEndTag( );
348 tableWriter.WriteLine( );
349 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
350 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
351 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");
352 tableWriter.RenderEndTag( );
354 //################ X-Axis <thead> #######################
355 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
356 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
358 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
359 tableWriter.Write("N, W");
360 tableWriter.RenderEndTag( );
362 for (int xAxisT = West_mostChunk; xAxisT <= East_mostChunk; xAxisT++) {
363 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
364 tableWriter.Write(xAxisT);
365 tableWriter.RenderEndTag( );
368 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
369 tableWriter.Write("N, E");
370 tableWriter.RenderEndTag( );
372 tableWriter.RenderEndTag( );
373 tableWriter.RenderEndTag( );
374 //###### </thead> ################################
376 //###### <tbody> - Chunk rows & Y-axis cols
377 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
378 //######## <tr> for every vertical row
379 for (int yAxis = North_mostChunk; yAxis <= South_mostChunk; yAxis++) {
380 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
381 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
382 tableWriter.Write(yAxis);//legend: Y-axis
383 tableWriter.RenderEndTag( );
385 for (int xAxis = West_mostChunk; xAxis <= East_mostChunk; xAxis++) {
386 //###### <td> #### for chunk shard
387 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
388 if (knownChunkTops.Contains( new Vec2i(xAxis, yAxis))){
389 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");
390 tableWriter.AddAttribute(HtmlTextWriterAttribute.Alt, $"X{xAxis}, Y{yAxis}");
391 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
392 tableWriter.RenderEndTag( );
395 tableWriter.Write("?");
397 tableWriter.RenderEndTag( );
398 }//############ </td> ###########
400 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
401 tableWriter.Write(yAxis);//legend: Y-axis
402 tableWriter.RenderEndTag( );
404 tableWriter.RenderEndTag( );
405 tableWriter.EndRender( );
406 tableWriter.Flush( );
408 tableWriter.RenderEndTag( );
410 //################ X-Axis <tfoot> #######################
411 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
412 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
414 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
415 tableWriter.Write("S, W");
416 tableWriter.RenderEndTag( );
418 for (int xAxisB = West_mostChunk; xAxisB <= East_mostChunk; xAxisB++) {
419 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
420 tableWriter.Write(xAxisB);
421 tableWriter.RenderEndTag( );
424 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
425 tableWriter.Write("S, E");
426 tableWriter.RenderEndTag( );
428 tableWriter.RenderEndTag( );
429 tableWriter.RenderEndTag( );
430 //###### </tfoot> ################################
433 tableWriter.RenderEndTag( );//</table>
434 tableWriter.EndRender( );
435 tableWriter.Flush( );
440 Logger.VerboseDebug("Generated HTML map");