2 using System.Collections.Generic;
3 using System.Collections.ObjectModel;
5 using System.Threading;
7 using System.Drawing.Imaging;
11 using Vintagestory.API.Client;
12 using Vintagestory.API.Common;
13 using Vintagestory.API.Common.Entities;
14 using Vintagestory.API.Config;
15 using Vintagestory.API.MathTools;
16 using Vintagestory.API.Server;
17 using Vintagestory.GameContent;
18 using Vintagestory.API.Datastructures;
23 public class AutomapMod : ModSystem
25 private ICoreAPI API { get; set; }
26 private ICoreClientAPI ClientAPI { get; set; }
27 private ILogger Logger { get; set; }
29 private const string _mapPath = @"Maps";
30 private const string _chunkPath = @"Chunks";
31 private readonly object colLock = new object( );
32 private Dictionary<Vec2i, uint> columnCounter = new Dictionary<Vec2i, uint>();
33 private HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>();
34 private Vec2i startPosition;
36 private Thread cartographer_thread;
38 public override bool ShouldLoad(EnumAppSide forSide)
40 return forSide.IsClient();
43 public override void StartClientSide(ICoreClientAPI api)
47 if (api.Side == EnumAppSide.Client) {
48 this.ClientAPI = api as ICoreClientAPI;
49 this.Logger = Mod.Logger;
51 ClientAPI.Event.LevelFinalize += StartAutomap;
54 base.StartClientSide(api);
57 public override double ExecuteOrder( )
65 private void StartAutomap( )
67 Mod.Logger.Notification("AUTOMAP SETUP");
68 startPosition = new Vec2i(ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X, ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z);
69 ClientAPI.Event.ChunkDirty += ChunkAChanging;
71 cartographer_thread = new Thread(Cartographer);
72 cartographer_thread.Name = "Cartographer";
73 cartographer_thread.Priority = ThreadPriority.Lowest;
74 cartographer_thread.IsBackground = true;
76 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
79 private void AwakenCartographer(float delayed)
82 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))
90 cartographer_thread.Start( );
92 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
93 //Time to (re)write chunk shards
94 cartographer_thread.Interrupt( );
101 private void Cartographer( )
104 Logger.VerboseDebug("Cartographer thread awoken");
108 while (columnCounter.Count > 0)
110 var mostActiveCol = columnCounter.OrderByDescending(kvp => kvp.Value).First();
111 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
112 Logger.VerboseDebug("Selected: ({0}) - Edits#:{1}", mostActiveCol.Key, mostActiveCol.Value);
114 if (mapChunk == null) {
115 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!",mostActiveCol.Key);
116 columnCounter.Remove(mostActiveCol.Key);
120 string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
122 filename = Path.Combine(ClientAPI.GetOrCreateDataPath(_mapPath), filename);
124 var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk);
126 chkImg.Save(filename, ImageFormat.Png);
128 knownChunkTops.Add(mostActiveCol.Key);
129 columnCounter.Remove(mostActiveCol.Key);
134 //Then sleep until interupted again, and repeat
136 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
138 Thread.Sleep(Timeout.Infinite);
140 } catch (ThreadInterruptedException) {
142 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
145 } catch (ThreadAbortException) {
146 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
149 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
156 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
158 //Logger.VerboseDebug($"Change: @({chunkCoord}) R: {reason}");
159 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
163 if (columnCounter.ContainsKey(topPosition)) { columnCounter[topPosition]++; } else { columnCounter.Add(topPosition, 1); }
168 private void print_stats(float interval)
170 if (columnCounter != null && columnCounter.Count > 0) {
171 foreach (var count in columnCounter) {
172 Logger.VerboseDebug($"({count.Key}): {count.Value}");
180 public Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
182 BlockPos tmpPos = new BlockPos( );
183 Vec2i localpos = new Vec2i( );
184 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
185 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
186 Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb );
187 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
188 //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
189 //Grab a chunk COLUMN... Topmost Y down...
190 for (int chunkY = 0; chunkY < topChunkY; chunkY++) {
191 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
192 //What to do if chunk is a void? invalid?
195 // Prefetch map chunks, in pattern
196 IMapChunk[ ] mapChunks = new IMapChunk[ ]
198 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
199 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
200 ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
204 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
205 int mapY = mc.RainHeightMap[posIndex];
206 int localChunkY = mapY / chunkSize;
207 if (localChunkY >= (chunksColumn.Length ) ) continue;//Out of range!
209 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
210 int localX = localpos.X;
211 int localZ = localpos.Y;
214 int leftTop, rightTop, leftBot;
216 IMapChunk leftTopMapChunk = mc;
217 IMapChunk rightTopMapChunk = mc;
218 IMapChunk leftBotMapChunk = mc;
220 int topX = localX - 1;
222 int leftZ = localZ - 1;
225 if (topX < 0 && leftZ < 0) {
226 leftTopMapChunk = mapChunks[0];
227 rightTopMapChunk = mapChunks[1];
228 leftBotMapChunk = mapChunks[2];
232 leftTopMapChunk = mapChunks[1];
233 rightTopMapChunk = mapChunks[1];
236 leftTopMapChunk = mapChunks[2];
237 leftBotMapChunk = mapChunks[2];
241 topX = GameMath.Mod(topX, chunkSize);
242 leftZ = GameMath.Mod(leftZ, chunkSize);
244 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
245 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
246 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
248 float slopeness = (leftTop + rightTop + leftBot);
250 if (slopeness > 0) b = 1.2f;
251 if (slopeness < 0) b = 0.8f;
253 b -= 0.15f; // Map seems overally a bit too bright
255 if (chunksColumn[localChunkY] == null) {
260 chunksColumn[localChunkY].Unpack( );
261 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
262 Block block = ClientAPI.World.Blocks[blockId];
264 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
266 int avgCol = block.GetColor(ClientAPI, tmpPos);
267 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
269 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.25f);
270 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b) | 255 << 24;//Is the Struct, truly so undesirable?
272 Color pixelColor = Color.FromArgb(ColorUtil.ColorR(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorB(packedFormat));
273 chunkImage.SetPixel(localX,localZ, pixelColor);