2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
6 using System.Drawing.Imaging;
9 using System.Threading;
13 using Vintagestory.API.Common;
14 using Vintagestory.API.MathTools;
20 public partial class AutomapMod
22 private Thread cartographer_thread;
24 private const string _mapPath = @"Maps";
25 private const string _chunkPath = @"Chunks";
26 private const string _domain = @"automap";
28 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 103 );//ChunkMeta struct?
29 private HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>( );
31 private PointsOfInterest POIs;
32 private Dictionary<int, Designator> BlockID_Designators;
34 private int North_mostChunk;
35 private int East_mostChunk;
36 private int West_mostChunk;
37 private int South_mostChunk;
38 private Vec2i startChunkColumn;
39 private uint lastUpdate;
42 private IAsset stylesFile;
45 private void StartAutomap( )
47 path = ClientAPI.GetOrCreateDataPath(_mapPath);
48 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too!
50 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
51 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
54 Prefill_POI_Designators( );
55 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));
56 North_mostChunk = startChunkColumn.Y;
57 South_mostChunk = startChunkColumn.Y;
58 East_mostChunk = startChunkColumn.X;
59 West_mostChunk = startChunkColumn.X;
61 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
63 ClientAPI.Event.ChunkDirty += ChunkAChanging;
65 cartographer_thread = new Thread(Cartographer);
66 cartographer_thread.Name = "Cartographer";
67 cartographer_thread.Priority = ThreadPriority.Lowest;
68 cartographer_thread.IsBackground = true;
70 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
73 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
75 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
77 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
80 private void AwakenCartographer(float delayed)
83 if (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true) {
85 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
88 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
89 cartographer_thread.Start( );
91 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
92 //Time to (re)write chunk shards
93 cartographer_thread.Interrupt( );
95 ClientAPI.TriggerChatMessage($"Automap {lastUpdate} changes - MAX (N:{North_mostChunk},S:{South_mostChunk},E:{East_mostChunk}, W:{West_mostChunk} - TOTAL: {knownChunkTops.Count})");
101 private void Cartographer( )
104 Logger.VerboseDebug("Cartographer thread awoken");
107 uint ejectedItem = 0;
108 uint updatedChunks = 0;
110 //-- Should dodge enumerator changing underfoot....at a cost.
111 if (!columnCounter.IsEmpty) {
112 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
113 foreach (var mostActiveCol in tempSet) {
115 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
117 if (mapChunk == null) {
118 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
120 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
124 string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
125 filename = Path.Combine(path, filename);
128 var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk, out pixels);
131 chkImg.Save(filename, ImageFormat.Png);
133 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, pixels);
136 knownChunkTops.Add(mostActiveCol.Key);
137 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
140 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
141 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
147 if (updatedChunks > 0) {
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);
174 private void Prefill_POI_Designators( )
176 this.POIs = new PointsOfInterest( );
177 this.BlockID_Designators = new Dictionary<int, Designator>( );
179 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
181 var theDesignators = new List<Designator>{
182 DefaultDesignators.Roads,
183 DefaultDesignators.GroundSigns,
184 DefaultDesignators.WallSigns,
185 DefaultDesignators.PostSigns,
188 Install_POI_Designators(theDesignators);
191 private void Install_POI_Designators(ICollection<Designator> designators)
193 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
194 foreach (var designator in designators) {
195 var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
196 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
197 foreach (var entry in blockIDs) {
198 BlockID_Designators.Add(entry.Key, designator);
207 //TODO: rewrite - with alternate algo.
208 //A slightly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
209 internal Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, out uint pixelCount)
212 BlockPos tmpPos = new BlockPos( );
213 Vec2i localpos = new Vec2i( );
214 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
215 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
216 Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb);
217 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
218 //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
219 //Grab a chunk COLUMN... Topmost Y down...
220 for (int chunkY = 0; chunkY <= topChunkY; chunkY++) {
221 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
222 //What to do if chunk is a void? invalid?
225 // Prefetch map chunks, in pattern
226 IMapChunk[ ] mapChunks = new IMapChunk[ ]
228 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
229 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
230 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
234 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
235 int mapY = mc.RainHeightMap[posIndex];
236 int localChunkY = mapY / chunkSize;
237 if (localChunkY >= (chunksColumn.Length)) continue;//Out of range!
239 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
240 int localX = localpos.X;
241 int localZ = localpos.Y;
244 int leftTop, rightTop, leftBot;
246 IMapChunk leftTopMapChunk = mc;
247 IMapChunk rightTopMapChunk = mc;
248 IMapChunk leftBotMapChunk = mc;
250 int topX = localX - 1;
252 int leftZ = localZ - 1;
255 if (topX < 0 && leftZ < 0) {
256 leftTopMapChunk = mapChunks[0];
257 rightTopMapChunk = mapChunks[1];
258 leftBotMapChunk = mapChunks[2];
262 leftTopMapChunk = mapChunks[1];
263 rightTopMapChunk = mapChunks[1];
266 leftTopMapChunk = mapChunks[2];
267 leftBotMapChunk = mapChunks[2];
271 topX = GameMath.Mod(topX, chunkSize);
272 leftZ = GameMath.Mod(leftZ, chunkSize);
274 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
275 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
276 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
278 float slopeness = (leftTop + rightTop + leftBot);
280 if (slopeness > 0) b = 1.2f;
281 if (slopeness < 0) b = 0.8f;
283 b -= 0.15f; //Slope boost value
285 if (chunksColumn[localChunkY] == null) {
290 chunksColumn[localChunkY].Unpack( );
291 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
293 Block block = ClientAPI.World.Blocks[blockId];
295 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
297 int avgCol = block.GetColor(ClientAPI, tmpPos);
298 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
299 //This is still, an abnormal color - the tint is too blue
300 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
301 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b);
303 Color pixelColor = Color.FromArgb(ColorUtil.ColorB(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorR(packedFormat));
305 //============ POI Population =================
306 if (BlockID_Designators.ContainsKey(blockId)) {
307 var desig = BlockID_Designators[blockId];
308 pixelColor = desig.OverwriteColor;
310 if (desig.SpecialAction != null) {
311 desig.SpecialAction(ClientAPI, this.POIs, tmpPos, block);
315 chunkImage.SetPixel(localX, localZ, pixelColor);
325 private void GenerateMapHTML( )
327 string mapFilename = Path.Combine(path, "Automap.html");
329 North_mostChunk = knownChunkTops.Min(tc => tc.Y);
330 South_mostChunk = knownChunkTops.Max(tc => tc.Y);
331 East_mostChunk = knownChunkTops.Max(tc => tc.X);
332 West_mostChunk = knownChunkTops.Min(tc => tc.X);
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:{West_mostChunk}, E: {East_mostChunk}, N:{North_mostChunk}, S:{South_mostChunk} ");
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 = West_mostChunk; xAxisT <= East_mostChunk; 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);
388 //######## <tr> for every vertical row
389 for (int yAxis = North_mostChunk; yAxis <= South_mostChunk; yAxis++) {
390 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
391 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
392 tableWriter.Write(yAxis);//legend: Y-axis
393 tableWriter.RenderEndTag( );
395 for (int xAxis = West_mostChunk; xAxis <= East_mostChunk; xAxis++) {
396 //###### <td> #### for chunk shard
397 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
398 if (knownChunkTops.Contains( new Vec2i(xAxis, yAxis))){
399 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");
400 tableWriter.AddAttribute(HtmlTextWriterAttribute.Alt, $"X{xAxis}, Y{yAxis}");
401 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
402 tableWriter.RenderEndTag( );
405 tableWriter.Write("?");
407 tableWriter.RenderEndTag( );
408 }//############ </td> ###########
410 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
411 tableWriter.Write(yAxis);//legend: Y-axis
412 tableWriter.RenderEndTag( );
414 tableWriter.RenderEndTag( );
415 tableWriter.EndRender( );
416 tableWriter.Flush( );
418 tableWriter.RenderEndTag( );
420 //################ X-Axis <tfoot> #######################
421 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
422 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
424 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
425 tableWriter.Write("S, W");
426 tableWriter.RenderEndTag( );
428 for (int xAxisB = West_mostChunk; xAxisB <= East_mostChunk; xAxisB++) {
429 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
430 tableWriter.Write(xAxisB);
431 tableWriter.RenderEndTag( );
434 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
435 tableWriter.Write("S, E");
436 tableWriter.RenderEndTag( );
438 tableWriter.RenderEndTag( );
439 tableWriter.RenderEndTag( );
440 //###### </tfoot> ################################
443 tableWriter.RenderEndTag( );//</table>
445 //############## POI list #####################
446 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
447 foreach (var poi in this.POIs) {
448 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
449 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
450 tableWriter.WriteEncodedText(poi.Notes);
451 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
452 tableWriter.RenderEndTag( );
455 tableWriter.RenderEndTag( );
456 tableWriter.RenderEndTag( );
457 tableWriter.RenderEndTag( );
459 tableWriter.EndRender( );
460 tableWriter.Flush( );
465 Logger.VerboseDebug("Generated HTML map");