2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
9 using System.Text.RegularExpressions;
10 using System.Threading;
14 using Hjg.Pngcs.Chunks;
16 using Vintagestory.API.Client;
17 using Vintagestory.API.Common;
18 using Vintagestory.API.MathTools;
24 public class AutomapSystem
26 private Thread cartographer_thread;
27 private ICoreClientAPI ClientAPI { get; set; }
28 private ILogger Logger { get; set; }
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);
36 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 150 );
37 private ColumnsMetadata chunkTopMetadata;
38 private PointsOfInterest POIs;
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;
49 private IAsset stylesFile;
52 public AutomapSystem(ICoreClientAPI clientAPI, ILogger logger)
54 this.ClientAPI = clientAPI;
56 ClientAPI.Event.LevelFinalize += EngageAutomap;
61 private void EngageAutomap( )
63 path = ClientAPI.GetOrCreateDataPath(_mapPath);
64 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too...'ServerApi.WorldManager.CurrentWorldName'
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);
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);
73 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
76 ClientAPI.Event.ChunkDirty += ChunkAChanging;
78 cartographer_thread = new Thread(Cartographer);
79 cartographer_thread.Name = "Cartographer";
80 cartographer_thread.Priority = ThreadPriority.Lowest;
81 cartographer_thread.IsBackground = true;
83 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
86 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
88 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
90 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);
93 private void AwakenCartographer(float delayed)
96 if (Enabled && (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true)) {
98 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
101 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
102 cartographer_thread.Start( );
104 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
105 //Time to (re)write chunk shards
106 cartographer_thread.Interrupt( );
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})");
114 private void Cartographer( )
117 Logger.VerboseDebug("Cartographer thread awoken");
120 uint ejectedItem = 0;
121 uint updatedChunks = 0;
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) {
128 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
130 if (mapChunk == null) {
131 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
133 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem );
137 ColumnMeta chunkMeta = UpdateColumnMetadata(mostActiveCol,mapChunk);
138 PngWriter pngWriter = SetupPngImage(mostActiveCol.Key, chunkMeta);
140 uint updatedPixels = 0;
141 GenerateChunkImage(mostActiveCol.Key, mapChunk, pngWriter , out updatedPixels);
143 if (updatedPixels > 0) {
146 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, updatedPixels);
149 chunkTopMetadata.Update(chunkMeta);
150 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
153 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
154 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
160 if (updatedChunks > 0) {
161 //TODO: ONLY update if chunk bounds have changed!
162 updatedChunksTotal += updatedChunks;
167 //Then sleep until interupted again, and repeat
169 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
171 Thread.Sleep(Timeout.Infinite);
173 } catch (ThreadInterruptedException) {
175 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
178 } catch (ThreadAbortException) {
179 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
182 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
189 private void Prefill_POI_Designators( )
191 this.POIs = new PointsOfInterest( );
192 this.BlockID_Designators = new Dictionary<int, Designator>( );
194 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
196 var theDesignators = new List<Designator>{
197 DefaultDesignators.Roads,
198 DefaultDesignators.GroundSigns,
199 DefaultDesignators.WallSigns,
200 DefaultDesignators.PostSigns,
203 Install_POI_Designators(theDesignators);
206 private void Install_POI_Designators(ICollection<Designator> designators)
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);
220 private void GenerateMapHTML( )
222 string mapFilename = Path.Combine(path, "Automap.html");
224 int TopNorth = chunkTopMetadata.North_mostChunk;
225 int TopSouth = chunkTopMetadata.South_mostChunk;
226 int TopEast = chunkTopMetadata.East_mostChunk;
227 int TopWest = chunkTopMetadata.West_mostChunk;
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);
234 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
235 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
236 tableWriter.WriteEncodedText("Generated Automap");
237 tableWriter.RenderEndTag( );
239 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
240 tableWriter.Write(stylesFile.ToText( ));
241 tableWriter.RenderEndTag( );//</style>
243 //## JSON map-state data ######################
244 tableWriter.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
245 tableWriter.RenderBeginTag(HtmlTextWriterTag.Script);
247 tableWriter.Write("var available_images = [");
249 foreach (var shard in this.chunkTopMetadata) {
250 tableWriter.Write("{{X:{0},Y:{1} }}, ", shard.Location.X, shard.Location.Y);
253 tableWriter.Write(" ];\n");
255 tableWriter.RenderEndTag( );
257 tableWriter.RenderEndTag( );
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( );
272 //################ X-Axis <thead> #######################
273 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
274 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
276 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
277 tableWriter.Write("N, W");
278 tableWriter.RenderEndTag( );
280 for (int xAxisT = TopWest; xAxisT <= TopEast; xAxisT++) {
281 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
282 tableWriter.Write(xAxisT);
283 tableWriter.RenderEndTag( );
286 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
287 tableWriter.Write("N, E");
288 tableWriter.RenderEndTag( );
290 tableWriter.RenderEndTag( );
291 tableWriter.RenderEndTag( );
292 //###### </thead> ################################
294 //###### <tbody> - Chunk rows & Y-axis cols
295 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
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( );
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];
311 tableWriter.AddAttribute(HtmlTextWriterAttribute.Class, "tooltip");
312 tableWriter.RenderBeginTag(HtmlTextWriterTag.Div);
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);
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")}, ");
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]);
336 tableWriter.WriteEncodedText(tooltipText.ToString() );
338 tableWriter.RenderEndTag( );//</span>
341 tableWriter.RenderEndTag( );//</div> --tooltip enclosure
344 tableWriter.Write("?");
347 tableWriter.RenderEndTag( );
348 }//############ </td> ###########
350 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
351 tableWriter.Write(yAxis);//legend: Y-axis
352 tableWriter.RenderEndTag( );
354 tableWriter.RenderEndTag( );
357 tableWriter.RenderEndTag( );
359 //################ X-Axis <tfoot> #######################
360 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
361 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
363 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
364 tableWriter.Write("S, W");
365 tableWriter.RenderEndTag( );
367 for (int xAxisB = TopWest; xAxisB <= TopEast; xAxisB++) {
368 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
369 tableWriter.Write(xAxisB);
370 tableWriter.RenderEndTag( );
373 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
374 tableWriter.Write("S, E");
375 tableWriter.RenderEndTag( );
377 tableWriter.RenderEndTag( );
378 tableWriter.RenderEndTag( );
379 //###### </tfoot> ################################
382 tableWriter.RenderEndTag( );//</table>
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( );
394 tableWriter.RenderEndTag( );
399 tableWriter.RenderEndTag( );//### </BODY> ###
401 tableWriter.EndRender( );
402 tableWriter.Flush( );
407 Logger.VerboseDebug("Generated HTML map");
412 private ColumnMeta UpdateColumnMetadata(KeyValuePair<Vec2i, uint> mostActiveCol, IMapChunk mapChunk)
414 ColumnMeta data = new ColumnMeta(mostActiveCol.Key.Copy());
415 BlockPos equivBP = new BlockPos(mostActiveCol.Key.X * ClientAPI.World.BlockAccessor.ChunkSize,
417 mostActiveCol.Key.Y * ClientAPI.World.BlockAccessor.ChunkSize);
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;
427 data.YMax = mapChunk.YMax;
430 /* Only present on server....
431 if (mapChunk.TopRockIdMap != null) {
432 foreach (var topRockId in mapChunk.TopRockIdMap) {
434 if (data.RockRatio.ContainsKey(topRockId)) { data.RockRatio[topRockId]++; }
435 else { data.RockRatio.Add(topRockId, 1); }
444 /// Reload chunk bounds from chunk shards
446 /// <returns>The metadata.</returns>
447 private void Reload_Metadata( )
449 var worldmapDir = new DirectoryInfo(path);
451 if (worldmapDir.Exists) {
453 var files = worldmapDir.GetFiles(chunkFile_filter);
455 if (files.Length > 0) {
457 Logger.VerboseDebug("{0} Existing world chunk shards", files.Length);
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 );
468 //Parse PNG chunks for METADATA in shard
469 using (var fileStream = shardFile.OpenRead( ))
471 PngReader pngRead = new PngReader(fileStream );
472 pngRead.ReadSkippingAllRows( );
475 PngMetadataChunk metadataFromPng = pngRead.GetChunksList( ).GetById1(PngMetadataChunk.ID) as PngMetadataChunk;
477 chunkTopMetadata.Add(metadataFromPng.ChunkMetadata);
487 Logger.VerboseDebug("Could not open world map directory");
495 private PngWriter SetupPngImage(Vec2i coord, ColumnMeta metadata)
497 ImageInfo imageInf = new ImageInfo(ClientAPI.World.BlockAccessor.ChunkSize, ClientAPI.World.BlockAccessor.ChunkSize, 8, false);
499 string filename = $"{coord.X}_{coord.Y}.png";
500 filename = Path.Combine(path, filename);
502 PngWriter pngWriter = FileHelper.CreatePngWriter(filename, imageInf, true);
503 PngMetadata meta = pngWriter.GetMetadata( );
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);
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)
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];
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?
537 // Prefetch map chunks, in pattern
538 IMapChunk[ ] mapChunks = new IMapChunk[ ]
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)
545 //pre-create PNG line slices...
546 ImageLine[ ] lines = Enumerable.Repeat(new object( ), chunkSize).Select(l => new ImageLine(pngWriter.ImgInfo)).ToArray( );
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!
553 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
554 int localX = localpos.X;
555 int localZ = localpos.Y;
558 int leftTop, rightTop, leftBot;
560 IMapChunk leftTopMapChunk = mc;
561 IMapChunk rightTopMapChunk = mc;
562 IMapChunk leftBotMapChunk = mc;
564 int topX = localX - 1;
566 int leftZ = localZ - 1;
569 if (topX < 0 && leftZ < 0) {
570 leftTopMapChunk = mapChunks[0];
571 rightTopMapChunk = mapChunks[1];
572 leftBotMapChunk = mapChunks[2];
576 leftTopMapChunk = mapChunks[1];
577 rightTopMapChunk = mapChunks[1];
580 leftTopMapChunk = mapChunks[2];
581 leftBotMapChunk = mapChunks[2];
585 topX = GameMath.Mod(topX, chunkSize);
586 leftZ = GameMath.Mod(leftZ, chunkSize);
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]);
592 float slopeness = (leftTop + rightTop + leftBot);
594 if (slopeness > 0) b = 1.2f;
595 if (slopeness < 0) b = 0.8f;
597 b -= 0.15f; //Slope boost value
599 if (chunksColumn[localChunkY] == null) {
604 chunksColumn[localChunkY].Unpack( );
605 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
607 Block block = ClientAPI.World.Blocks[blockId];
609 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
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);
616 int red = ColorUtil.ColorB(packedFormat);
617 int green = ColorUtil.ColorG(packedFormat);
618 int blue = ColorUtil.ColorR(packedFormat);
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;
628 if (desig.SpecialAction != null) {
629 desig.SpecialAction(ClientAPI, this.POIs, tmpPos, block);
633 ImageLineHelper.SetPixel(lines[localZ], localX, red, green, blue);
635 //chunkImage.SetPixel(localX, localZ, pixelColor);
639 for (int row = 0; row < pngWriter.ImgInfo.Rows; row++) {
640 pngWriter.WriteRow(lines[row], row);