using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using AllocsFixes.FileCache; using AllocsFixes.JSON; using UnityEngine; using UnityEngine.Profiling; using Object = UnityEngine.Object; namespace MapRendering { public class MapRenderer { private static MapRenderer instance; private static readonly object lockObject = new object (); public static bool renderingEnabled = true; private readonly MapTileCache cache = new MapTileCache (Constants.MapBlockSize); private readonly Dictionary dirtyChunks = new Dictionary (); private readonly MicroStopwatch msw = new MicroStopwatch (); private readonly MapRenderBlockBuffer[] zoomLevelBuffers; private Coroutine renderCoroutineRef; private bool renderingFullMap; private float renderTimeout = float.MaxValue; private bool shutdown; private MapRenderer () { Constants.MapDirectory = GameIO.GetSaveGameDir () + "/map"; lock (lockObject) { if (!LoadMapInfo ()) { WriteMapInfo (); } } cache.SetZoomCount (Constants.Zoomlevels); zoomLevelBuffers = new MapRenderBlockBuffer[Constants.Zoomlevels]; for (int i = 0; i < Constants.Zoomlevels; i++) { zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache); } renderCoroutineRef = ThreadManager.StartCoroutine (renderCoroutine ()); } public static MapRenderer Instance => instance ??= new MapRenderer (); public static MapTileCache GetTileCache () { return Instance.cache; } public static void Shutdown () { if (Instance == null) { return; } Instance.shutdown = true; if (Instance.renderCoroutineRef != null) { ThreadManager.StopCoroutine (Instance.renderCoroutineRef); Instance.renderCoroutineRef = null; } } public static void RenderSingleChunk (Chunk _chunk) { if (renderingEnabled && Instance != null) { // TODO: Replace with regular thread and a blocking queue / set ThreadPool.UnsafeQueueUserWorkItem (_o => { try { if (Instance.renderingFullMap) { return; } lock (lockObject) { Chunk c = (Chunk) _o; Vector3i cPos = c.GetWorldPos (); Vector2i cPos2 = new Vector2i (cPos.x / Constants.MapChunkSize, cPos.z / Constants.MapChunkSize); ushort[] mapColors = c.GetMapColors (); if (mapColors == null) { return; } Color32[] realColors = new Color32[Constants.MapChunkSize * Constants.MapChunkSize]; for (int iColors = 0; iColors < mapColors.Length; iColors++) { realColors [iColors] = shortColorToColor32 (mapColors [iColors]); } Instance.dirtyChunks [cPos2] = realColors; //Log.Out ("Add Dirty: " + cPos2); } } catch (Exception e) { Log.Out ("Exception in MapRendering.RenderSingleChunk(): " + e); } }, _chunk); } } public void RenderFullMap () { MicroStopwatch microStopwatch = new MicroStopwatch (); string regionSaveDir = GameIO.GetSaveGameRegionDir (); RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false); Texture2D fullMapTexture = null; getWorldExtent (rfm, out Vector2i minChunk, out Vector2i maxChunk, out Vector2i minPos, out Vector2i maxPos, out int widthChunks, out int heightChunks, out int widthPix, out int heightPix); Log.Out ( $"RenderMap: min: {minChunk.ToString ()}, max: {maxChunk.ToString ()}, minPos: {minPos.ToString ()}, maxPos: {maxPos.ToString ()}, w/h: {widthChunks}/{heightChunks}, wP/hP: {widthPix}/{heightPix}" ); lock (lockObject) { for (int i = 0; i < Constants.Zoomlevels; i++) { zoomLevelBuffers [i].ResetBlock (); } if (Directory.Exists (Constants.MapDirectory)) { Directory.Delete (Constants.MapDirectory, true); } WriteMapInfo (); renderingFullMap = true; if (widthPix <= 8192 && heightPix <= 8192) { fullMapTexture = new Texture2D (widthPix, heightPix); } Vector2i curFullMapPos = default; Vector2i curChunkPos = default; for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MapChunkSize) { for (curFullMapPos.y = 0; curFullMapPos.y < heightPix; curFullMapPos.y += Constants.MapChunkSize) { curChunkPos.x = curFullMapPos.x / Constants.MapChunkSize + minChunk.x; curChunkPos.y = curFullMapPos.y / Constants.MapChunkSize + minChunk.y; try { long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y); if (rfm.ContainsChunkSync (chunkKey)) { Chunk c = rfm.GetChunkSync (chunkKey); ushort[] mapColors = c.GetMapColors (); if (mapColors != null) { Color32[] realColors = new Color32[Constants.MapChunkSize * Constants.MapChunkSize]; for (int iColors = 0; iColors < mapColors.Length; iColors++) { realColors [iColors] = shortColorToColor32 (mapColors [iColors]); } dirtyChunks [curChunkPos] = realColors; if (fullMapTexture != null) { fullMapTexture.SetPixels32 (curFullMapPos.x, curFullMapPos.y, Constants.MapChunkSize, Constants.MapChunkSize, realColors); } } } } catch (Exception e) { Log.Out ("Exception: " + e); } } while (dirtyChunks.Count > 0) { RenderDirtyChunks (); } Log.Out ($"RenderMap: {curFullMapPos.x}/{widthPix} ({(int)((float)curFullMapPos.x / widthPix * 100)}%)"); } } rfm.Cleanup (); if (fullMapTexture != null) { byte[] array = fullMapTexture.EncodeToPNG (); File.WriteAllBytes (Constants.MapDirectory + "/map.png", array); Object.Destroy (fullMapTexture); } renderingFullMap = false; Log.Out ("Generating map took: " + microStopwatch.ElapsedMilliseconds + " ms"); Log.Out ("World extent: " + minPos + " - " + maxPos); } private void SaveAllBlockMaps () { for (int i = 0; i < Constants.Zoomlevels; i++) { zoomLevelBuffers [i].SaveBlock (); } } private readonly WaitForSeconds coroutineDelay = new WaitForSeconds (0.2f); private IEnumerator renderCoroutine () { while (!shutdown) { lock (lockObject) { if (dirtyChunks.Count > 0 && renderTimeout >= float.MaxValue / 2) { renderTimeout = Time.time + 0.5f; } if (Time.time > renderTimeout || dirtyChunks.Count > 200) { Profiler.BeginSample ("RenderDirtyChunks"); RenderDirtyChunks (); Profiler.EndSample (); } } yield return coroutineDelay; } } private readonly List chunksToRender = new List (); private readonly List chunksRendered = new List (); private void RenderDirtyChunks () { msw.ResetAndRestart (); if (dirtyChunks.Count <= 0) { return; } Profiler.BeginSample ("RenderDirtyChunks.Prepare"); chunksToRender.Clear (); chunksRendered.Clear (); dirtyChunks.CopyKeysTo (chunksToRender); Vector2i chunkPos = chunksToRender [0]; chunksRendered.Add (chunkPos); //Log.Out ("Start Dirty: " + chunkPos); getBlockNumber (chunkPos, out Vector2i block, out _, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MapChunkSize); zoomLevelBuffers [Constants.Zoomlevels - 1].LoadBlock (block); Profiler.EndSample (); Profiler.BeginSample ("RenderDirtyChunks.Work"); // Write all chunks that are in the same image tile of the highest zoom level foreach (Vector2i v in chunksToRender) { getBlockNumber (v, out Vector2i vBlock, out Vector2i vBlockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MapChunkSize); if (!vBlock.Equals (block)) { continue; } //Log.Out ("Dirty: " + v + " render: true"); chunksRendered.Add (v); if (dirtyChunks [v].Length != Constants.MapChunkSize * Constants.MapChunkSize) { Log.Error ( $"Rendering chunk has incorrect data size of {dirtyChunks [v].Length} instead of {Constants.MapChunkSize * Constants.MapChunkSize}"); } zoomLevelBuffers [Constants.Zoomlevels - 1] .SetPart (vBlockOffset, Constants.MapChunkSize, dirtyChunks [v]); } Profiler.EndSample (); foreach (Vector2i v in chunksRendered) { dirtyChunks.Remove (v); } // Update lower zoom levels affected by the change of the highest one RenderZoomLevel (block); Profiler.BeginSample ("RenderDirtyChunks.SaveAll"); SaveAllBlockMaps (); Profiler.EndSample (); } private void RenderZoomLevel (Vector2i _innerBlock) { Profiler.BeginSample ("RenderZoomLevel"); int level = Constants.Zoomlevels - 1; while (level > 0) { getBlockNumber (_innerBlock, out Vector2i block, out Vector2i blockOffset, 2, Constants.MapBlockSize / 2); zoomLevelBuffers [level - 1].LoadBlock (block); Profiler.BeginSample ("RenderZoomLevel.Transfer"); if ((zoomLevelBuffers [level].FormatSelf == TextureFormat.ARGB32 || zoomLevelBuffers [level].FormatSelf == TextureFormat.RGBA32) && zoomLevelBuffers [level].FormatSelf == zoomLevelBuffers [level - 1].FormatSelf) { zoomLevelBuffers [level - 1].SetPartNative (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaledNative ()); } else { zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaled ()); } Profiler.EndSample (); level--; _innerBlock = block; } Profiler.EndSample (); } private void getBlockNumber (Vector2i _innerPos, out Vector2i _block, out Vector2i _blockOffset, int _scaleFactor, int _offsetSize) { _block = default; _blockOffset = default; _block.x = (_innerPos.x + 16777216) / _scaleFactor - 16777216 / _scaleFactor; _block.y = (_innerPos.y + 16777216) / _scaleFactor - 16777216 / _scaleFactor; _blockOffset.x = (_innerPos.x + 16777216) % _scaleFactor * _offsetSize; _blockOffset.y = (_innerPos.y + 16777216) % _scaleFactor * _offsetSize; } private void WriteMapInfo () { JsonObject mapInfo = new JsonObject (); mapInfo.Add ("blockSize", new JsonNumber (Constants.MapBlockSize)); mapInfo.Add ("maxZoom", new JsonNumber (Constants.Zoomlevels - 1)); Directory.CreateDirectory (Constants.MapDirectory); File.WriteAllText (Constants.MapDirectory + "/mapinfo.json", mapInfo.ToString (), Encoding.UTF8); } private bool LoadMapInfo () { if (!File.Exists (Constants.MapDirectory + "/mapinfo.json")) { return false; } string json = File.ReadAllText (Constants.MapDirectory + "/mapinfo.json", Encoding.UTF8); try { JsonNode node = Parser.Parse (json); if (node is JsonObject jo) { if (jo.ContainsKey ("blockSize")) { Constants.MapBlockSize = ((JsonNumber) jo ["blockSize"]).GetInt (); } if (jo.ContainsKey ("maxZoom")) { Constants.Zoomlevels = ((JsonNumber) jo ["maxZoom"]).GetInt () + 1; } return true; } } catch (MalformedJsonException e) { Log.Out ("Exception in LoadMapInfo: " + e); } catch (InvalidCastException e) { Log.Out ("Exception in LoadMapInfo: " + e); } return false; } private void getWorldExtent (RegionFileManager _rfm, out Vector2i _minChunk, out Vector2i _maxChunk, out Vector2i _minPos, out Vector2i _maxPos, out int _widthChunks, out int _heightChunks, out int _widthPix, out int _heightPix) { _minChunk = default; _maxChunk = default; _minPos = default; _maxPos = default; long[] keys = _rfm.GetAllChunkKeys (); int minX = int.MaxValue; int minY = int.MaxValue; int maxX = int.MinValue; int maxY = int.MinValue; foreach (long key in keys) { int x = WorldChunkCache.extractX (key); int y = WorldChunkCache.extractZ (key); if (x < minX) { minX = x; } if (x > maxX) { maxX = x; } if (y < minY) { minY = y; } if (y > maxY) { maxY = y; } } _minChunk.x = minX; _minChunk.y = minY; _maxChunk.x = maxX; _maxChunk.y = maxY; _minPos.x = minX * Constants.MapChunkSize; _minPos.y = minY * Constants.MapChunkSize; _maxPos.x = maxX * Constants.MapChunkSize; _maxPos.y = maxY * Constants.MapChunkSize; _widthChunks = maxX - minX + 1; _heightChunks = maxY - minY + 1; _widthPix = _widthChunks * Constants.MapChunkSize; _heightPix = _heightChunks * Constants.MapChunkSize; } private static Color32 shortColorToColor32 (ushort _col) { byte r = (byte) (256 * ((_col >> 10) & 31) / 32); byte g = (byte) (256 * ((_col >> 5) & 31) / 32); byte b = (byte) (256 * (_col & 31) / 32); const byte a = 255; return new Color32 (r, g, b, a); } } }