summaryrefslogtreecommitdiffstats
path: root/Tools/AnvilStats
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/AnvilStats')
-rw-r--r--Tools/AnvilStats/.gitignore4
-rw-r--r--Tools/AnvilStats/AnvilStats.cpp3
-rw-r--r--Tools/AnvilStats/AnvilStats.txt1
-rw-r--r--Tools/AnvilStats/AnvilStats.vcproj16
-rw-r--r--Tools/AnvilStats/Callback.h46
-rw-r--r--Tools/AnvilStats/HeightBiomeMap.cpp230
-rw-r--r--Tools/AnvilStats/HeightBiomeMap.h81
-rw-r--r--Tools/AnvilStats/HeightMap.h4
-rw-r--r--Tools/AnvilStats/ImageComposingCallback.cpp219
-rw-r--r--Tools/AnvilStats/ImageComposingCallback.h105
-rw-r--r--Tools/AnvilStats/Processor.cpp8
11 files changed, 698 insertions, 19 deletions
diff --git a/Tools/AnvilStats/.gitignore b/Tools/AnvilStats/.gitignore
index 4ed720fed..5d98f06ec 100644
--- a/Tools/AnvilStats/.gitignore
+++ b/Tools/AnvilStats/.gitignore
@@ -1,5 +1,9 @@
.xls
Statistics.txt
*.bmp
+Debug/
+Release/
Profiling
*.png
+world/
+*.html \ No newline at end of file
diff --git a/Tools/AnvilStats/AnvilStats.cpp b/Tools/AnvilStats/AnvilStats.cpp
index f0b9dd7e6..d98c21985 100644
--- a/Tools/AnvilStats/AnvilStats.cpp
+++ b/Tools/AnvilStats/AnvilStats.cpp
@@ -8,6 +8,7 @@
#include "Statistics.h"
#include "BiomeMap.h"
#include "HeightMap.h"
+#include "HeightBiomeMap.h"
#include "ChunkExtract.h"
#include "SpringStats.h"
@@ -26,6 +27,7 @@ int main(int argc, char * argv[])
LOG(" 2 - height map");
LOG(" 3 - extract chunks");
LOG(" 4 - count lava- and water- springs");
+ LOG(" 5 - biome and height map");
LOG("\nNo method number present, aborting.");
return -1;
}
@@ -48,6 +50,7 @@ int main(int argc, char * argv[])
case 2: Factory = new cHeightMapFactory; break;
case 3: Factory = new cChunkExtractFactory(WorldFolder); break;
case 4: Factory = new cSpringStatsFactory; break;
+ case 5: Factory = new cHeightBiomeMapFactory; break;
default:
{
LOG("Unknown method \"%s\", aborting.", argv[1]);
diff --git a/Tools/AnvilStats/AnvilStats.txt b/Tools/AnvilStats/AnvilStats.txt
index 19aa4f324..1d8130aa2 100644
--- a/Tools/AnvilStats/AnvilStats.txt
+++ b/Tools/AnvilStats/AnvilStats.txt
@@ -15,6 +15,7 @@ Possible usage:
- count the per-chunk density of specific blocks
- count the per-chunk density of dungeons, by measuring the number of zombie/skeleton/regularspider spawners
- count the per-chunk-per-biome density of trees, by measuring the number of dirt-log vertical transitions, correlating to biome data
+ - draw a vertical map of the world based on a specific measured value (biome, elevation, ...)
This project is Windows-only, although it shouldn't be too difficult to make it portable.
diff --git a/Tools/AnvilStats/AnvilStats.vcproj b/Tools/AnvilStats/AnvilStats.vcproj
index ed4ffa9a5..038f32b97 100644
--- a/Tools/AnvilStats/AnvilStats.vcproj
+++ b/Tools/AnvilStats/AnvilStats.vcproj
@@ -314,6 +314,14 @@
>
</File>
<File
+ RelativePath=".\HeightBiomeMap.cpp"
+ >
+ </File>
+ <File
+ RelativePath=".\HeightBiomeMap.h"
+ >
+ </File>
+ <File
RelativePath=".\HeightMap.cpp"
>
</File>
@@ -322,6 +330,14 @@
>
</File>
<File
+ RelativePath=".\ImageComposingCallback.cpp"
+ >
+ </File>
+ <File
+ RelativePath=".\ImageComposingCallback.h"
+ >
+ </File>
+ <File
RelativePath=".\Processor.cpp"
>
</File>
diff --git a/Tools/AnvilStats/Callback.h b/Tools/AnvilStats/Callback.h
index 83b330651..eda4a8478 100644
--- a/Tools/AnvilStats/Callback.h
+++ b/Tools/AnvilStats/Callback.h
@@ -22,44 +22,53 @@ class cParsedNBT;
/** The base class for all chunk-processor callbacks, declares the interface.
The processor calls each virtual function in the order they are declared here with the specified args.
-If the function returns true, the processor moves on to next chunk and starts calling the callbacks again from start with
-the new chunk data.
+If the function returns true, the processor doesn't process the data item, moves on to the next chunk
+and starts calling the callbacks again from start with the new chunk data.
So if a statistics collector doesn't need data decompression at all, it can stop the processor from doing so early-enough
and still get meaningful data.
-A callback is guaranteed to run in a single thread and always the same thread.
+A callback is guaranteed to run in a single thread and always the same thread for the same chunk.
A callback is guaranteed to run on all chunks in a region and one region is guaranteed to be handled by only callback.
*/
class cCallback abstract
{
public:
+ enum
+ {
+ CALLBACK_CONTINUE = false,
+ CALLBACK_ABORT = true,
+ } ;
+
virtual ~cCallback() {} // Force a virtual destructor in each descendant
+
+ /// Called when a new region file is about to be opened; by default allow the region
+ virtual bool OnNewRegion(int a_RegionX, int a_RegionZ) { return CALLBACK_CONTINUE; }
/// Called to inform the stats module of the chunk coords for newly processing chunk
virtual bool OnNewChunk(int a_ChunkX, int a_ChunkZ) = 0;
/// Called to inform about the chunk's data offset in the file (chunk mini-header), the number of sectors it uses and the timestamp field value
- virtual bool OnHeader(int a_FileOffset, unsigned char a_NumSectors, int a_Timestamp) { return true; }
+ virtual bool OnHeader(int a_FileOffset, unsigned char a_NumSectors, int a_Timestamp) { return CALLBACK_ABORT; }
/// Called to inform of the compressed chunk data size and position in the file (offset from file start to the actual data)
- virtual bool OnCompressedDataSizePos(int a_CompressedDataSize, int a_DataOffset, char a_CompressionMethod) { return true; }
+ virtual bool OnCompressedDataSizePos(int a_CompressedDataSize, int a_DataOffset, char a_CompressionMethod) { return CALLBACK_ABORT; }
/// Just in case you wanted to process the NBT yourself ;)
- virtual bool OnDecompressedData(const char * a_DecompressedNBT, int a_DataSize) { return true; }
+ virtual bool OnDecompressedData(const char * a_DecompressedNBT, int a_DataSize) { return CALLBACK_ABORT; }
/// The chunk's NBT should specify chunk coords, these are sent here:
- virtual bool OnRealCoords(int a_ChunkX, int a_ChunkZ) { return true; }
+ virtual bool OnRealCoords(int a_ChunkX, int a_ChunkZ) { return CALLBACK_ABORT; }
/// The chunk contains a LastUpdate value specifying the last tick in which it was saved.
- virtual bool OnLastUpdate(Int64 a_LastUpdate) { return true; }
+ virtual bool OnLastUpdate(Int64 a_LastUpdate) { return CALLBACK_ABORT; }
- virtual bool OnTerrainPopulated(bool a_Populated) { return true; }
+ virtual bool OnTerrainPopulated(bool a_Populated) { return CALLBACK_ABORT; }
- virtual bool OnBiomes(const unsigned char * a_BiomeData) { return true; }
+ virtual bool OnBiomes(const unsigned char * a_BiomeData) { return CALLBACK_ABORT; }
/** Called when a heightmap for the chunk is read from the file.
Note that the heightmap is given in big-endian ints, so if you want it, you need to ntohl() it first!
*/
- virtual bool OnHeightMap(const int * a_HeightMapBE) { return true; }
+ virtual bool OnHeightMap(const int * a_HeightMapBE) { return CALLBACK_ABORT; }
/** If there is data for the section, this callback is called; otherwise OnEmptySection() is called instead.
All OnSection() callbacks are called first, and only then all the remaining sections are reported in OnEmptySection().
@@ -71,16 +80,16 @@ public:
const NIBBLETYPE * a_BlockMeta,
const NIBBLETYPE * a_BlockLight,
const NIBBLETYPE * a_BlockSkyLight
- ) { return true; }
+ ) { return CALLBACK_ABORT; }
/** If there is no data for a section, this callback is called; otherwise OnSection() is called instead.
OnEmptySection() callbacks are called after all OnSection() callbacks.
*/
- virtual bool OnEmptySection(unsigned char a_Y) { return false; }
+ virtual bool OnEmptySection(unsigned char a_Y) { return CALLBACK_CONTINUE; }
/** Called after all sections have been processed via either OnSection() or OnEmptySection().
*/
- virtual bool OnSectionsFinished(void) { return true; }
+ virtual bool OnSectionsFinished(void) { return CALLBACK_ABORT; }
/** Called for each entity in the chunk.
Common parameters are parsed from the NBT.
@@ -98,7 +107,7 @@ public:
char a_IsOnGround,
cParsedNBT & a_NBT,
int a_NBTTag
- ) { return true; }
+ ) { return CALLBACK_ABORT; }
/** Called for each tile entity in the chunk.
Common parameters are parsed from the NBT.
@@ -110,14 +119,17 @@ public:
int a_PosX, int a_PosY, int a_PosZ,
cParsedNBT & a_NBT,
int a_NBTTag
- ) { return true; }
+ ) { return CALLBACK_ABORT; }
/// Called for each tile tick in the chunk
virtual bool OnTileTick(
int a_BlockType,
int a_TicksLeft,
int a_PosX, int a_PosY, int a_PosZ
- ) { return true; }
+ ) { return CALLBACK_ABORT; }
+
+ /// Called after the entire region file has been processed. No more callbacks for this region will be called. No processing by default
+ virtual void OnRegionFinished(int a_RegionX, int a_RegionZ) {}
} ;
typedef std::vector<cCallback *> cCallbacks;
diff --git a/Tools/AnvilStats/HeightBiomeMap.cpp b/Tools/AnvilStats/HeightBiomeMap.cpp
new file mode 100644
index 000000000..36918f644
--- /dev/null
+++ b/Tools/AnvilStats/HeightBiomeMap.cpp
@@ -0,0 +1,230 @@
+
+// HeightBiomeMap.cpp
+
+// Declares the cHeightBiomeMap class representing a stats module that produces an image of heights and biomes combined
+
+#include "Globals.h"
+#include "HeightBiomeMap.h"
+#include "HeightMap.h"
+
+
+
+
+
+cHeightBiomeMap::cHeightBiomeMap(void) :
+ super("HeBi"),
+ m_MinRegionX(100000),
+ m_MaxRegionX(-100000),
+ m_MinRegionZ(100000),
+ m_MaxRegionZ(-100000)
+{
+}
+
+
+
+
+
+bool cHeightBiomeMap::OnNewRegion(int a_RegionX, int a_RegionZ)
+{
+ if (a_RegionX < m_MinRegionX)
+ {
+ m_MinRegionX = a_RegionX;
+ }
+ if (a_RegionX > m_MaxRegionX)
+ {
+ m_MaxRegionX = a_RegionX;
+ }
+ if (a_RegionZ < m_MinRegionZ)
+ {
+ m_MinRegionZ = a_RegionZ;
+ }
+ if (a_RegionZ > m_MaxRegionZ)
+ {
+ m_MaxRegionZ = a_RegionZ;
+ }
+ return super::OnNewRegion(a_RegionX, a_RegionZ);
+}
+
+
+
+
+
+bool cHeightBiomeMap::OnNewChunk(int a_ChunkX, int a_ChunkZ)
+{
+ m_CurrentChunkX = a_ChunkX;
+ m_CurrentChunkZ = a_ChunkZ;
+ m_CurrentChunkRelX = m_CurrentChunkX - m_CurrentRegionX * 32;
+ m_CurrentChunkRelZ = m_CurrentChunkZ - m_CurrentRegionZ * 32;
+
+ ASSERT((m_CurrentChunkRelX >= 0) && (m_CurrentChunkRelX < 32));
+ ASSERT((m_CurrentChunkRelZ >= 0) && (m_CurrentChunkRelZ < 32));
+
+ memset(m_BlockTypes, 0, sizeof(m_BlockTypes));
+
+ return CALLBACK_CONTINUE;
+}
+
+
+
+
+
+
+bool cHeightBiomeMap::OnBiomes(const unsigned char * a_BiomeData)
+{
+ memcpy(m_ChunkBiomes, a_BiomeData, sizeof(m_ChunkBiomes));
+
+ return CALLBACK_CONTINUE;
+}
+
+
+
+
+
+bool cHeightBiomeMap::OnHeightMap(const int * a_HeightMapBE)
+{
+ for (int i = 0; i < ARRAYCOUNT(m_ChunkHeight); i++)
+ {
+ m_ChunkHeight[i] = ntohl(a_HeightMapBE[i]);
+ } // for i - m_ChunkHeight
+
+ return CALLBACK_CONTINUE;
+}
+
+
+
+
+
+bool cHeightBiomeMap::OnSection(
+ unsigned char a_Y,
+ const BLOCKTYPE * a_BlockTypes,
+ const NIBBLETYPE * a_BlockAdditional,
+ const NIBBLETYPE * a_BlockMeta,
+ const NIBBLETYPE * a_BlockLight,
+ const NIBBLETYPE * a_BlockSkyLight
+)
+{
+ // Copy the section data into the appropriate place in the internal buffer
+ memcpy(m_BlockTypes + a_Y * 16 * 16 * 16, a_BlockTypes, 16 * 16 * 16);
+ return CALLBACK_CONTINUE;
+}
+
+
+
+
+
+bool cHeightBiomeMap::OnSectionsFinished(void)
+{
+ static const int BiomePalette[] =
+ {
+ // ARGB:
+ 0xff0000ff, /* Ocean */
+ 0xff00cf3f, /* Plains */
+ 0xffffff00, /* Desert */
+ 0xff7f7f7f, /* Extreme Hills */
+ 0xff00cf00, /* Forest */
+ 0xff007f3f, /* Taiga */
+ 0xff3f7f00, /* Swampland */
+ 0xff003fff, /* River */
+ 0xff7f0000, /* Hell */
+ 0xff007fff, /* Sky */
+ 0xff3f3fff, /* Frozen Ocean */
+ 0xff3f3fff, /* Frozen River */
+ 0xff7fffcf, /* Ice Plains */
+ 0xff3fcf7f, /* Ice Mountains */
+ 0xffcf00cf, /* Mushroom Island */
+ 0xff7f00ff, /* Mushroom Island Shore */
+ 0xffffff3f, /* Beach */
+ 0xffcfcf00, /* Desert Hills */
+ 0xff00cf3f, /* Forest Hills */
+ 0xff006f1f, /* Taiga Hills */
+ 0xff7f8f7f, /* Extreme Hills Edge */
+ 0xff004f00, /* Jungle */
+ 0xff003f00, /* Jungle Hills */
+ } ;
+
+ // Remove trees and other unwanted stuff from the heightmap:
+ for (int z = 0; z < 16; z++)
+ {
+ int PixelLine[16]; // line of 16 pixels that is used as a buffer for setting the image pixels
+ for (int x = 0; x < 16; x++)
+ {
+ int Height = m_ChunkHeight[16 * z + x];
+ for (int y = Height; y >= 0; y--)
+ {
+ if (cHeightMap::IsGround(m_BlockTypes[256 * y + 16 * z + x]))
+ {
+ Height = y;
+ break; // for y
+ }
+ } // for y
+
+ // Set the color based on the biome and height:
+ char Biome = m_ChunkBiomes[16 * z + x];
+ PixelLine[x] = ShadeColor(BiomePalette[Biome], Height);
+ } // for x
+
+ // Set the pixelline into the image:
+ SetPixelURow(m_CurrentChunkRelX * 16, m_CurrentChunkRelZ * 16 + z, 16, PixelLine);
+ } // for z
+ return CALLBACK_ABORT;
+}
+
+
+
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// cHeightBiomeMapFactory:
+
+cHeightBiomeMapFactory::~cHeightBiomeMapFactory()
+{
+ // Get the min and max region coords:
+ int MinRegionX = 100000;
+ int MaxRegionX = -100000;
+ int MinRegionZ = 100000;
+ int MaxRegionZ = -100000;
+ for (cCallbacks::iterator itr = m_Callbacks.begin(), end = m_Callbacks.end(); itr != end; ++itr)
+ {
+ cHeightBiomeMap * cb = (cHeightBiomeMap *)(*itr);
+ if (cb->m_MinRegionX < MinRegionX)
+ {
+ MinRegionX = cb->m_MinRegionX;
+ }
+ if (cb->m_MaxRegionX > MaxRegionX)
+ {
+ MaxRegionX = cb->m_MaxRegionX;
+ }
+ if (cb->m_MinRegionZ < MinRegionZ)
+ {
+ MinRegionZ = cb->m_MinRegionZ;
+ }
+ if (cb->m_MaxRegionZ > MaxRegionZ)
+ {
+ MaxRegionZ = cb->m_MaxRegionZ;
+ }
+ }
+
+ // If the size is small enough, write an HTML file referencing all the images in a table:
+ if ((MaxRegionX >= MinRegionX) && (MaxRegionZ >= MinRegionZ) && (MaxRegionX - MinRegionX < 100) && (MaxRegionZ - MinRegionZ < 100))
+ {
+ cFile HTML("HeBi.html", cFile::fmWrite);
+ if (HTML.IsOpen())
+ {
+ HTML.Printf("<html><body><table cellspacing=0 cellpadding=0>\n");
+ for (int z = MinRegionZ; z <= MaxRegionZ; z++)
+ {
+ HTML.Printf("<tr>");
+ for (int x = MinRegionX; x <= MaxRegionX; x++)
+ {
+ HTML.Printf("<td><img src=\"HeBi.%d.%d.bmp\" /></td>", x, z);
+ }
+ HTML.Printf("</tr>\n");
+ }
+ HTML.Printf("</table></body></html>");
+ }
+ }
+}
+
+
+
+
diff --git a/Tools/AnvilStats/HeightBiomeMap.h b/Tools/AnvilStats/HeightBiomeMap.h
new file mode 100644
index 000000000..d38fa4733
--- /dev/null
+++ b/Tools/AnvilStats/HeightBiomeMap.h
@@ -0,0 +1,81 @@
+
+// HeightBiomeMap.h
+
+// Declares the cHeightBiomeMap class representing a stats module that produces an image of heights and biomes combined
+
+
+
+
+
+#pragma once
+
+#include "ImageComposingCallback.h"
+
+
+
+
+
+class cHeightBiomeMap :
+ public cImageComposingCallback
+{
+ typedef cImageComposingCallback super;
+
+public:
+ // Minima and maxima for the regions processed through this callback
+ int m_MinRegionX, m_MaxRegionX;
+ int m_MinRegionZ, m_MaxRegionZ;
+
+ cHeightBiomeMap(void);
+
+protected:
+ int m_CurrentChunkX; // Absolute chunk coords
+ int m_CurrentChunkZ;
+ int m_CurrentChunkRelX; // Chunk offset from the start of the region
+ int m_CurrentChunkRelZ;
+
+ char m_ChunkBiomes[16 * 16]; ///< Biome-map for the current chunk
+ int m_ChunkHeight[16 * 16]; ///< Height-map for the current chunk
+ BLOCKTYPE m_BlockTypes [16 * 16 * 256]; ///< Block data for the current chunk (between OnSection() and OnSectionsFinished() )
+
+ // cCallback overrides:
+ virtual bool OnNewRegion(int a_RegionX, int a_RegionZ) override;
+ virtual bool OnNewChunk(int a_ChunkX, int a_ChunkZ) override;
+ virtual bool OnHeader(int a_FileOffset, unsigned char a_NumSectors, int a_Timestamp) override { return CALLBACK_CONTINUE; }
+ virtual bool OnCompressedDataSizePos(int a_CompressedDataSize, int a_DataOffset, char a_CompressionMethod) override { return CALLBACK_CONTINUE; }
+ virtual bool OnDecompressedData(const char * a_DecompressedNBT, int a_DataSize) override { return CALLBACK_CONTINUE; }
+ virtual bool OnRealCoords(int a_ChunkX, int a_ChunkZ) override { return CALLBACK_CONTINUE; }
+ virtual bool OnLastUpdate(Int64 a_LastUpdate) override { return CALLBACK_CONTINUE; }
+ virtual bool OnTerrainPopulated(bool a_Populated) override { return a_Populated ? CALLBACK_CONTINUE : CALLBACK_ABORT; } // If not populated, we don't want it!
+ virtual bool OnBiomes(const unsigned char * a_BiomeData) override;
+ virtual bool OnHeightMap(const int * a_HeightMapBE) override;
+ virtual bool OnSection(
+ unsigned char a_Y,
+ const BLOCKTYPE * a_BlockTypes,
+ const NIBBLETYPE * a_BlockAdditional,
+ const NIBBLETYPE * a_BlockMeta,
+ const NIBBLETYPE * a_BlockLight,
+ const NIBBLETYPE * a_BlockSkyLight
+ ) override;
+ virtual bool OnSectionsFinished(void) override;
+
+} ;
+
+
+
+
+
+class cHeightBiomeMapFactory :
+ public cCallbackFactory
+{
+public:
+ virtual ~cHeightBiomeMapFactory();
+
+ virtual cCallback * CreateNewCallback(void) override
+ {
+ return new cHeightBiomeMap;
+ }
+} ;
+
+
+
+
diff --git a/Tools/AnvilStats/HeightMap.h b/Tools/AnvilStats/HeightMap.h
index c0e71cbc1..e1d73f300 100644
--- a/Tools/AnvilStats/HeightMap.h
+++ b/Tools/AnvilStats/HeightMap.h
@@ -23,6 +23,8 @@ public:
void Finish(void);
+ static bool IsGround(BLOCKTYPE a_BlockType);
+
protected:
int m_CurrentChunkX; // Absolute chunk coords
int m_CurrentChunkZ;
@@ -55,8 +57,6 @@ protected:
virtual bool OnSectionsFinished(void) override;
void StartNewRegion(int a_RegionX, int a_RegionZ);
-
- static bool IsGround(BLOCKTYPE a_BlockType);
} ;
diff --git a/Tools/AnvilStats/ImageComposingCallback.cpp b/Tools/AnvilStats/ImageComposingCallback.cpp
new file mode 100644
index 000000000..eb43ad49f
--- /dev/null
+++ b/Tools/AnvilStats/ImageComposingCallback.cpp
@@ -0,0 +1,219 @@
+
+// ImageComposingCallback.cpp
+
+// Implements the cImageComposingCallback class that implements a subset of cCallback for composing per-region images
+
+#include "Globals.h"
+#include "ImageComposingCallback.h"
+
+
+
+
+
+cImageComposingCallback::cImageComposingCallback(const AString & a_FileNamePrefix) :
+ m_FileNamePrefix(a_FileNamePrefix),
+ m_CurrentRegionX(INVALID_REGION_COORD),
+ m_CurrentRegionZ(INVALID_REGION_COORD),
+ m_ImageData(new int[32 * 16 * 32 * 16])
+{
+}
+
+
+
+
+
+cImageComposingCallback::~cImageComposingCallback()
+{
+ delete[] m_ImageData;
+}
+
+
+
+
+
+bool cImageComposingCallback::OnNewRegion(int a_RegionX, int a_RegionZ)
+{
+ ASSERT(m_CurrentRegionX == INVALID_REGION_COORD);
+ ASSERT(m_CurrentRegionZ == INVALID_REGION_COORD); // Has any previous region been finished properly?
+
+ m_CurrentRegionX = a_RegionX;
+ m_CurrentRegionZ = a_RegionZ;
+ OnEraseImage();
+
+ return CALLBACK_CONTINUE;
+}
+
+
+
+
+
+void cImageComposingCallback::OnRegionFinished(int a_RegionX, int a_RegionZ)
+{
+ ASSERT(m_CurrentRegionX != INVALID_REGION_COORD);
+ ASSERT(m_CurrentRegionZ != INVALID_REGION_COORD); // Has a region been started properly?
+ ASSERT(m_CurrentRegionX == a_RegionX);
+ ASSERT(m_CurrentRegionZ == a_RegionZ); // Is it the same region that has been started?
+
+ AString FileName = GetFileName(a_RegionX, a_RegionZ);
+ if (!FileName.empty())
+ {
+ OnBeforeImageSaved(a_RegionX, a_RegionZ, FileName);
+ SaveImage(FileName);
+ OnAfterImageSaved(a_RegionX, a_RegionZ, FileName);
+ }
+
+ m_CurrentRegionX = INVALID_REGION_COORD;
+ m_CurrentRegionZ = INVALID_REGION_COORD;
+}
+
+
+
+
+
+AString cImageComposingCallback::GetFileName(int a_RegionX, int a_RegionZ)
+{
+ return Printf("%s.%d.%d.bmp", m_FileNamePrefix.c_str(), a_RegionX, a_RegionZ);
+}
+
+
+
+
+
+void cImageComposingCallback::OnEraseImage(void)
+{
+ // By default erase the image to black:
+ EraseImage(0);
+}
+
+
+
+
+
+void cImageComposingCallback::EraseImage(int a_Color)
+{
+ for (int i = 0; i < PIXEL_COUNT; i++)
+ {
+ m_ImageData[i] = a_Color;
+ }
+}
+
+
+
+
+
+void cImageComposingCallback::EraseChunk(int a_Color, int a_RelChunkX, int a_RelChunkZ)
+{
+ int Base = a_RelChunkZ * IMAGE_HEIGHT + a_RelChunkX * 16;
+ for (int v = 0; v < 16; v++)
+ {
+ int BaseV = Base + v * IMAGE_HEIGHT;
+ for (int u = 0; u < 16; u++)
+ {
+ m_ImageData[BaseV + u] = a_Color;
+ }
+ } // for y
+}
+
+
+
+
+
+void cImageComposingCallback::SetPixel(int a_RelU, int a_RelV, int a_Color)
+{
+ ASSERT((a_RelU >= 0) && (a_RelU < IMAGE_WIDTH));
+ ASSERT((a_RelV >= 0) && (a_RelV < IMAGE_HEIGHT));
+
+ m_ImageData[a_RelU + IMAGE_WIDTH * a_RelV] = a_Color;
+}
+
+
+
+
+
+int cImageComposingCallback::GetPixel(int a_RelU, int a_RelV)
+{
+ if ((a_RelU < 0) || (a_RelU >= IMAGE_WIDTH) || (a_RelV < 0) || (a_RelV >= IMAGE_HEIGHT))
+ {
+ // Outside the image data
+ return -1;
+ }
+
+ return m_ImageData[a_RelU + IMAGE_WIDTH * a_RelV];
+}
+
+
+
+
+
+
+void cImageComposingCallback::SetPixelURow(int a_RelUStart, int a_RelV, int a_CountU, int * a_Pixels)
+{
+ ASSERT((a_RelUStart >= 0) && (a_RelUStart + a_CountU <= IMAGE_WIDTH));
+ ASSERT((a_RelV >= 0) && (a_RelV < IMAGE_HEIGHT));
+ ASSERT(a_Pixels != NULL);
+
+ int Base = a_RelUStart + a_RelV * IMAGE_WIDTH;
+ for (int u = 0; u < a_CountU; u++)
+ {
+ m_ImageData[Base + u] = a_Pixels[u];
+ }
+}
+
+
+
+
+
+int cImageComposingCallback::ShadeColor(int a_Color, int a_Shade)
+{
+ if (a_Shade < 64)
+ {
+ return MixColor(0, a_Color, a_Shade * 4);
+ }
+ return MixColor(a_Color, 0xffffff, (a_Shade - 64) * 4);
+}
+
+
+
+
+
+int cImageComposingCallback::MixColor(int a_Src, int a_Dest, int a_Amount)
+{
+ int r = a_Src & 0xff;
+ int g = (a_Src >> 8) & 0xff;
+ int b = (a_Src >> 16) & 0xff;
+ int rd = a_Dest & 0xff;
+ int gd = (a_Dest >> 8) & 0xff;
+ int bd = (a_Dest >> 16) & 0xff;
+ int nr = r + (rd - r) * a_Amount / 256;
+ int ng = g + (gd - g) * a_Amount / 256;
+ int nb = b + (bd - b) * a_Amount / 256;
+ return nr | (ng << 8) | (nb << 16);
+}
+
+
+
+
+void cImageComposingCallback::SaveImage(const AString & a_FileName)
+{
+ cFile f(a_FileName, cFile::fmWrite);
+ if (!f.IsOpen())
+ {
+ return;
+ }
+
+ // Header for BMP files (is the same for the same-size files)
+ static const unsigned char BMPHeader[] =
+ {
+ 0x42, 0x4D, 0x36, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
+ 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x13, 0x0B, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ } ;
+
+ f.Write(BMPHeader, sizeof(BMPHeader));
+ f.Write(m_ImageData, PIXEL_COUNT * 4);
+}
+
+
+
+
diff --git a/Tools/AnvilStats/ImageComposingCallback.h b/Tools/AnvilStats/ImageComposingCallback.h
new file mode 100644
index 000000000..2936361d6
--- /dev/null
+++ b/Tools/AnvilStats/ImageComposingCallback.h
@@ -0,0 +1,105 @@
+
+// ImageComposingCallback
+
+// Declares the cImageComposingCallback class that implements a subset of cCallback for composing per-region images
+
+
+
+
+
+#pragma once
+
+#include "Callback.h"
+
+
+
+
+/** Implements the plumbing for composing per-region images from multiple chunks.
+To use this class, create a descendant that writes the image data using
+SetPixel() or SetPixelURow() functions.
+
+For the purpose of this class the image data is indexed U (horz) * V (vert), to avoid confusion with other coords.
+The image is a 32bpp raw imagedata, written into a BMP file.
+*/
+class cImageComposingCallback :
+ public cCallback
+{
+public:
+ enum
+ {
+ INVALID_REGION_COORD = 99999, ///< Used for un-assigned region coords
+ IMAGE_WIDTH = 32 * 16,
+ IMAGE_HEIGHT = 32 * 16,
+ PIXEL_COUNT = IMAGE_WIDTH * IMAGE_HEIGHT, ///< Total pixel count of the image data
+ } ;
+
+ cImageComposingCallback(const AString & a_FileNamePrefix);
+ virtual ~cImageComposingCallback();
+
+ // cCallback overrides:
+ virtual bool OnNewRegion(int a_RegionX, int a_RegionZ) override;
+ virtual void OnRegionFinished(int a_RegionX, int a_RegionZ) override;
+
+ // New introduced overridable functions:
+
+ /// Called when a file is about to be saved, to generate the filename
+ virtual AString GetFileName(int a_RegionX, int a_RegionZ);
+
+ /// Called before the file is saved
+ virtual void OnBeforeImageSaved(int a_RegionX, int a_RegionZ, const AString & a_FileName) {}
+
+ /// Called after the image is saved to a file
+ virtual void OnAfterImageSaved(int a_RegionX, int a_RegionZ, const AString & a_FileName) {}
+
+ /// Called when a new region is beginning, to erase the image data
+ virtual void OnEraseImage(void);
+
+ // Functions for manipulating the image:
+
+ /// Erases the entire image with the specified color
+ void EraseImage(int a_Color);
+
+ /// Erases the specified chunk's portion of the image with the specified color. Note that chunk coords are relative to the current region
+ void EraseChunk(int a_Color, int a_RelChunkX, int a_RelChunkZ);
+
+ /// Returns the current region X coord
+ int GetCurrentRegionX(void) const { return m_CurrentRegionX; }
+
+ /// Returns the current region Z coord
+ int GetCurrentRegionZ(void) const { return m_CurrentRegionZ; }
+
+ /// Sets the pixel at the specified UV coords to the specified color
+ void SetPixel(int a_RelU, int a_RelV, int a_Color);
+
+ /// Returns the color of the pixel at the specified UV coords; -1 if outside
+ int GetPixel(int a_RelU, int a_RelV);
+
+ /// Sets a row of pixels. a_Pixels is expected to be a_CountU pixels wide. a_RelUStart + a_CountU is assumed less than image width
+ void SetPixelURow(int a_RelUStart, int a_RelV, int a_CountU, int * a_Pixels);
+
+ /** "Shades" the given color based on the shade amount given
+ Shade amount 0 .. 63 shades the color from black to a_Color.
+ Shade amount 64 .. 127 shades the color from a_Color to white.
+ All other shade amounts have undefined results.
+ */
+ static int ShadeColor(int a_Color, int a_Shade);
+
+ /// Mixes the two colors in the specified ratio; a_Ratio is between 0 and 256, 0 returning a_Src
+ static int MixColor(int a_Src, int a_Dest, int a_Ratio);
+
+protected:
+ /// Prefix for the filenames, when generated by the default GetFileName() function
+ AString m_FileNamePrefix;
+
+ /// Coords of the currently processed region
+ int m_CurrentRegionX, m_CurrentRegionZ;
+
+ /// Raw image data; 1 MiB worth of data, therefore unsuitable for stack allocation. [u + IMAGE_WIDTH * v]
+ int * m_ImageData;
+
+ void SaveImage(const AString & a_FileName);
+} ;
+
+
+
+
diff --git a/Tools/AnvilStats/Processor.cpp b/Tools/AnvilStats/Processor.cpp
index e7f7eb21d..8e1cc4c4b 100644
--- a/Tools/AnvilStats/Processor.cpp
+++ b/Tools/AnvilStats/Processor.cpp
@@ -76,6 +76,12 @@ void cProcessor::cThread::ProcessFile(const AString & a_FileName)
return;
}
+ if (m_Callback.OnNewRegion(RegionX, RegionZ))
+ {
+ // Callback doesn't want the region file processed
+ return;
+ }
+
cFile f;
if (!f.Open(a_FileName, cFile::fmRead))
{
@@ -92,6 +98,8 @@ void cProcessor::cThread::ProcessFile(const AString & a_FileName)
}
ProcessFileData(FileContents.data(), FileContents.size(), RegionX * 32, RegionZ * 32);
+
+ m_Callback.OnRegionFinished(RegionX, RegionZ);
}