source: binary-improvements2/MapRendering/src/MapRenderer.cs@ 437

Last change on this file since 437 was 437, checked in by alloc, 23 months ago

Added GameStats and GamePrefs APIs
Fixed ServerInfo API JSON output

File size: 12.7 KB
RevLine 
[224]1using System;
[325]2using System.Collections;
[224]3using System.Collections.Generic;
4using System.IO;
5using System.Text;
6using System.Threading;
7using UnityEngine;
[329]8using UnityEngine.Profiling;
[402]9using Utf8Json;
10using Webserver.FileCache;
[325]11using Object = UnityEngine.Object;
[224]12
[391]13namespace MapRendering {
14 public class MapRenderer {
15 private static MapRenderer instance;
[224]16
[325]17 private static readonly object lockObject = new object ();
[224]18 public static bool renderingEnabled = true;
[391]19 private readonly MapTileCache cache = new MapTileCache (Constants.MapBlockSize);
[325]20 private readonly Dictionary<Vector2i, Color32[]> dirtyChunks = new Dictionary<Vector2i, Color32[]> ();
21 private readonly MicroStopwatch msw = new MicroStopwatch ();
22 private readonly MapRenderBlockBuffer[] zoomLevelBuffers;
23 private Coroutine renderCoroutineRef;
24 private bool renderingFullMap;
[299]25 private float renderTimeout = float.MaxValue;
[383]26 private bool shutdown;
[224]27
[425]28
29 public static bool Enabled => ConnectionManager.Instance.IsServer && GamePrefs.GetBool (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.EnableMapRendering)));
30
[391]31 private MapRenderer () {
[402]32 Constants.MapDirectory = $"{GameIO.GetSaveGameDir ()}/map";
[224]33
[402]34 if (!LoadMapInfo ()) {
35 WriteMapInfo ();
[224]36 }
37
[391]38 cache.SetZoomCount (Constants.Zoomlevels);
[224]39
[391]40 zoomLevelBuffers = new MapRenderBlockBuffer[Constants.Zoomlevels];
41 for (int i = 0; i < Constants.Zoomlevels; i++) {
[224]42 zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache);
43 }
44
[299]45 renderCoroutineRef = ThreadManager.StartCoroutine (renderCoroutine ());
[224]46 }
47
[423]48 public static bool HasInstance => instance != null;
49
[391]50 public static MapRenderer Instance => instance ??= new MapRenderer ();
[325]51
[402]52 public static AbstractCache GetTileCache () {
[325]53 return Instance.cache;
54 }
55
56 public static void Shutdown () {
[423]57 if (instance == null) {
[383]58 return;
59 }
60
[423]61 instance.shutdown = true;
[383]62
[423]63 if (instance.renderCoroutineRef != null) {
64 ThreadManager.StopCoroutine (instance.renderCoroutineRef);
65 instance.renderCoroutineRef = null;
[299]66 }
[423]67
68 instance = null;
[299]69 }
70
[351]71 public static void RenderSingleChunk (Chunk _chunk) {
[423]72 if (!renderingEnabled || instance == null) {
73 return;
74 }
75
76 // TODO: Replace with regular thread and a blocking queue / set
77 ThreadPool.UnsafeQueueUserWorkItem (_o => {
78 try {
79 if (instance.renderingFullMap) {
80 return;
81 }
82
83 lock (lockObject) {
84 Chunk c = (Chunk) _o;
85 Vector3i cPos = c.GetWorldPos ();
86 Vector2i cPos2 = new Vector2i (cPos.x / Constants.MapChunkSize,
87 cPos.z / Constants.MapChunkSize);
88
89 ushort[] mapColors = c.GetMapColors ();
90 if (mapColors == null) {
[391]91 return;
92 }
[224]93
[423]94 Color32[] realColors =
95 new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
96 for (int iColors = 0; iColors < mapColors.Length; iColors++) {
97 realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
98 }
[325]99
[423]100 instance.dirtyChunks [cPos2] = realColors;
[325]101
[423]102 //Log.Out ("Add Dirty: " + cPos2);
[224]103 }
[423]104 } catch (Exception e) {
105 Log.Out ($"Exception in MapRendering.RenderSingleChunk(): {e}");
106 }
107 }, _chunk);
[224]108 }
109
[325]110 public void RenderFullMap () {
[224]111 MicroStopwatch microStopwatch = new MicroStopwatch ();
112
[369]113 string regionSaveDir = GameIO.GetSaveGameRegionDir ();
[224]114 RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false);
115 Texture2D fullMapTexture = null;
116
[391]117 getWorldExtent (rfm, out Vector2i minChunk, out Vector2i maxChunk, out Vector2i minPos, out Vector2i maxPos, out int widthChunks, out int heightChunks,
118 out int widthPix, out int heightPix);
[224]119
[391]120 Log.Out (
121 $"RenderMap: min: {minChunk.ToString ()}, max: {maxChunk.ToString ()}, minPos: {minPos.ToString ()}, maxPos: {maxPos.ToString ()}, w/h: {widthChunks}/{heightChunks}, wP/hP: {widthPix}/{heightPix}"
[224]122 );
123
124 lock (lockObject) {
[391]125 for (int i = 0; i < Constants.Zoomlevels; i++) {
[224]126 zoomLevelBuffers [i].ResetBlock ();
127 }
128
[391]129 if (Directory.Exists (Constants.MapDirectory)) {
130 Directory.Delete (Constants.MapDirectory, true);
[224]131 }
[325]132
[224]133 WriteMapInfo ();
134
135 renderingFullMap = true;
136
[325]137 if (widthPix <= 8192 && heightPix <= 8192) {
[224]138 fullMapTexture = new Texture2D (widthPix, heightPix);
[325]139 }
[224]140
[391]141 Vector2i curFullMapPos = default;
142 Vector2i curChunkPos = default;
143 for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MapChunkSize) {
[325]144 for (curFullMapPos.y = 0;
145 curFullMapPos.y < heightPix;
[391]146 curFullMapPos.y += Constants.MapChunkSize) {
147 curChunkPos.x = curFullMapPos.x / Constants.MapChunkSize + minChunk.x;
148 curChunkPos.y = curFullMapPos.y / Constants.MapChunkSize + minChunk.y;
[224]149
150 try {
151 long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y);
152 if (rfm.ContainsChunkSync (chunkKey)) {
153 Chunk c = rfm.GetChunkSync (chunkKey);
154 ushort[] mapColors = c.GetMapColors ();
155 if (mapColors != null) {
[325]156 Color32[] realColors =
[391]157 new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
158 for (int iColors = 0; iColors < mapColors.Length; iColors++) {
159 realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
[224]160 }
[325]161
[224]162 dirtyChunks [curChunkPos] = realColors;
[325]163 if (fullMapTexture != null) {
164 fullMapTexture.SetPixels32 (curFullMapPos.x, curFullMapPos.y,
[391]165 Constants.MapChunkSize, Constants.MapChunkSize, realColors);
[325]166 }
[224]167 }
168 }
169 } catch (Exception e) {
[402]170 Log.Out ($"Exception: {e}");
[224]171 }
172 }
173
174 while (dirtyChunks.Count > 0) {
175 RenderDirtyChunks ();
176 }
177
[391]178 Log.Out ($"RenderMap: {curFullMapPos.x}/{widthPix} ({(int)((float)curFullMapPos.x / widthPix * 100)}%)");
[224]179 }
180 }
[346]181
182 rfm.Cleanup ();
[224]183
184 if (fullMapTexture != null) {
185 byte[] array = fullMapTexture.EncodeToPNG ();
[402]186 File.WriteAllBytes ($"{Constants.MapDirectory}/map.png", array);
[325]187 Object.Destroy (fullMapTexture);
[224]188 }
189
190 renderingFullMap = false;
191
[402]192 Log.Out ($"Generating map took: {microStopwatch.ElapsedMilliseconds} ms");
193 Log.Out ($"World extent: {minPos} - {maxPos}");
[224]194 }
195
[326]196 private void SaveAllBlockMaps () {
[391]197 for (int i = 0; i < Constants.Zoomlevels; i++) {
[224]198 zoomLevelBuffers [i].SaveBlock ();
199 }
200 }
[329]201
202 private readonly WaitForSeconds coroutineDelay = new WaitForSeconds (0.2f);
[224]203
[325]204 private IEnumerator renderCoroutine () {
[383]205 while (!shutdown) {
[299]206 lock (lockObject) {
[383]207 if (dirtyChunks.Count > 0 && renderTimeout >= float.MaxValue / 2) {
[299]208 renderTimeout = Time.time + 0.5f;
209 }
[325]210
[299]211 if (Time.time > renderTimeout || dirtyChunks.Count > 200) {
[329]212 Profiler.BeginSample ("RenderDirtyChunks");
[299]213 RenderDirtyChunks ();
[329]214 Profiler.EndSample ();
[299]215 }
216 }
[325]217
[329]218 yield return coroutineDelay;
[224]219 }
220 }
221
[329]222 private readonly List<Vector2i> chunksToRender = new List<Vector2i> ();
223 private readonly List<Vector2i> chunksRendered = new List<Vector2i> ();
224
[325]225 private void RenderDirtyChunks () {
[224]226 msw.ResetAndRestart ();
227
[326]228 if (dirtyChunks.Count <= 0) {
229 return;
230 }
[224]231
[329]232 Profiler.BeginSample ("RenderDirtyChunks.Prepare");
233 chunksToRender.Clear ();
234 chunksRendered.Clear ();
[224]235
[329]236 dirtyChunks.CopyKeysTo (chunksToRender);
[224]237
[329]238 Vector2i chunkPos = chunksToRender [0];
239 chunksRendered.Add (chunkPos);
240
[326]241 //Log.Out ("Start Dirty: " + chunkPos);
[224]242
[391]243 getBlockNumber (chunkPos, out Vector2i block, out _, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MapChunkSize);
[224]244
[391]245 zoomLevelBuffers [Constants.Zoomlevels - 1].LoadBlock (block);
[329]246 Profiler.EndSample ();
[325]247
[329]248 Profiler.BeginSample ("RenderDirtyChunks.Work");
[331]249 // Write all chunks that are in the same image tile of the highest zoom level
[329]250 foreach (Vector2i v in chunksToRender) {
[391]251 getBlockNumber (v, out Vector2i vBlock, out Vector2i vBlockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV,
252 Constants.MapChunkSize);
253 if (!vBlock.Equals (block)) {
254 continue;
255 }
[224]256
[391]257 //Log.Out ("Dirty: " + v + " render: true");
258 chunksRendered.Add (v);
259 if (dirtyChunks [v].Length != Constants.MapChunkSize * Constants.MapChunkSize) {
260 Log.Error (
261 $"Rendering chunk has incorrect data size of {dirtyChunks [v].Length} instead of {Constants.MapChunkSize * Constants.MapChunkSize}");
[325]262 }
[391]263
264 zoomLevelBuffers [Constants.Zoomlevels - 1]
265 .SetPart (vBlockOffset, Constants.MapChunkSize, dirtyChunks [v]);
[326]266 }
[329]267 Profiler.EndSample ();
[224]268
[329]269 foreach (Vector2i v in chunksRendered) {
[326]270 dirtyChunks.Remove (v);
271 }
[224]272
[331]273 // Update lower zoom levels affected by the change of the highest one
[326]274 RenderZoomLevel (block);
275
[329]276 Profiler.BeginSample ("RenderDirtyChunks.SaveAll");
[326]277 SaveAllBlockMaps ();
[329]278 Profiler.EndSample ();
[224]279 }
280
[351]281 private void RenderZoomLevel (Vector2i _innerBlock) {
[329]282 Profiler.BeginSample ("RenderZoomLevel");
[391]283 int level = Constants.Zoomlevels - 1;
[326]284 while (level > 0) {
[391]285 getBlockNumber (_innerBlock, out Vector2i block, out Vector2i blockOffset, 2, Constants.MapBlockSize / 2);
[224]286
287 zoomLevelBuffers [level - 1].LoadBlock (block);
288
[329]289 Profiler.BeginSample ("RenderZoomLevel.Transfer");
[331]290 if ((zoomLevelBuffers [level].FormatSelf == TextureFormat.ARGB32 ||
291 zoomLevelBuffers [level].FormatSelf == TextureFormat.RGBA32) &&
292 zoomLevelBuffers [level].FormatSelf == zoomLevelBuffers [level - 1].FormatSelf) {
[391]293 zoomLevelBuffers [level - 1].SetPartNative (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaledNative ());
[329]294 } else {
[391]295 zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaled ());
[329]296 }
297 Profiler.EndSample ();
298
299 level--;
[351]300 _innerBlock = block;
[224]301 }
[329]302 Profiler.EndSample ();
[224]303 }
304
[351]305 private void getBlockNumber (Vector2i _innerPos, out Vector2i _block, out Vector2i _blockOffset, int _scaleFactor,
306 int _offsetSize) {
[391]307 _block = default;
308 _blockOffset = default;
[351]309 _block.x = (_innerPos.x + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
310 _block.y = (_innerPos.y + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
311 _blockOffset.x = (_innerPos.x + 16777216) % _scaleFactor * _offsetSize;
312 _blockOffset.y = (_innerPos.y + 16777216) % _scaleFactor * _offsetSize;
[224]313 }
314
[325]315 private void WriteMapInfo () {
[402]316 JsonWriter writer = new JsonWriter ();
317 writer.WriteBeginObject ();
318
[437]319 writer.WritePropertyName ("blockSize");
[402]320 writer.WriteInt32 (Constants.MapBlockSize);
321
322 writer.WriteValueSeparator ();
[437]323 writer.WritePropertyName ("maxZoom");
[402]324 writer.WriteInt32 (Constants.Zoomlevels - 1);
325
326 writer.WriteEndObject ();
[224]327
[391]328 Directory.CreateDirectory (Constants.MapDirectory);
[402]329 File.WriteAllBytes ($"{Constants.MapDirectory}/mapinfo.json", writer.ToUtf8ByteArray ());
[224]330 }
331
[325]332 private bool LoadMapInfo () {
[402]333 if (!File.Exists ($"{Constants.MapDirectory}/mapinfo.json")) {
[326]334 return false;
335 }
[325]336
[402]337 string json = File.ReadAllText ($"{Constants.MapDirectory}/mapinfo.json", Encoding.UTF8);
[326]338 try {
[402]339 IDictionary<string,object> inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (json);
[325]340
[402]341 if (inputJson.TryGetValue ("blockSize", out object fieldNode) && fieldNode is double value) {
342 Constants.MapBlockSize = (int)value;
343 }
[326]344
[402]345 if (inputJson.TryGetValue ("maxZoom", out fieldNode) && fieldNode is double value2) {
346 Constants.Zoomlevels = (int)value2 + 1;
[224]347 }
[402]348
349 return true;
350 } catch (Exception e) {
351 Log.Out ($"Exception in LoadMapInfo: {e}");
[224]352 }
[325]353
[224]354 return false;
355 }
356
[351]357 private void getWorldExtent (RegionFileManager _rfm, out Vector2i _minChunk, out Vector2i _maxChunk,
358 out Vector2i _minPos, out Vector2i _maxPos,
359 out int _widthChunks, out int _heightChunks,
360 out int _widthPix, out int _heightPix) {
[391]361 _minChunk = default;
362 _maxChunk = default;
363 _minPos = default;
364 _maxPos = default;
[224]365
[351]366 long[] keys = _rfm.GetAllChunkKeys ();
[325]367 int minX = int.MaxValue;
368 int minY = int.MaxValue;
369 int maxX = int.MinValue;
370 int maxY = int.MinValue;
[224]371 foreach (long key in keys) {
372 int x = WorldChunkCache.extractX (key);
373 int y = WorldChunkCache.extractZ (key);
374
[325]375 if (x < minX) {
[224]376 minX = x;
[325]377 }
378
379 if (x > maxX) {
[224]380 maxX = x;
[325]381 }
382
383 if (y < minY) {
[224]384 minY = y;
[325]385 }
386
387 if (y > maxY) {
[224]388 maxY = y;
[325]389 }
[224]390 }
391
[351]392 _minChunk.x = minX;
393 _minChunk.y = minY;
[224]394
[351]395 _maxChunk.x = maxX;
396 _maxChunk.y = maxY;
[224]397
[391]398 _minPos.x = minX * Constants.MapChunkSize;
399 _minPos.y = minY * Constants.MapChunkSize;
[224]400
[391]401 _maxPos.x = maxX * Constants.MapChunkSize;
402 _maxPos.y = maxY * Constants.MapChunkSize;
[224]403
[351]404 _widthChunks = maxX - minX + 1;
405 _heightChunks = maxY - minY + 1;
[224]406
[391]407 _widthPix = _widthChunks * Constants.MapChunkSize;
408 _heightPix = _heightChunks * Constants.MapChunkSize;
[224]409 }
410
[351]411 private static Color32 shortColorToColor32 (ushort _col) {
412 byte r = (byte) (256 * ((_col >> 10) & 31) / 32);
413 byte g = (byte) (256 * ((_col >> 5) & 31) / 32);
414 byte b = (byte) (256 * (_col & 31) / 32);
415 const byte a = 255;
[238]416 return new Color32 (r, g, b, a);
417 }
[224]418 }
419}
Note: See TracBrowser for help on using the repository browser.