OSDN Git Service

FIX: Temporary chunks list to avoid Enumerator mutation
[automap/automap.git] / Automap / Automap_Internals.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Collections.ObjectModel;
5 using System.Drawing;
6 using System.Drawing.Imaging;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10
11 using System.Web.UI;
12
13 using Vintagestory.API.Common;
14 using Vintagestory.API.MathTools;
15
16
17
18 namespace Automap
19 {
20         public partial class AutomapMod
21         {
22                 private Thread cartographer_thread;
23
24                 private const string _mapPath = @"Maps";
25                 private const string _chunkPath = @"Chunks";
26                 private const string _domain = @"automap";
27
28                 private ConcurrentDictionary<Vec2i, uint> columnCounter = new ConcurrentDictionary<Vec2i, uint>(3, 103 );//ChunkMeta struct?
29                 private HashSet<Vec2i> knownChunkTops = new HashSet<Vec2i>( );
30
31                 private PointsOfInterest POIs;
32                 private Dictionary<int, Designator> BlockID_Designators;
33
34                 private int North_mostChunk;
35                 private int East_mostChunk;
36                 private int West_mostChunk;
37                 private int South_mostChunk;
38                 private Vec2i startChunkColumn;
39                 private uint lastUpdate;
40
41                 private string path;
42                 private IAsset stylesFile;
43
44                 #region Internals
45                 private void StartAutomap( )
46                 {
47                 path = ClientAPI.GetOrCreateDataPath(_mapPath);
48                 path = ClientAPI.GetOrCreateDataPath(Path.Combine(path, "World_" + ClientAPI.World.Seed));//Add name of World too!
49
50                 stylesFile = ClientAPI.World.AssetManager.Get(new AssetLocation(_domain, "config/automap_format.css"));
51                 Logger.VerboseDebug("CSS loaded: {0} size: {1}",stylesFile.IsLoaded() ,stylesFile.ToText( ).Length);
52
53
54                 Prefill_POI_Designators( );
55                 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));
56                 North_mostChunk = startChunkColumn.Y;
57                 South_mostChunk = startChunkColumn.Y;
58                 East_mostChunk = startChunkColumn.X;
59                 West_mostChunk = startChunkColumn.X;
60
61                 Logger.Notification("AUTOMAP Start {0}", startChunkColumn);
62
63                 ClientAPI.Event.ChunkDirty += ChunkAChanging;
64
65                 cartographer_thread = new Thread(Cartographer);
66                 cartographer_thread.Name = "Cartographer";
67                 cartographer_thread.Priority = ThreadPriority.Lowest;
68                 cartographer_thread.IsBackground = true;
69
70                 ClientAPI.Event.RegisterGameTickListener(AwakenCartographer, 6000);
71                 }
72
73                 private void ChunkAChanging(Vec3i chunkCoord, IWorldChunk chunk, EnumChunkDirtyReason reason)
74                 {                       
75                 Vec2i topPosition = new Vec2i(chunkCoord.X, chunkCoord.Z);
76
77                 columnCounter.AddOrUpdate(topPosition, 1, (key, colAct) => colAct + 1);         
78                 }
79
80                 private void AwakenCartographer(float delayed)
81                 {
82
83                 if (ClientAPI.IsGamePaused != false || ClientAPI.IsShuttingDown != true) {
84                 #if DEBUG
85                 Logger.VerboseDebug("Cartographer re-trigger from [{0}]", cartographer_thread.ThreadState);
86                 #endif
87
88                 if (cartographer_thread.ThreadState.HasFlag(ThreadState.Unstarted)) {
89                 cartographer_thread.Start( );
90                 }
91                 else if (cartographer_thread.ThreadState.HasFlag(ThreadState.WaitSleepJoin)) {
92                 //Time to (re)write chunk shards
93                 cartographer_thread.Interrupt( );
94                 }
95                 ClientAPI.TriggerChatMessage($"Automap {lastUpdate} changes - MAX (N:{North_mostChunk},S:{South_mostChunk},E:{East_mostChunk}, W:{West_mostChunk} - TOTAL: {knownChunkTops.Count})");
96                 }
97
98                 }
99
100
101                 private void Cartographer( )
102                 {
103         wake:
104                 Logger.VerboseDebug("Cartographer thread awoken");
105
106                 try {
107                 uint ejectedItem = 0;
108                 uint updatedChunks = 0;
109
110                 //-- Should dodge enumerator changing underfoot....at a cost.
111                 if (!columnCounter.IsEmpty) {
112                 var tempSet = columnCounter.ToArray( ).OrderByDescending(kvp => kvp.Value);
113                 foreach (var mostActiveCol in tempSet) {
114
115                 var mapChunk = ClientAPI.World.BlockAccessor.GetMapChunk(mostActiveCol.Key);
116
117                 if (mapChunk == null) {
118                 Logger.Warning("SKIP CHUNK: ({0}) - Map Chunk NULL!", mostActiveCol.Key);
119
120                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
121                 continue;
122                 }
123
124                 string filename = $"{mostActiveCol.Key.X}_{mostActiveCol.Key.Y}.png";
125                 filename = Path.Combine(path, filename);
126
127                 uint pixels = 0;
128                 var chkImg = GenerateChunkImage(mostActiveCol.Key, mapChunk, out pixels);
129
130                 if (pixels > 0) {
131                 chkImg.Save(filename, ImageFormat.Png);
132                 #if DEBUG
133                 Logger.VerboseDebug("Wrote chunk shard: ({0}) - Edits#:{1}, Pixels#:{2}", mostActiveCol.Key, mostActiveCol.Value, pixels);
134                 #endif
135                 updatedChunks++;
136                 knownChunkTops.Add(mostActiveCol.Key);
137                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
138                 }
139                 else {
140                 columnCounter.TryRemove(mostActiveCol.Key, out ejectedItem);
141                 Logger.VerboseDebug("Un-painted chunk: ({0}) ", mostActiveCol.Key);
142                 }
143
144                 }
145                 }
146
147                 if (updatedChunks > 0) {
148                 lastUpdate = updatedChunks;
149                 GenerateMapHTML( );
150                 updatedChunks = 0;
151                 }
152
153                 //Then sleep until interupted again, and repeat
154
155                 Logger.VerboseDebug("Thread '{0}' about to sleep indefinitely.", Thread.CurrentThread.Name);
156
157                 Thread.Sleep(Timeout.Infinite);
158
159                 } catch (ThreadInterruptedException) {
160
161                 Logger.VerboseDebug("Thread '{0}' interupted [awoken]", Thread.CurrentThread.Name);
162                 goto wake;
163
164                 } catch (ThreadAbortException) {
165                 Logger.VerboseDebug("Thread '{0}' aborted.", Thread.CurrentThread.Name);
166
167                 } finally {
168                 Logger.VerboseDebug("Thread '{0}' executing finally block.", Thread.CurrentThread.Name);
169                 }
170                 }
171                 #endregion
172
173
174                 private void Prefill_POI_Designators( )
175                 {
176                 this.POIs = new PointsOfInterest( );
177                 this.BlockID_Designators = new Dictionary<int, Designator>( );
178
179                 //Add special marker types for BlockID's of "Interest", overwrite colour, and method
180
181                 var theDesignators = new List<Designator>{
182                                 DefaultDesignators.Roads,
183                 DefaultDesignators.GroundSigns,
184                 DefaultDesignators.WallSigns,
185                 DefaultDesignators.PostSigns,
186                                 };
187
188                 Install_POI_Designators(theDesignators);
189                 }
190
191                 private void Install_POI_Designators(ICollection<Designator> designators)
192                 {
193                 Logger.VerboseDebug("Connecting {0} configured Designators", designators.Count);
194                 foreach (var designator in designators) {                               
195                         var blockIDs = Helpers.ArbitrarytBlockIdHunter(ClientAPI, designator.Pattern, designator.Material);
196                                 if (blockIDs.Count > 0) { Logger.VerboseDebug("Designator {0} has {1} associated blockIDs", designator.ToString( ), blockIDs.Count); }
197                         foreach (var entry in blockIDs) {
198                         BlockID_Designators.Add(entry.Key, designator);
199                         }
200                 }
201
202                 }
203
204
205
206                 #region COPYPASTA
207                 //TODO: rewrite - with alternate algo.
208                 //A slightly re-written; ChunkMapLayer :: public int[] GenerateChunkImage(Vec2i chunkPos, IMapChunk mc)
209                 internal Bitmap GenerateChunkImage(Vec2i chunkPos, IMapChunk mc, out uint pixelCount)
210                 {
211                 pixelCount = 0;
212                 BlockPos tmpPos = new BlockPos( );
213                 Vec2i localpos = new Vec2i( );
214                 int chunkSize = ClientAPI.World.BlockAccessor.ChunkSize;
215                 var chunksColumn = new IWorldChunk[ClientAPI.World.BlockAccessor.MapSizeY / chunkSize];
216                 Bitmap chunkImage = new Bitmap(chunkSize, chunkSize, PixelFormat.Format24bppRgb);
217                 int topChunkY = mc.YMax / chunkSize;//Heywaitaminute -- this isn't a highest FEATURE, if Rainmap isn't accurate!
218                                                                                         //Metadata of DateTime chunk was edited, chunk coords.,world-seed? Y-Max feature height
219                                                                                         //Grab a chunk COLUMN... Topmost Y down...
220                 for (int chunkY = 0; chunkY <= topChunkY; chunkY++) {
221                 chunksColumn[chunkY] = ClientAPI.World.BlockAccessor.GetChunk(chunkPos.X, chunkY, chunkPos.Y);
222                 //What to do if chunk is a void? invalid?
223                 }
224
225                 // Prefetch map chunks, in pattern
226                 IMapChunk[ ] mapChunks = new IMapChunk[ ]
227                 {
228                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y - 1),
229                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X - 1, chunkPos.Y),
230                         ClientAPI.World.BlockAccessor.GetMapChunk(chunkPos.X, chunkPos.Y - 1)
231                 };
232
233
234                 for (int posIndex = 0; posIndex < (chunkSize * chunkSize); posIndex++) {
235                 int mapY = mc.RainHeightMap[posIndex];
236                 int localChunkY = mapY / chunkSize;
237                 if (localChunkY >= (chunksColumn.Length)) continue;//Out of range!
238
239                 MapUtil.PosInt2d(posIndex, chunkSize, localpos);
240                 int localX = localpos.X;
241                 int localZ = localpos.Y;
242
243                 float b = 1;
244                 int leftTop, rightTop, leftBot;
245
246                 IMapChunk leftTopMapChunk = mc;
247                 IMapChunk rightTopMapChunk = mc;
248                 IMapChunk leftBotMapChunk = mc;
249
250                 int topX = localX - 1;
251                 int botX = localX;
252                 int leftZ = localZ - 1;
253                 int rightZ = localZ;
254
255                 if (topX < 0 && leftZ < 0) {
256                 leftTopMapChunk = mapChunks[0];
257                 rightTopMapChunk = mapChunks[1];
258                 leftBotMapChunk = mapChunks[2];
259                 }
260                 else {
261                 if (topX < 0) {
262                 leftTopMapChunk = mapChunks[1];
263                 rightTopMapChunk = mapChunks[1];
264                 }
265                 if (leftZ < 0) {
266                 leftTopMapChunk = mapChunks[2];
267                 leftBotMapChunk = mapChunks[2];
268                 }
269                 }
270
271                 topX = GameMath.Mod(topX, chunkSize);
272                 leftZ = GameMath.Mod(leftZ, chunkSize);
273
274                 leftTop = leftTopMapChunk == null ? 0 : Math.Sign(mapY - leftTopMapChunk.RainHeightMap[leftZ * chunkSize + topX]);
275                 rightTop = rightTopMapChunk == null ? 0 : Math.Sign(mapY - rightTopMapChunk.RainHeightMap[rightZ * chunkSize + topX]);
276                 leftBot = leftBotMapChunk == null ? 0 : Math.Sign(mapY - leftBotMapChunk.RainHeightMap[leftZ * chunkSize + botX]);
277
278                 float slopeness = (leftTop + rightTop + leftBot);
279
280                 if (slopeness > 0) b = 1.2f;
281                 if (slopeness < 0) b = 0.8f;
282
283                 b -= 0.15f; //Slope boost value 
284
285                 if (chunksColumn[localChunkY] == null) {
286
287                 continue;
288                 }
289
290                 chunksColumn[localChunkY].Unpack( );
291                 int blockId = chunksColumn[localChunkY].Blocks[MapUtil.Index3d(localpos.X, mapY % chunkSize, localpos.Y, chunkSize, chunkSize)];
292
293                 Block block = ClientAPI.World.Blocks[blockId];
294
295                 tmpPos.Set(chunkSize * chunkPos.X + localpos.X, mapY, chunkSize * chunkPos.Y + localpos.Y);
296
297                 int avgCol = block.GetColor(ClientAPI, tmpPos);
298                 int rndCol = block.GetRandomColor(ClientAPI, tmpPos, BlockFacing.UP);
299                 //This is still, an abnormal color - the tint is too blue
300                 int col = ColorUtil.ColorOverlay(avgCol, rndCol, 0.125f);
301                 var packedFormat = ColorUtil.ColorMultiply3Clamped(col, b);
302
303                 Color pixelColor = Color.FromArgb(ColorUtil.ColorB(packedFormat), ColorUtil.ColorG(packedFormat), ColorUtil.ColorR(packedFormat));
304
305                 //============ POI Population =================
306                 if (BlockID_Designators.ContainsKey(blockId)) {
307                 var desig = BlockID_Designators[blockId];
308                 pixelColor = desig.OverwriteColor;
309
310                 if (desig.SpecialAction != null) {              
311                 desig.SpecialAction(ClientAPI, this.POIs, tmpPos, block);
312                 }
313                 }
314
315                 chunkImage.SetPixel(localX, localZ, pixelColor);
316                 pixelCount++;
317                 }
318
319
320                 return chunkImage;
321                 }
322                 #endregion
323
324
325                 private void GenerateMapHTML( )
326                 {
327                 string mapFilename = Path.Combine(path, "Automap.html");
328
329                 North_mostChunk = knownChunkTops.Min(tc => tc.Y);
330                 South_mostChunk = knownChunkTops.Max(tc => tc.Y);
331                 East_mostChunk = knownChunkTops.Max(tc => tc.X);
332                 West_mostChunk = knownChunkTops.Min(tc => tc.X);
333
334
335                 using (StreamWriter outputText = new StreamWriter(File.Open(mapFilename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))) {
336                 using (HtmlTextWriter tableWriter = new HtmlTextWriter(outputText)) {
337                 tableWriter.BeginRender( );
338                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Html);
339
340                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Head);
341                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Title);
342                 tableWriter.WriteEncodedText("Generated Automap");
343                 tableWriter.RenderEndTag( );
344                 //CSS  style  here
345                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Style);
346                 tableWriter.Write(stylesFile.ToText( ));
347                 tableWriter.RenderEndTag( );//</style>
348
349                 tableWriter.RenderEndTag( );
350
351                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Body);
352                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
353                 tableWriter.WriteEncodedText($"Created {DateTimeOffset.UtcNow.ToString("u")}");
354                 tableWriter.RenderEndTag( );
355                 tableWriter.RenderBeginTag(HtmlTextWriterTag.P);
356                 tableWriter.WriteEncodedText($"W:{West_mostChunk}, E: {East_mostChunk}, N:{North_mostChunk}, S:{South_mostChunk} ");
357                 tableWriter.RenderEndTag( );
358                 tableWriter.WriteLine( );
359                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Table);
360                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Caption);
361                 tableWriter.WriteEncodedText($"Start: {startChunkColumn}, Seed: {ClientAPI.World.Seed}\n");             
362                 tableWriter.RenderEndTag( );
363
364                 //################ X-Axis <thead> #######################
365                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Thead);
366                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
367
368                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
369                 tableWriter.Write("N, W");
370                 tableWriter.RenderEndTag( );
371
372                 for (int xAxisT = West_mostChunk; xAxisT <= East_mostChunk; xAxisT++) {
373                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
374                 tableWriter.Write(xAxisT);
375                 tableWriter.RenderEndTag( );
376                 }
377
378                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Th);
379                 tableWriter.Write("N, E");
380                 tableWriter.RenderEndTag( );
381                 
382                 tableWriter.RenderEndTag( );
383                 tableWriter.RenderEndTag( );
384                 //###### </thead> ################################
385
386                 //###### <tbody> - Chunk rows & Y-axis cols
387                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tbody);
388                 //######## <tr> for every vertical row
389                 for (int yAxis = North_mostChunk; yAxis <= South_mostChunk; yAxis++) {
390                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
391                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
392                 tableWriter.Write(yAxis);//legend: Y-axis
393                 tableWriter.RenderEndTag( );
394
395                 for (int xAxis = West_mostChunk; xAxis <= East_mostChunk; xAxis++) {
396                 //###### <td>  #### for chunk shard 
397                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
398                 if (knownChunkTops.Contains( new Vec2i(xAxis, yAxis))){
399                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Src, $"{xAxis}_{yAxis}.png");
400                 tableWriter.AddAttribute(HtmlTextWriterAttribute.Alt, $"X{xAxis}, Y{yAxis}");
401                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Img);
402                 tableWriter.RenderEndTag( );
403                 }
404                 else {
405                 tableWriter.Write("?");
406                 }               
407                 tableWriter.RenderEndTag( );
408                 }//############ </td> ###########
409
410                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
411                 tableWriter.Write(yAxis);//legend: Y-axis
412                 tableWriter.RenderEndTag( );
413
414                 tableWriter.RenderEndTag( );
415                 tableWriter.EndRender( );
416                 tableWriter.Flush( );
417                 }
418                 tableWriter.RenderEndTag( );
419
420                 //################ X-Axis <tfoot> #######################
421                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tfoot);
422                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Tr);
423
424                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
425                 tableWriter.Write("S, W");
426                 tableWriter.RenderEndTag( );
427
428                 for (int xAxisB = West_mostChunk; xAxisB <= East_mostChunk; xAxisB++) {
429                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
430                 tableWriter.Write(xAxisB);
431                 tableWriter.RenderEndTag( );
432                 }
433
434                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Td);
435                 tableWriter.Write("S, E");
436                 tableWriter.RenderEndTag( );
437
438                 tableWriter.RenderEndTag( );
439                 tableWriter.RenderEndTag( );
440                 //###### </tfoot> ################################
441
442
443                 tableWriter.RenderEndTag( );//</table>
444                 
445                 //############## POI list #####################
446                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Ul);
447                 foreach (var poi in this.POIs) {
448                 tableWriter.RenderBeginTag(HtmlTextWriterTag.Li);
449                 tableWriter.WriteEncodedText(poi.Timestamp.ToString("u"));
450                 tableWriter.WriteEncodedText(poi.Notes);
451                 tableWriter.WriteEncodedText(poi.Location.PrettyCoords(this.ClientAPI));
452                 tableWriter.RenderEndTag( );
453                 }
454
455                 tableWriter.RenderEndTag( );
456                 tableWriter.RenderEndTag( );
457                 tableWriter.RenderEndTag( );
458
459                 tableWriter.EndRender( );
460                 tableWriter.Flush( );
461                 }
462                 outputText.Flush( );            
463                 }
464
465                 Logger.VerboseDebug("Generated HTML map");
466                 }
467
468
469
470         }
471
472 }