using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
-
+using System.Collections.Concurrent;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Datastructures;
+
namespace Automap
{
public class AutomapMod : ModSystem
private ICoreClientAPI ClientAPI { get; set; }
private ILogger Logger { get; set; }
+ private Thread cartographer_thread;
+
private const string _mapPath = @"Maps";
private const string _chunkPath = @"Chunks";
- private readonly object colLock = new object( );
- private Dictionary<Vec2i, uint> columnCounter = new Dictionary<Vec2i, uint>();
+
+ private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>();
private HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>();
private Vec2i startPosition;
-
- private Thread cartographer_thread;
+ private List<PointOfInterest> POIs;
+ private Dictionary<int,Designator> BlockID_Designators;
public override bool ShouldLoad(EnumAppSide forSide)
{
if (api.Side == EnumAppSide.Client) {
this.ClientAPI = api as ICoreClientAPI;
this.Logger = Mod.Logger;
-
+
+ ClientAPI.Logger.VerboseDebug("Automap Present!");
ClientAPI.Event.LevelFinalize += StartAutomap;
}
#region Internals
private void StartAutomap( )
{
- Mod.Logger.Notification("AUTOMAP SETUP");
- startPosition = new Vec2i(ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X, ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z);
+ Prefill_POI_Designators( );
+ startPosition = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / ClientAPI.World.BlockAccessor.ChunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z/ ClientAPI.World.BlockAccessor.ChunkSize));
+ Logger.Notification("AUTOMAP Start {0}", startPosition);
ClientAPI.Event.ChunkDirty += ChunkAChanging;
cartographer_thread = new Thread(Cartographer);
//Time to (re)write chunk shards
cartographer_thread.Interrupt( );
}
+
+ ClientAPI.TriggerChatMessage($"Automap processed {knownChunkTops.Count} chunks.");
}
}
private void Cartographer( )
{
- wake:
+ wake:
Logger.VerboseDebug("Cartographer thread awoken");
try {
-
+ uint ejectedItem = 0;
+
while (columnCounter.Count > 0)
{
var mostActiveCol = columnCounter.OrderByDescending(kvp => kvp.Value).First();
var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
- Logger.VerboseDebug("Selected: ({0}) - Edits#:{1}", mostActiveCol.Key, mostActiveCol.Value);
+
if (mapChunk == null) {
Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!",mostActiveCol.Key);
- columnCounter.Remove(mostActiveCol.Key);
+
+ columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
continue;
}
string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
-
- filename = Path.Combine(ClientAPI.GetOrCreateDataPath(_mapPath), filename);
-
- var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk);
+ string path = ClientAPI.GetOrCreateDataPath(_mapPath);
+ path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));
+
+ filename = Path.Combine(path, filename);
+ uint pixels = 0;
+ var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk, out pixels);
+
+ if (pixels > 0) {
chkImg.Save(filename, ImageFormat.Png);
+ #if DEBUG
+ Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, pixels);
+ #endif
knownChunkTops.Add(mostActiveCol.Key);
- columnCounter.Remove(mostActiveCol.Key);
- }
-
+ columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
+ }
+ else {
+ columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
+ Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
+ }
+ }
+
+
//Then sleep until interupted again, and repeat
Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
{
//Logger.VerboseDebug($"Change: @({chunkCoord}) R: {reason}");
Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
-
- lock (colLock)
- {
- if (columnCounter.ContainsKey(topPosition)) { columnCounter[topPosition]++; } else { columnCounter.Add(topPosition, 1); }
- }
-
+
+ columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
}
- private void print_stats(float interval)
+ private void Prefill_POI_Designators( )
{
- if (columnCounter != null && columnCounter.Count > 0) {
- foreach (var count in columnCounter) {
- Logger.VerboseDebug($"({count.Key}): {count.Value}");
- }
+ this.POIs = new List<PointOfInterest>( );
+ var roadIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, new AssetLocation("game","stonepath"), EnumBlockMaterial.Gravel);
+
+ //Add special marker types for BlockID's of "Interest", plus a special overwrite colour for them
+ this.BlockID_Designators = new Dictionary<int, Designator>( );
+
+ foreach (var entry in roadIDs) {
+ BlockID_Designators.Add (entry.Key, new Designator
+ (
+ Color.Yellow
+ ));
}
+
+
}
+
#region COPYPASTA
- public Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
+ //A slightly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
+ internal Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, out uint pixelCount)
{
+ pixelCount = 0;
BlockPos tmpPos = new BlockPos( );
Vec2i localpos = new Vec2i( );
int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
chunksColumn[localChunkY].Unpack( );
int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
+
Block block = ClientAPI.World.Blocks[blockId];
tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
int avgCol = block.GetColor(ClientAPI, tmpPos);
int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
//Merge color?
- int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.25f);
+ int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b) | 255 << 24;//Is the Struct, truly so undesirable?
Color pixelColor = Color.FromArgb(ColorUtil.ColorR(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorB(packedFormat));
+
+ //============ POI Population =================
+ if (BlockID_Designators.ContainsKey(blockId)) {
+ var desig = BlockID_Designators[blockId];
+ pixelColor = desig.OverwriteColor;
+
+ if (desig.SpecialAction != null) {
+ desig.SpecialAction.Invoke(tmpPos, block);
+ }
+ }
+
chunkImage.SetPixel(localX,localZ, pixelColor);
+ pixelCount++;
}
--- /dev/null
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+using Vintagestory.API.Client;
+using Vintagestory.API.Common;
+using Vintagestory.API.MathTools;
+
+namespace Automap
+{
+ public static class Helpers
+ {
+
+ /// <summary>
+ /// Hue, Saturation Value colorspace
+ /// </summary>
+ /// <returns>The color equiv.</returns>
+ /// <param name="hue">0 - 360 for hue.</param>
+ /// <param name="saturation"> 0 - 1 for saturation or value..</param>
+ /// <param name="value"> 0 - 1 for saturation or value..</param>
+ public static Color FromHSV(double hue, double saturation, double value)
+ {
+ int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
+ double f = hue / 60 - Math.Floor(hue / 60);
+
+ value = value * 255;
+ int v = Convert.ToInt32(value);
+ int p = Convert.ToInt32(value * (1 - saturation));
+ int q = Convert.ToInt32(value * (1 - f * saturation));
+ int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));
+
+ if (hi == 0)
+ return Color.FromArgb(255, v, t, p);
+ else if (hi == 1)
+ return Color.FromArgb(255, q, v, p);
+ else if (hi == 2)
+ return Color.FromArgb(255, p, v, t);
+ else if (hi == 3)
+ return Color.FromArgb(255, p, q, v);
+ else if (hi == 4)
+ return Color.FromArgb(255, t, p, v);
+ else
+ return Color.FromArgb(255, v, p, q);
+ }
+
+ public static string PrettyCoords(this BlockPos location, ICoreClientAPI ClientApi)
+ {
+ var start = ClientApi.World.DefaultSpawnPosition.AsBlockPos;
+
+ return string.Format("X{0}, Y{1}, Z{2}", location.X - start.X, location.Y, location.Z - start.Z );
+ }
+
+ public static BlockPos AverageHighestPos(List<BlockPos> positions)
+ {
+ int x = 0, y = 0, z = 0, length = positions.Count;
+ foreach (BlockPos pos in positions)
+ {
+ x += pos.X;
+ y = Math.Max(y, pos.Y);//Mutant Y-axis, take "HIGHEST"
+ z += pos.Z;
+ }
+ return new BlockPos(x/ length, y, z / length);
+ }
+
+ public static BlockPos PickRepresentativePosition(List<BlockPos> positions)
+ {
+ var averagePos = AverageHighestPos( positions );
+ if ( positions.Any( pos => pos.X == averagePos.X && pos.Y == averagePos.Y && pos.Z == averagePos.Z ) ) {
+ return averagePos;//lucky ~ center was it!
+ }
+
+ //Otherwise...pick one
+ var whichever = positions.Last(poz => poz.Y == averagePos.Y);
+
+ return whichever;
+ }
+
+
+
+ /// <summary>
+ /// Find a BLOCK partial path match: BlockID
+ /// </summary>
+ /// <returns>Matching finds</returns>
+ /// <param name="assetName">Asset name.</param>
+ public static Dictionary<int, string> ArbitrarytBlockIdHunter(this ICoreAPI CoreApi ,AssetLocation assetName, EnumBlockMaterial? material = null)
+ {
+ Dictionary<int, string> arbBlockIDTable = new Dictionary<int, string>( );
+ uint emptyCount = 0;
+
+ if (CoreApi.World.Blocks != null) {
+
+ #if DEBUG
+ CoreApi.World.Logger.VerboseDebug(" World Blocks [Count: {0}]", CoreApi.World.Blocks.Count);
+ #endif
+ //If Brute force won't work; use GROOT FORCE!
+ //var theBlock = ClientApi.World.BlockAccessor.GetBlock(0);
+
+ if (!material.HasValue) {
+ foreach (Block blk in CoreApi.World.Blocks) {
+ if (blk.IsMissing || blk.Id == 0 || blk.BlockId == 0) {
+ emptyCount++;
+ } else if (blk.Code != null && blk.Code.BeginsWith(assetName.Domain, assetName.Path)) {
+ #if DEBUG
+ //CoreApi.World.Logger.VerboseDebug("Block: [{0} ({1})] = #{2}", blk.Code.Path, blk.BlockMaterial, blk.BlockId);
+ #endif
+
+ arbBlockIDTable.Add(blk.BlockId, blk.Code.Path);
+ }
+ }
+ } else {
+ foreach (Block blk in CoreApi.World.Blocks) {
+ if (blk.IsMissing || blk.Id == 0 || blk.BlockId == 0) {
+ emptyCount++;
+ } else if (blk.Code != null && material.Value == blk.BlockMaterial && blk.Code.BeginsWith(assetName.Domain, assetName.Path)) {
+ #if DEBUG
+ //CoreApi.World.Logger.VerboseDebug("Block: [{0} ({1})] = #{2}", blk.Code.Path, blk.BlockMaterial, blk.BlockId);
+ #endif
+
+ arbBlockIDTable.Add(blk.BlockId, blk.Code.Path);
+ }
+ }
+ }
+
+ #if DEBUG
+ CoreApi.World.Logger.VerboseDebug("Block gaps: {0}", emptyCount);
+ #endif
+ }
+
+ return arbBlockIDTable;
+ }
+
+
+
+ /// <summary>
+ /// Chunk local index. Not block position!
+ /// </summary>
+ /// <remarks>Clamps to 5 bit ranges automagically</remarks>
+ public static int ChunkBlockIndicie16(int X_index, int Y_index, int Z_index)
+ {
+ return ((Y_index & 31) * 32 + (Z_index & 31)) * 32 + (X_index & 31);
+ }
+
+ /// <summary>
+ /// Chunk index converted from block position (in world)
+ /// </summary>
+ /// <returns>The block indicie.</returns>
+ /// <param name="blockPos">Block position.</param>
+ /// <remarks>Clamps to 5 bit ranges automagically</remarks>
+ public static int ChunkBlockIndicie16(BlockPos blockPos)
+ {
+ //Chunk masked
+ return ((blockPos.Y & 31) * 32 + (blockPos.Z & 31)) * 32 + (blockPos.X & 31);
+ }
+ }
+}
+