source: TFP-WebServer/MapRendering/src/MapRenderer.cs@ 489

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

Added GameStats and GamePrefs APIs
Fixed ServerInfo API JSON output

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