OSDN Git Service

W.I.P. #6: GUI incomplete, injected JSON map state on HTML
[automap/automap.git] / Automap / Subsystems / AutomapSystem.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5
6 using System.IO;
7 using System.Linq;
8 using System.Text;
9 using System.Text.RegularExpressions;
10 using System.Threading;
11 using System.Web.UI;
12
13 using Hjg.Pngcs;
14 using Hjg.Pngcs.Chunks;
15
16 using Vintagestory.API.Client;
17 using Vintagestory.API.Common;
18 using Vintagestory.API.MathTools;
19
20
21
22 namespace Automap
23 {
24         public class AutomapSystem
25         {
26                 private Thread cartographer_thread;
27                 private ICoreClientAPI ClientAPI { get; set; }
28                 private ILogger Logger { get; set; }
29
30                 private const string _mapPath = @"Maps";
31                 private const string _chunkPath = @"Chunks";
32                 private const string _domain = @"automap";
33                 private const string chunkFile_filter = @"*_*.png";
34                 private static Regex chunkShardRegex = new Regex(@"(?<X>[\d]+)_(?<Z>[\d]+).png", RegexOptions.Singleline);
35
36                 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150 );
37                 private ColumnsMetadata chunkTopMetadata;
38                 private PointsOfInterest POIs;
39
40                 internal Dictionary<int, Designator> BlockID_Designators { get; private set;}
41                 internal bool Enabled { get; set; }
42                 //Run status, Chunks processed, stats, center of map....
43                 internal uint nullChunkCount;
44                 internal uint updatedChunksTotal;
45                 internal Vec2i startChunkColumn;
46
47
48                 private string path;
49                 private IAsset stylesFile;
50
51
52                 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger)
53                 {
54                 this.ClientAPI = clientAPI;
55                 this.Logger = logger;
56                 ClientAPI.Event.LevelFinalize += EngageAutomap;
57                 }
58
59
60                 #region Internals
61                 private void EngageAutomap( )
62                 {
63                 path = ClientAPI.GetOrCreateDataPath(_mapPath);
64                 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
65
66                 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
67                 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
68
69                 Prefill_POI_Designators( );
70                 startChunkColumn = new Vec2i((ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.X / ClientAPI.World.BlockAccessor.ChunkSize), (ClientAPI.World.Player.Entity.LocalPos.AsBlockPos.Z / ClientAPI.World.BlockAccessor.ChunkSize));
71                 chunkTopMetadata = new ColumnsMetadata(startChunkColumn);
72
73                 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
74                 Reload_Metadata( );
75
76                 ClientAPI.Event.ChunkDirty += ChunkAChanging;
77
78                 cartographer_thread = new Thread(Cartographer);
79                 cartographer_thread.Name = "Cartographer";
80                 cartographer_thread.Priority = ThreadPriority.Lowest;
81                 cartographer_thread.IsBackground = true;
82
83                 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
84                 }
85
86                 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
87                 {                       
88                 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
89
90                         columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
91                 }
92
93                 private void AwakenCartographer(float delayed)
94                 {
95
96                 if (Enabled && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true)) {
97                 #if DEBUG
98                 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
99                 #endif
100
101                 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
102                 cartographer_thread.Start( );
103                 }
104                 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
105                 //Time to (re)write chunk shards
106                 cartographer_thread.Interrupt( );
107                 }
108                 ClientAPI.TriggerChatMessage($"Automap {updatedChunksTotal} Updates - MAX (N:{chunkTopMetadata.North_mostChunk},S:{chunkTopMetadata.South_mostChunk},E:{chunkTopMetadata.East_mostChunk}, W:{chunkTopMetadata.West_mostChunk} - TOTAL: {chunkTopMetadata.Count})");
109                 }
110
111                 }
112
113
114                 private void Cartographer( )
115                 {
116         wake:
117                 Logger.VerboseDebug("Cartographer thread awoken");
118
119                 try {
120                 uint ejectedItem = 0;
121                 uint updatedChunks = 0;
122
123                 //-- Should dodge enumerator changing underfoot....at a cost.
124                 if (!columnCounter.IsEmpty) {
125                 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
126                 foreach (var mostActiveCol in tempSet) {
127
128                 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
129
130                 if (mapChunk == null) {
131                 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
132                 nullChunkCount++;
133                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem );
134                 continue;
135                 }
136                 
137                 ColumnMeta chunkMeta = UpdateColumnMetadata(mostActiveCol,mapChunk);
138                 PngWriter pngWriter = SetupPngImage(mostActiveCol.Key, chunkMeta);
139
140                 uint updatedPixels = 0;
141                 GenerateChunkImage(mostActiveCol.Key, mapChunk, pngWriter , out updatedPixels);
142                 
143                 if (updatedPixels > 0) {                
144                 
145                 #if DEBUG
146                 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
147                 #endif
148                 updatedChunks++;
149                 chunkTopMetadata.Update(chunkMeta);
150                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
151                 }
152                 else {
153                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
154                 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
155                 }
156
157                 }
158                 }
159
160                 if (updatedChunks > 0) {
161                 //TODO: ONLY update if chunk bounds have changed!
162                 updatedChunksTotal += updatedChunks;
163                 GenerateMapHTML( );
164                 updatedChunks = 0;
165                 }
166
167                 //Then sleep until interupted again, and repeat
168
169                 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
170
171                 Thread.Sleep(Timeout.Infinite);
172
173                 } catch (ThreadInterruptedException) {
174
175                 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
176                 goto wake;
177
178                 } catch (ThreadAbortException) {
179                 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
180
181                 } finally {
182                 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
183                 }
184                 }
185
186
187
188
189                 private void Prefill_POI_Designators( )
190                 {
191                 this.POIs = new PointsOfInterest( );
192                 this.BlockID_Designators = new Dictionary<int, Designator>( );
193
194                 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
195
196                 var theDesignators = new List<Designator>{
197                                 DefaultDesignators.Roads,
198                 DefaultDesignators.GroundSigns,
199                 DefaultDesignators.WallSigns,
200                 DefaultDesignators.PostSigns,
201                                 };
202
203                 Install_POI_Designators(theDesignators);
204                 }
205
206                 private void Install_POI_Designators(ICollection<Designator> designators)
207                 {
208                 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
209                 foreach (var designator in designators) {                               
210                         var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
211                                 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
212                         foreach (var entry in blockIDs) {
213                         BlockID_Designators.Add(entry.Key, designator);
214                         }
215                 }
216
217                 }
218
219
220                 private void GenerateMapHTML( )
221                 {
222                 string mapFilename = Path.Combine(path, "Automap.html");
223
224                 int TopNorth = chunkTopMetadata.North_mostChunk;
225                 int TopSouth = chunkTopMetadata.South_mostChunk;
226                 int TopEast = chunkTopMetadata.East_mostChunk;
227                 int TopWest = chunkTopMetadata.West_mostChunk;
228
229                 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))) {
230                 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
231                 tableWriter.BeginRender( );
232                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
233
234                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
235                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
236                 tableWriter.WriteEncodedText("Generated Automap");
237                 tableWriter.RenderEndTag( );
238                 //CSS  style  here
239                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
240                 tableWriter.Write(stylesFile.ToText( ));
241                 tableWriter.RenderEndTag( );//</style>
242
243                 //## JSON map-state data ######################
244                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
245                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Script);
246
247                 tableWriter.Write("var available_images = [");
248
249                 foreach (var shard in this.chunkTopMetadata) {
250                 tableWriter.Write("{{X:{0},Y:{1} }}, ", shard.Location.X, shard.Location.Y);
251                 }
252
253                 tableWriter.Write(" ];\n");
254
255                 tableWriter.RenderEndTag( );
256
257                 tableWriter.RenderEndTag( );
258
259                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
260                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
261                 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
262                 tableWriter.RenderEndTag( );
263                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
264                 tableWriter.WriteEncodedText($"W:{TopWest}, E: {TopEast}, N:{TopNorth}, S:{TopSouth} ");
265                 tableWriter.RenderEndTag( );
266                 tableWriter.WriteLine( );
267                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
268                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
269                 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");             
270                 tableWriter.RenderEndTag( );
271
272                 //################ X-Axis <thead> #######################
273                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
274                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
275
276                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
277                 tableWriter.Write("N, W");
278                 tableWriter.RenderEndTag( );
279
280                 for (int xAxisT = TopWest; xAxisT <= TopEast; xAxisT++) {
281                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
282                 tableWriter.Write(xAxisT);
283                 tableWriter.RenderEndTag( );
284                 }
285
286                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
287                 tableWriter.Write("N, E");
288                 tableWriter.RenderEndTag( );
289                 
290                 tableWriter.RenderEndTag( );
291                 tableWriter.RenderEndTag( );
292                 //###### </thead> ################################
293
294                 //###### <tbody> - Chunk rows & Y-axis cols
295                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
296
297                 //######## <tr> for every vertical row
298                 for (int yAxis = TopNorth; yAxis <= TopSouth; yAxis++) {
299                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
300                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
301                 tableWriter.Write(yAxis);//legend: Y-axis
302                 tableWriter.RenderEndTag( );
303
304                 for (int xAxis = TopWest; xAxis <= TopEast; xAxis++) {
305                 //###### <td>  #### for chunk shard 
306                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
307                 var colLoc = new Vec2i(xAxis, yAxis);
308                 if (chunkTopMetadata.Contains( colLoc)){
309                 ColumnMeta meta = chunkTopMetadata[colLoc];
310                 //Tooltip first                                 
311                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltip");
312                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Div);
313
314                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");          
315                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
316                 tableWriter.RenderEndTag( );
317                 // <span class="tooltiptext">Tooltip text
318                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltiptext");
319                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Span);
320
321                 StringBuilder tooltipText = new StringBuilder( );
322                 tooltipText.Append($"{meta.Location.PrettyCoords(ClientAPI)} ");
323                 tooltipText.Append($" Max-Height: {meta.YMax}, Temp: {meta.Temperature.ToString("F1")} " );
324                 tooltipText.Append($" Rainfall: {meta.Rainfall.ToString("F1")}, ");
325                 tooltipText.Append($" Shrubs: {meta.ShrubDensity.ToString("F1")}, ");
326                 tooltipText.Append($" Forest: {meta.ForestDensity.ToString("F1")}, ");
327                 tooltipText.Append($" Fertility: {meta.Fertility.ToString("F1")}, ");
328
329                 if (meta.RockRatio != null) {
330                 foreach (KeyValuePair<int, uint> blockID in meta.RockRatio) {
331                 var block = ClientAPI.World.GetBlock(blockID.Key);
332                 tooltipText.AppendFormat(" {0} × {1},\t", block.Code.GetName( ), meta.RockRatio[blockID.Key]);
333                 }
334                 }
335
336                 tableWriter.WriteEncodedText(tooltipText.ToString() );
337                 
338                 tableWriter.RenderEndTag( );//</span>
339                                                                                 
340
341                 tableWriter.RenderEndTag( );//</div> --tooltip enclosure
342                 }
343                 else {
344                 tableWriter.Write("?");
345                 }       
346
347                 tableWriter.RenderEndTag( );
348                 }//############ </td> ###########
349
350                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
351                 tableWriter.Write(yAxis);//legend: Y-axis
352                 tableWriter.RenderEndTag( );
353
354                 tableWriter.RenderEndTag( );
355                 
356                 }
357                 tableWriter.RenderEndTag( );
358
359                 //################ X-Axis <tfoot> #######################
360                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
361                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
362
363                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
364                 tableWriter.Write("S, W");
365                 tableWriter.RenderEndTag( );
366
367                 for (int xAxisB = TopWest; xAxisB <= TopEast; xAxisB++) {
368                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
369                 tableWriter.Write(xAxisB);
370                 tableWriter.RenderEndTag( );
371                 }
372
373                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
374                 tableWriter.Write("S, E");
375                 tableWriter.RenderEndTag( );
376
377                 tableWriter.RenderEndTag( );
378                 tableWriter.RenderEndTag( );
379                 //###### </tfoot> ################################
380
381
382                 tableWriter.RenderEndTag( );//</table>
383                 
384                 //############## POI list #####################
385                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
386                 foreach (var poi in this.POIs) {
387                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
388                 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
389                 tableWriter.WriteEncodedText(poi.Notes);
390                 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
391                 tableWriter.RenderEndTag( );
392                 }
393
394                 tableWriter.RenderEndTag( );
395
396                 
397
398
399                 tableWriter.RenderEndTag( );//### </BODY> ###
400                                                         
401                 tableWriter.EndRender( );
402                 tableWriter.Flush( );
403                 }
404                 outputText.Flush( );            
405                 }
406
407                 Logger.VerboseDebug("Generated HTML map");
408                 }
409
410
411
412                 private ColumnMeta UpdateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
413                 {
414                 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy());
415                 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * ClientAPI.World.BlockAccessor.ChunkSize,
416                                                                                 mapChunk.YMax,
417                                                                                 mostActiveCol.Key.Y * ClientAPI.World.BlockAccessor.ChunkSize);
418
419                 var climate = ClientAPI.World.BlockAccessor.GetClimateAt(equivBP);
420                 data.ChunkAge = TimeSpan.FromHours(ClientAPI.World.Calendar.TotalHours);
421                 data.Temperature = climate.Temperature;
422                 data.Fertility = climate.Fertility;
423                 data.ForestDensity = climate.ForestDensity;
424                 data.Rainfall = climate.Rainfall;
425                 data.ShrubDensity = climate.ShrubDensity;
426
427                 data.YMax = mapChunk.YMax;
428
429                 
430                 /* Only present on server....
431                 if (mapChunk.TopRockIdMap != null) {
432                 foreach (var topRockId in mapChunk.TopRockIdMap) {
433
434                 if (data.RockRatio.ContainsKey(topRockId)) { data.RockRatio[topRockId]++; }
435                 else { data.RockRatio.Add(topRockId, 1); }
436                 }
437                 }*/
438
439
440                 return data;
441                 }
442
443                 /// <summary>
444                 /// Reload chunk bounds from chunk shards
445                 /// </summary>
446                 /// <returns>The metadata.</returns>
447                 private void Reload_Metadata( )
448                 {       
449                 var worldmapDir = new DirectoryInfo(path);
450
451                 if (worldmapDir.Exists) {
452
453                 var files = worldmapDir.GetFiles(chunkFile_filter);
454
455                 if (files.Length > 0) {
456                 #if DEBUG
457                 Logger.VerboseDebug("{0} Existing world chunk shards", files.Length);
458                 #endif
459
460                 PngChunk.FactoryRegister(PngMetadataChunk.ID, typeof(PngMetadataChunk));
461
462                 foreach (var shardFile in files) {
463                 var result = chunkShardRegex.Match(shardFile.Name);
464                 if (result.Success) {
465                 int X_chunk_pos = int.Parse(result.Groups["X"].Value );
466                 int Z_chunk_pos = int.Parse(result.Groups["Z"].Value );
467                 
468                 //Parse PNG chunks for METADATA in shard
469                 using (var fileStream = shardFile.OpenRead( ))
470                 {
471                 PngReader pngRead = new PngReader(fileStream );
472                 pngRead.ReadSkippingAllRows( );
473                 pngRead.End( );
474
475                 PngMetadataChunk metadataFromPng = pngRead.GetChunksList( ).GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
476
477                 chunkTopMetadata.Add(metadataFromPng.ChunkMetadata);
478                 }
479                 
480                 }
481                 }
482
483                 }
484                 }
485                 else {
486                 #if DEBUG
487                 Logger.VerboseDebug("Could not open world map directory");
488                 #endif
489                 }
490
491
492
493                 }
494
495                 private PngWriter SetupPngImage(Vec2i coord, ColumnMeta metadata)
496                 {
497                 ImageInfo imageInf = new ImageInfo(ClientAPI.World.BlockAccessor.ChunkSize, ClientAPI.World.BlockAccessor.ChunkSize, 8, false);
498                 
499                 string filename = $"{coord.X}_{coord.Y}.png";
500                 filename = Path.Combine(path, filename);
501
502                 PngWriter pngWriter = FileHelper.CreatePngWriter(filename, imageInf, true);
503                 PngMetadata meta = pngWriter.GetMetadata( );
504                 meta.SetTimeNow( );
505                 meta.SetText("Chunk_X", coord.X.ToString("D"));
506                 meta.SetText("Chunk_Y", coord.Y.ToString("D"));
507                 //Setup specialized meta-data PNG chunks here...
508                 PngMetadataChunk pngChunkMeta = new PngMetadataChunk(pngWriter.ImgInfo);
509                 pngChunkMeta.ChunkMetadata = metadata;          
510                 pngWriter.GetChunksList( ).Queue(pngChunkMeta);
511
512                 return pngWriter;
513                 }
514
515                 #endregion
516
517
518                 #region COPYPASTA
519                 //TODO: rewrite - with vertical ray caster, down to bottom-most chunk (for object detection...)
520                 //A partly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
521                 private void GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, PngWriter pngWriter, out uint pixelCount)
522                 {
523                 pixelCount = 0;
524                 BlockPos tmpPos = new BlockPos( );
525                 Vec2i localpos = new Vec2i( );
526                 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
527                 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
528
529                 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
530                                                                                         //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
531                                                                                         //Grab a chunk COLUMN... Topmost Y down...
532                 for (int chunkY = 0; chunkY <= topChunkY; chunkY++) {
533                 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
534                 //What to do if chunk is a void? invalid?
535                 }
536
537                 // Prefetch map chunks, in pattern
538                 IMapChunk[ ] mapChunks = new IMapChunk[ ]
539                 {
540                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
541                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
542                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
543                 };
544
545                 //pre-create PNG line slices...
546                 ImageLine[ ] lines = Enumerable.Repeat(new object( ), chunkSize).Select(l => new ImageLine(pngWriter.ImgInfo)).ToArray( );
547
548                 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
549                 int mapY = mc.RainHeightMap[posIndex];
550                 int localChunkY = mapY / chunkSize;
551                 if (localChunkY >= (chunksColumn.Length)) continue;//Out of range!
552
553                 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
554                 int localX = localpos.X;
555                 int localZ = localpos.Y;
556
557                 float b = 1;
558                 int leftTop, rightTop, leftBot;
559
560                 IMapChunk leftTopMapChunk = mc;
561                 IMapChunk rightTopMapChunk = mc;
562                 IMapChunk leftBotMapChunk = mc;
563
564                 int topX = localX - 1;
565                 int botX = localX;
566                 int leftZ = localZ - 1;
567                 int rightZ = localZ;
568
569                 if (topX < 0 && leftZ < 0) {
570                 leftTopMapChunk = mapChunks[0];
571                 rightTopMapChunk = mapChunks[1];
572                 leftBotMapChunk = mapChunks[2];
573                 }
574                 else {
575                 if (topX < 0) {
576                 leftTopMapChunk = mapChunks[1];
577                 rightTopMapChunk = mapChunks[1];
578                 }
579                 if (leftZ < 0) {
580                 leftTopMapChunk = mapChunks[2];
581                 leftBotMapChunk = mapChunks[2];
582                 }
583                 }
584
585                 topX = GameMath.Mod(topX, chunkSize);
586                 leftZ = GameMath.Mod(leftZ, chunkSize);
587
588                 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
589                 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
590                 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
591
592                 float slopeness = (leftTop + rightTop + leftBot);
593
594                 if (slopeness > 0) b = 1.2f;
595                 if (slopeness < 0) b = 0.8f;
596
597                 b -= 0.15f; //Slope boost value 
598
599                 if (chunksColumn[localChunkY] == null) {
600
601                 continue;
602                 }
603
604                 chunksColumn[localChunkY].Unpack( );
605                 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
606
607                 Block block = ClientAPI.World.Blocks[blockId];
608
609                 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
610
611                 int avgCol = block.GetColor(ClientAPI, tmpPos);
612                 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
613                 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
614                 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b);
615
616                 int red = ColorUtil.ColorB(packedFormat);
617                 int green = ColorUtil.ColorG(packedFormat);
618                 int blue = ColorUtil.ColorR(packedFormat);
619
620
621                 //============ POI Population =================
622                 if (BlockID_Designators.ContainsKey(blockId)) {
623                 var desig = BlockID_Designators[blockId];
624                 red = desig.OverwriteColor.R;
625                 green = desig.OverwriteColor.G;
626                 blue = desig.OverwriteColor.B;
627
628                 if (desig.SpecialAction != null) {
629                 desig.SpecialAction(ClientAPI, this.POIs, tmpPos, block);
630                 }
631                 }
632
633                 ImageLineHelper.SetPixel(lines[localZ], localX, red, green, blue);
634
635                 //chunkImage.SetPixel(localX, localZ, pixelColor);
636                 pixelCount++;
637                 }
638
639                 for (int row = 0; row < pngWriter.ImgInfo.Rows; row++) {
640                 pngWriter.WriteRow(lines[row], row);
641                 }
642
643                 pngWriter.End( );
644                 }
645                 #endregion
646         }
647
648 }