--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{0287E0DF-B785-40E8-BB50-2D684B45FC61}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <RootNamespace>Automap</RootNamespace>
+ <AssemblyName>Automap</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug</OutputPath>
+ <DefineConstants>DEBUG;</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ <CustomCommands>
+ <CustomCommands>
+ <Command type="AfterBuild" command="7z -tzip a ${ProjectName}_${ProjectConfig}.zip" workingdir="${TargetDir}" />
+ <Command type="AfterClean" command="rm -f *.zip" workingdir="${TargetDir}" />
+ </CustomCommands>
+ </CustomCommands>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release</OutputPath>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <ConsolePause>false</ConsolePause>
+ <CustomCommands>
+ <CustomCommands>
+ <Command type="AfterBuild" command="7z -tzip a ${ProjectName}_${ProjectConfig}.zip" workingdir="${TargetDir}" />
+ <Command type="AfterClean" command="rm -f *.zip" workingdir="${TargetDir}" />
+ </CustomCommands>
+ </CustomCommands>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="VintagestoryAPI">
+ <HintPath>VS_libs\VintagestoryAPI.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="VintagestoryLib">
+ <HintPath>VS_libs\VintagestoryLib.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="VSCreativeMod">
+ <HintPath>VS_libs\VSCreativeMod.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="VSEssentials">
+ <HintPath>VS_libs\VSEssentials.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="VSSurvivalMod">
+ <HintPath>VS_libs\VSSurvivalMod.dll</HintPath>
+ <Private>False</Private>
+ </Reference>
+ <Reference Include="System.Drawing" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="AutomapMod.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <Folder Include="VS_libs\" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="modinfo.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+</Project>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Threading;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+
+
+using Vintagestory.API.Client;
+using Vintagestory.API.Common;
+using Vintagestory.API.Common.Entities;
+using Vintagestory.API.Config;
+using Vintagestory.API.MathTools;
+using Vintagestory.API.Server;
+using Vintagestory.GameContent;
+using Vintagestory.API.Datastructures;
+
+
+namespace Automap
+{
+ public class AutomapMod : ModSystem
+ {
+ private ICoreAPI API { get; set; }
+ private ICoreClientAPI ClientAPI { get; set; }
+ private ILogger Logger { get; set; }
+
+ 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 HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>();
+ private Vec2i startPosition;
+
+ private Thread cartographer_thread;
+
+ public override bool ShouldLoad(EnumAppSide forSide)
+ {
+ return forSide.IsClient();
+ }
+
+ public override void StartClientSide(ICoreClientAPI api)
+ {
+ this.API = api;
+
+ if (api.Side == EnumAppSide.Client) {
+ this.ClientAPI = api as ICoreClientAPI;
+ this.Logger = Mod.Logger;
+
+ ClientAPI.Event.LevelFinalize += StartAutomap;
+ }
+
+ base.StartClientSide(api);
+ }
+
+ public override double ExecuteOrder( )
+ {
+ return 0.2;
+ }
+
+
+
+ #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);
+ ClientAPI.Event.ChunkDirty += ChunkAChanging;
+
+ cartographer_thread = new Thread(Cartographer);
+ cartographer_thread.Name = "Cartographer";
+ cartographer_thread.Priority = ThreadPriority.Lowest;
+ cartographer_thread.IsBackground = true;
+
+ ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
+ }
+
+ private void AwakenCartographer(float delayed)
+ {
+
+ if (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true)
+ {
+ #if DEBUG
+ Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
+ #endif
+
+ if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted))
+ {
+ cartographer_thread.Start( );
+ }
+ else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
+ //Time to (re)write chunk shards
+ cartographer_thread.Interrupt( );
+ }
+ }
+
+ }
+
+
+ private void Cartographer( )
+ {
+ wake:
+ Logger.VerboseDebug("Cartographer thread awoken");
+
+ try {
+
+ 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);
+ continue;
+ }
+
+ string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
+
+ filename = Path.Combine(ClientAPI.GetOrCreateDataPath(_mapPath), filename);
+
+ var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk);
+
+ chkImg.Save(filename, ImageFormat.Png);
+
+ knownChunkTops.Add(mostActiveCol.Key);
+ columnCounter.Remove(mostActiveCol.Key);
+ }
+
+
+
+ //Then sleep until interupted again, and repeat
+
+ Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
+
+ Thread.Sleep(Timeout.Infinite);
+
+ } catch (ThreadInterruptedException) {
+
+ Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
+ goto wake;
+
+ } catch (ThreadAbortException) {
+ Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
+
+ } finally {
+ Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
+ }
+ }
+ #endregion
+
+
+
+ private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
+ {
+ //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); }
+ }
+
+ }
+
+ private void print_stats(float interval)
+ {
+ if (columnCounter != null && columnCounter.Count > 0) {
+ foreach (var count in columnCounter) {
+ Logger.VerboseDebug($"({count.Key}): {count.Value}");
+ }
+ }
+
+ }
+
+
+ #region COPYPASTA
+ public Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
+ {
+ BlockPos tmpPos = new BlockPos( );
+ Vec2i localpos = new Vec2i( );
+ int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
+ var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
+ Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb );
+ int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
+ //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
+ //Grab a chunk COLUMN... Topmost Y down...
+ for (int chunkY = 0; chunkY < topChunkY; chunkY++) {
+ chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
+ //What to do if chunk is a void? invalid?
+ }
+
+ // Prefetch map chunks, in pattern
+ IMapChunk[ ] mapChunks = new IMapChunk[ ]
+ {
+ ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
+ ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
+ ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
+ };
+
+
+ for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
+ int mapY = mc.RainHeightMap[posIndex];
+ int localChunkY = mapY / chunkSize;
+ if (localChunkY >= (chunksColumn.Length ) ) continue;//Out of range!
+
+ MapUtil.PosInt2d(posIndex, chunkSize, localpos);
+ int localX = localpos.X;
+ int localZ = localpos.Y;
+
+ float b = 1;
+ int leftTop, rightTop, leftBot;
+
+ IMapChunk leftTopMapChunk = mc;
+ IMapChunk rightTopMapChunk = mc;
+ IMapChunk leftBotMapChunk = mc;
+
+ int topX = localX - 1;
+ int botX = localX;
+ int leftZ = localZ - 1;
+ int rightZ = localZ;
+
+ if (topX < 0 && leftZ < 0) {
+ leftTopMapChunk = mapChunks[0];
+ rightTopMapChunk = mapChunks[1];
+ leftBotMapChunk = mapChunks[2];
+ }
+ else {
+ if (topX < 0) {
+ leftTopMapChunk = mapChunks[1];
+ rightTopMapChunk = mapChunks[1];
+ }
+ if (leftZ < 0) {
+ leftTopMapChunk = mapChunks[2];
+ leftBotMapChunk = mapChunks[2];
+ }
+ }
+
+ topX = GameMath.Mod(topX, chunkSize);
+ leftZ = GameMath.Mod(leftZ, chunkSize);
+
+ leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
+ rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
+ leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
+
+ float slopeness = (leftTop + rightTop + leftBot);
+
+ if (slopeness > 0) b = 1.2f;
+ if (slopeness < 0) b = 0.8f;
+
+ b -= 0.15f; // Map seems overally a bit too bright
+ //b = 1;
+ if (chunksColumn[localChunkY] == null) {
+
+ continue;
+ }
+
+ 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);
+ 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));
+ chunkImage.SetPixel(localX,localZ, pixelColor);
+ }
+
+
+ return chunkImage;
+ }
+ #endregion
+ }
+}
+