Просмотр исходного кода

The world map loads as a single model

Iñigo Valentin 1 неделя назад
Родитель
Сommit
6b587adf9b

+ 199 - 88
src/data/worldmap/WorldmapFileSerializer.cpp

@@ -15,14 +15,88 @@
 
 #include "WorldmapFileSerializer.h"
 
+#include <OgreException.h>
+#include <string>
 #include "common/LzsFile.h"
 
 namespace VGears {
 
-    /**
-     * Size of each block in a world map file.
-     */
-    const int WORLD_MAP_BLOCK_SIZE = 0xB800;
+    namespace {
+
+        const size_t WORLD_MAP_BLOCK_SIZE = 0xB800;
+        const size_t WORLD_MAP_BLOCK_HEADER_SIZE = sizeof(uint32) * 16;
+        const size_t WORLD_MAP_TRIANGLE_SIZE = 12;
+        const size_t WORLD_MAP_VERTEX_SIZE = 8;
+
+        uint16 ReadLittleEndian16(const std::vector<uint8>& data, size_t offset){
+            return static_cast<uint16>(data[offset])
+              | (static_cast<uint16>(data[offset + 1]) << 8);
+        }
+
+        uint32 ReadLittleEndian32(const std::vector<uint8>& data, size_t offset){
+            return static_cast<uint32>(data[offset])
+              | (static_cast<uint32>(data[offset + 1]) << 8)
+              | (static_cast<uint32>(data[offset + 2]) << 16)
+              | (static_cast<uint32>(data[offset + 3]) << 24);
+        }
+
+        sint16 ReadLittleEndianSigned16(
+          const std::vector<uint8>& data, size_t offset
+        ){
+            const uint16 value = ReadLittleEndian16(data, offset);
+            const sint32 signed_value = value <= 0x7FFF
+              ? static_cast<sint32>(value)
+              : static_cast<sint32>(value) - 0x10000;
+            return static_cast<sint16>(signed_value);
+        }
+
+        void ThrowInvalidWorldmap(const std::string& reason){
+            OGRE_EXCEPT(
+              Ogre::Exception::ERR_INVALIDPARAMS,
+              "Invalid world map file: " + reason,
+              "WorldmapFileSerializer::ImportWorldmapFile"
+            );
+        }
+
+        void ValidateLzsData(
+          const std::vector<uint8>& data, size_t max_output_size
+        ){
+            if (data.size() <= 4
+              || ReadLittleEndian32(data, 0) != data.size() - 4){
+                ThrowInvalidWorldmap("invalid LZS payload size");
+            }
+
+            size_t input_offset = 4;
+            size_t output_size = 0;
+            while (input_offset < data.size()){
+                const uint8 control = data[input_offset++];
+                bool has_token = false;
+                for (unsigned int bit = 0;
+                  bit < 8 && input_offset < data.size(); ++bit){
+                    has_token = true;
+                    size_t token_output_size = 1;
+                    if ((control & (1u << bit)) == 0){
+                        if (data.size() - input_offset < 2){
+                            ThrowInvalidWorldmap("truncated LZS back-reference");
+                        }
+                        token_output_size
+                          = static_cast<size_t>(data[input_offset + 1] & 0x0F) + 3;
+                        input_offset += 2;
+                    }
+                    else ++input_offset;
+
+                    if (token_output_size > max_output_size - output_size){
+                        ThrowInvalidWorldmap("LZS output exceeds a MAP block");
+                    }
+                    output_size += token_output_size;
+                }
+                if (!has_token){
+                    ThrowInvalidWorldmap("truncated LZS control byte");
+                  }
+            }
+        }
+
+    }
 
     WorldmapFileSerializer::WorldmapFileSerializer(){}
 
@@ -31,101 +105,138 @@ namespace VGears {
     void WorldmapFileSerializer::ImportWorldmapFile(
       Ogre::DataStreamPtr& stream, WorldmapFile& dest
     ){
-        const auto fileSize = stream->size();
-        auto nublocks = fileSize / WORLD_MAP_BLOCK_SIZE;
-        for (unsigned int j = 0; j < nublocks; j ++){
+        static_cast<void>(dest);
+        if (stream.isNull()){
+            ThrowInvalidWorldmap("input stream is null");
+        }
+
+        const size_t file_size = stream->size();
+        if (file_size == 0 || file_size % WORLD_MAP_BLOCK_SIZE != 0){
+            ThrowInvalidWorldmap("file size is not a whole number of MAP blocks");
+        }
+
+        std::vector<uint8> file_data(file_size);
+        stream->seek(0);
+        if (stream->read(file_data.data(), file_data.size()) != file_data.size()){
+            ThrowInvalidWorldmap("could not read the complete file");
+        }
+
+        std::vector<Block> parsed_blocks;
+        const size_t block_count = file_size / WORLD_MAP_BLOCK_SIZE;
+        parsed_blocks.reserve(block_count);
+        for (size_t block_index = 0; block_index < block_count; ++block_index){
             Block block;
-            const size_t basePos = WORLD_MAP_BLOCK_SIZE * j;
-            stream->seek(basePos);
-            // Read the offset to compressed data in this block.
-            BlockHeader header = {};
-            for (auto i = 0u; i < 16; i ++)
-                ReadUInt32(stream, header.compressed_data_offsets[i]);
-            for (auto i = 0u; i < 16; i ++){
-                BlockPart block_part;
-                // Go to the offset.
-                stream->seek(basePos + header.compressed_data_offsets[i]);
-                // Read the size of the compressed data.
-                uint32 compressed_data_size = 0;
-                ReadUInt32(stream, compressed_data_size);
-                // Go back to before the compressed data size.
-                stream->seek(basePos + header.compressed_data_offsets[i]);
-                // Read the compressed data into a temp buffer, including the
-                // compressed data size.
-                std::vector<uint8> buffer(compressed_data_size + 4);
-                stream->read(buffer.data(), buffer.size());
-                // Decompress the data.
-                auto decompressed = LzsBuffer::Decompress(buffer);
-                Ogre::MemoryDataStream decompressed_stream(
-                  decompressed.data(), decompressed.size(), false, true
+            const size_t block_start = block_index * WORLD_MAP_BLOCK_SIZE;
+            block.meshes.reserve(16);
+            for (size_t mesh_index = 0; mesh_index < 16; ++mesh_index){
+                const uint32 mesh_offset = ReadLittleEndian32(
+                  file_data, block_start + mesh_index * sizeof(uint32)
                 );
-                ReadUInt16(
-                  decompressed_stream, block_part.header.triengle_count
+                if (mesh_offset < WORLD_MAP_BLOCK_HEADER_SIZE
+                  || mesh_offset % 4 != 0
+                  || mesh_offset > WORLD_MAP_BLOCK_SIZE - sizeof(uint32)){
+                    ThrowInvalidWorldmap("mesh offset is invalid");
+                }
+
+                const size_t mesh_start = block_start + mesh_offset;
+                const uint32 compressed_data_size
+                  = ReadLittleEndian32(file_data, mesh_start);
+                const size_t max_compressed_size
+                  = WORLD_MAP_BLOCK_SIZE - mesh_offset - sizeof(uint32);
+                if (compressed_data_size > max_compressed_size){
+                    ThrowInvalidWorldmap("compressed mesh extends past its MAP block");
+                }
+
+                const size_t compressed_size
+                  = static_cast<size_t>(compressed_data_size) + sizeof(uint32);
+                std::vector<uint8> compressed_data(
+                  file_data.begin() + mesh_start,
+                  file_data.begin() + mesh_start + compressed_size
                 );
-                ReadUInt16(decompressed_stream, block_part.header.vertex_count);
-
-                /*
-                std::cout << "block: " << j
-                  << " from offset " << header.compressed_data_offsets[i]
-                  << " old size: " << buffer.size()
-                  << " decompressed size is " << decompressed.size()
-                  << " header is tris: " <<  block_part.header.triengle_count
-                  << " verts " << block_part.header.vertex_count
-                  << std::endl;
-                */
+                ValidateLzsData(compressed_data, WORLD_MAP_BLOCK_SIZE);
+                const std::vector<uint8> decompressed
+                  = LzsBuffer::Decompress(compressed_data);
+                if (decompressed.size() < 4){
+                    ThrowInvalidWorldmap("decompressed mesh header is truncated");
+                }
 
+                BlockPart block_part;
+                block_part.header.triengle_count
+                  = ReadLittleEndian16(decompressed, 0);
+                block_part.header.vertex_count
+                  = ReadLittleEndian16(decompressed, 2);
+                const size_t expected_size = 4
+                  + static_cast<size_t>(block_part.header.triengle_count)
+                    * WORLD_MAP_TRIANGLE_SIZE
+                  + static_cast<size_t>(block_part.header.vertex_count)
+                    * WORLD_MAP_VERTEX_SIZE * 2;
+                if (expected_size > decompressed.size()){
+                    ThrowInvalidWorldmap("decompressed mesh data is truncated");
+                }
+
+                size_t data_offset = 4;
                 block_part.triangles.resize(block_part.header.triengle_count);
-                for (int k = 0; k < block_part.header.triengle_count; k ++){
-                    BlockTriangle& s = block_part.triangles[k];
-                    ReadUInt8(decompressed_stream, s.vertex_0_index);
-                    ReadUInt8(decompressed_stream, s.vertex_1_index);
-                    ReadUInt8(decompressed_stream, s.vertex_2_index);
-                    ReadUInt8(decompressed_stream, s.walkability_info_);
-                    //ReadUInt8(decompressed_stream, s.unknown);
-                    ReadUInt8(decompressed_stream, s.vertex_0_u);
-                    ReadUInt8(decompressed_stream, s.vertex_0_v);
-                    ReadUInt8(decompressed_stream, s.vertex_1_u);
-                    ReadUInt8(decompressed_stream, s.vertex_1_v);
-                    ReadUInt8(decompressed_stream, s.vertex_2_u);
-                    ReadUInt8(decompressed_stream, s.vertex_2_v);
-                    ReadUInt16(decompressed_stream, s.TextureInfo);
-                    s.TextureInfo = s.TextureInfo & 0x1FF;
-                    //ReadUInt16(decompressed_stream, s.Location);
-                    /*
-                    std::cout << "v0: " << int(s.vertex_0_index)
-                      << " v1 " << int(s.vertex_1_index)
-                      << " v2 " << int(s.vertex_2_index)
-                      << " walk " << int(s.walkability_info_)
-                      << " u1 " << int(s.vertex_1_u)
-                      << " v1 " << int(s.vertex_1_v)
-                      << " v2 " << int(s.vertex_2_u)
-                      << " u2 " << int(s.vertex_2_v)
-                      << " texture " << s.TextureInfo
-                      << " locId " << s.Location
-                      << std::endl;
-                    */
+                for (size_t triangle_index = 0;
+                  triangle_index < block_part.triangles.size(); ++triangle_index){
+                    BlockTriangle& triangle = block_part.triangles[triangle_index];
+                    triangle.vertex_0_index = decompressed[data_offset++];
+                    triangle.vertex_1_index = decompressed[data_offset++];
+                    triangle.vertex_2_index = decompressed[data_offset++];
+                    const uint8 walkmap_and_function = decompressed[data_offset++];
+                    triangle.walkability_info_ = walkmap_and_function & 0x1F;
+                    triangle.unknown = (walkmap_and_function >> 5) & 0x07;
+                    triangle.vertex_0_u = decompressed[data_offset++];
+                    triangle.vertex_0_v = decompressed[data_offset++];
+                    triangle.vertex_1_u = decompressed[data_offset++];
+                    triangle.vertex_1_v = decompressed[data_offset++];
+                    triangle.vertex_2_u = decompressed[data_offset++];
+                    triangle.vertex_2_v = decompressed[data_offset++];
+                    const uint16 texture_and_location
+                      = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    triangle.TextureInfo = texture_and_location & 0x1FF;
+                    triangle.Location = (texture_and_location >> 9) & 0x7F;
+
+                    if (triangle.vertex_0_index >= block_part.header.vertex_count
+                      || triangle.vertex_1_index >= block_part.header.vertex_count
+                      || triangle.vertex_2_index >= block_part.header.vertex_count){
+                        ThrowInvalidWorldmap("triangle references a missing vertex");
+                    }
                 }
-                block_part.normals.resize(block_part.header.vertex_count);
+
                 block_part.vertices.resize(block_part.header.vertex_count);
-                // All vertices.
-                for (int k = 0; k < block_part.header.vertex_count; k ++){
-                    Vertex& v = block_part.vertices[k];
-                    ReadInt16(decompressed_stream, v.x);
-                    ReadInt16(decompressed_stream, v.y);
-                    ReadInt16(decompressed_stream, v.z);
-                    ReadUInt16(decompressed_stream, v.unused);
+                for (size_t vertex_index = 0;
+                  vertex_index < block_part.vertices.size(); ++vertex_index){
+                    Vertex& vertex = block_part.vertices[vertex_index];
+                    vertex.x = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.y = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.z = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.unused = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
                 }
-                // Then all normals.
-                for (int k = 0; k < block_part.header.vertex_count; k ++){
-                    Normal& n = block_part.normals[k];
-                    ReadInt16(decompressed_stream, n.x);
-                    ReadInt16(decompressed_stream, n.y);
-                    ReadInt16(decompressed_stream, n.z);
-                    ReadUInt16(decompressed_stream, n.unused);
+
+                block_part.normals.resize(block_part.header.vertex_count);
+                for (size_t normal_index = 0;
+                  normal_index < block_part.normals.size(); ++normal_index){
+                    Normal& normal = block_part.normals[normal_index];
+                    normal.x = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.y = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.z = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.unused = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
                 }
+
                 block.meshes.push_back(block_part);
             }
-            blocks.push_back(block);
+            parsed_blocks.push_back(block);
         }
+
+        blocks.swap(parsed_blocks);
     }
 }

+ 5 - 5
src/data/worldmap/WorldmapFileSerializer.h

@@ -36,8 +36,8 @@ namespace VGears {
                 /**
                  * Offset in this block of the compressed data.
                  *
-                 * At the compressed data offset we have another uint32 which
-                 * is the size of the uncompressed data.
+                 * At the compressed data offset there is a uint32 containing
+                 * the size of the compressed LZS payload.
                  */
                 uint32 compressed_data_offsets[16];
             };
@@ -139,11 +139,11 @@ namespace VGears {
                 uint8 walkability_info_;
 
                 /**
-                 * Unknown data.
+                 * Mesh function ID.
                  *
                  * Only 3 bits are valid.
                  *
-                 * @todo Find out what this is for.
+                 * Values 0-2 mean no script is attached to this triangle.
                  */
                 uint8 unknown;
 
@@ -445,7 +445,7 @@ namespace VGears {
              * Imports and parses a map file.
              *
              * @param[in] stream Map file contents.
-             * @param[in] dest Unused
+             * @param[in] dest Resource parameter retained for API compatibility.
              */
             void ImportWorldmapFile(Ogre::DataStreamPtr& stream, WorldmapFile& dest);
 

+ 3 - 0
src/main.cpp

@@ -122,6 +122,9 @@ int main(int argc, char *argv[]){
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/wm", "FileSystem", "WORLD_MAP"
         );
+        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
+          "./temp/world_models", "FileSystem", "WORLD_MAP"
+        );
         Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
 
         // Initialize it before console because it may use it

+ 170 - 223
src/modules/worldmap/WorldmapModule.cpp

@@ -15,21 +15,26 @@
 
 #include "WorldmapModule.h"
 
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
 #include <OgreSceneManager.h>
 #include <OgreManualObject.h>
 #include <OgreRoot.h>
+#include <OgreException.h>
 #include <OgreHardwarePixelBuffer.h>
-#include <OgreEntity.h>
 #include <OgreLogManager.h>
 #include <OgreTextureManager.h>
 #include <OgreMaterialManager.h>
-#include <OgreTechnique.h>
+#include <OgreResourceGroupManager.h>
+#include "data/VGearsTexFile.h"
 #include "data/worldmap/WorldmapFile.h"
 #include "data/worldmap/WorldmapFileManager.h"
 #include "data/worldmap/WorldmapFileSerializer.h"
 #include "core/CameraManager.h"
-#include "data/worldmap/TxzFile.h"
-#include "data/worldmap/TxzFileSerializer.h"
 
 /**
  * World map module singleton.
@@ -364,80 +369,38 @@ namespace VGears {
         {281, "wtrk", 32, 64, 64, 96}
     };
 
-    static const char* WORLD_RESOURCE_GROUP = "FFVII_World";
+    static const char* WORLD_MAP_RESOURCE_GROUP = "WORLD_MAP";
 
     /**
-     * Creates a reference texture.
+     * Loads and parses the installed WM0 map.
      *
-     * @param[out] s The texture will be loaded here.
-     * @todo Understand and document properly.
-     */
-    static void CreateReferenceTextureFileInstance(VGears::TxzFileSerializer& s){
-
-        /**
-         * A test file.
-         */
-        class TestFile : public VGears::TxzFile{
-
-            public:
-
-                /**
-                 * Constructor.
-                 *
-                 * Generates a null TXZ file.
-                 */
-                TestFile() : VGears::TxzFile(nullptr, "", 0, "") {}
-        };
-
-        // TODO: Get correct path
-        // TODO: What is this file? Where does it come from?
-        const char* file_name = "data/wm/wm0.txz";
-        std::ifstream ifs(file_name, std::ifstream::binary);
-        Ogre::DataStreamPtr stream(OGRE_NEW Ogre::FileStreamDataStream(&ifs, false));
-        // Ogre::LogManager log;
-        TestFile f;
-        s.Import(stream, f);
-    }
-
-    /**
-     * Creates a reference file.
-     *
-     * @param[in,out] callBack The callBack function.
-     * @todo Understand and document properly.
+     * @return The map blocks.
      */
-    static void CreateReferenceFileInstance(
-      std::function<void(Ogre::DataStreamPtr& stream, VGears::WorldmapFile& file)> callBack
-    ){
+    static std::vector<VGears::WorldmapFileSerializer::Block> LoadWorldMap(){
         /**
-         * A test file.
+       * The serializer retains its parsed blocks; the resource argument is
+       * currently unused by the MAP serializer.
          */
         class TestFile : public VGears::WorldmapFile{
 
             public:
 
-                /**
-                 * Constructor.
-                 *
-                 * Generates a null WM file.
-                 */
                 TestFile() : VGears::WorldmapFile(nullptr, "", 0, "") {}
         };
-        const char* file_name = "data/wm/WM0.MAP";
-        std::ifstream ifs(file_name, std::ifstream::binary);
-        if (!ifs.is_open()){
-            LogManager::getSingleton().logMessage("ERROR: Failed to open data/wm/WM0.MAP");
-            return;
-        }
-        {
-            // BOOST_REQUIRE(ifs.is_open());
-            Ogre::DataStreamPtr stream(OGRE_NEW Ogre::FileStreamDataStream(&ifs, false));
-            // BOOST_REQUIRE(stream->isReadable());
-            //Ogre::LogManager log;
-            TestFile f;
-            // log.createLog("Default Log", true, true, true);
-            callBack(stream, f);
-            // log.destroyLog("Default Log");
-        }
+
+        Ogre::DataStreamPtr stream
+          = Ogre::ResourceGroupManager::getSingleton().openResource(
+              "WM0.MAP", WORLD_MAP_RESOURCE_GROUP, nullptr, true
+            );
+        TestFile file;
+        VGears::WorldmapFileSerializer serializer;
+        serializer.ImportWorldmapFile(stream, file);
+
+        Ogre::LogManager::getSingleton().logMessage(
+          "Loaded WM0.MAP with " + std::to_string(serializer.blocks.size())
+            + " blocks."
+        );
+        return serializer.blocks;
     }
 
     /**
@@ -451,43 +414,6 @@ namespace VGears {
         return (float(coord) / 4096.0f);
     }
 
-    /**
-     * Tests something.
-     *
-     * @param[in] data The test data.
-     * @todo Understand and document properly.
-     */
-    void Test(const std::vector<std::vector<TxzFileSerializer::Rgba>>& data){
-        //Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().load(
-        //  "clf_l.png.png",
-        //  Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-        //);
-
-        // World resource group
-        Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().createManual(
-          "BackgroundTex", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
-          Ogre::TEX_TYPE_2D, 640, 256, 0, Ogre::PF_B8G8R8, Ogre:: TU_DYNAMIC
-        );
-        Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create(
-          "BackgroundMat", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-        );
-        material->getTechnique(0)->getPass(0)->createTextureUnitState("BackgroundTex");
-        //material->getTechnique(0)->getPass(0)->setSceneBlending(Ogre::SBT_TRANSPARENT_COLOUR);
-        Ogre::HardwarePixelBufferSharedPtr pixel_buffer = texture->getBuffer();
-        pixel_buffer->lock(Ogre::HardwareBuffer::HBL_DISCARD);
-        const Ogre::PixelBox& pixelBox = pixel_buffer->getCurrentLock();
-        Ogre::uint8* dest = static_cast<Ogre::uint8*>(pixelBox.data);
-        for (size_t i = 0; i < 256; i ++){
-           for(size_t j = 0; j < 640; j ++){
-              *dest ++ = data[i][j].b;
-              *dest ++ = data[i][j].g;
-              *dest ++ = data[i][j].r;
-              *dest ++ = 0;
-           }
-        }
-        pixel_buffer->unlock();
-    }
-
     /**
      * Converts a coordinate to a texture U component coordinate.
      *
@@ -496,7 +422,7 @@ namespace VGears {
      * @return Texture coordinate U component.
      * @todo Understand and document properly.
      */
-    float ToTextureCoordU(uint8 coord, uint16 id){
+    static float ToTextureCoordU(uint8 coord, uint16 id){
         return (float(coord) - float(textures[id].u)) / float(textures[id].width);
         //return (float(textures[id].u) + float(coord)) / 256.0f;
     }
@@ -509,48 +435,60 @@ namespace VGears {
      * @return Texture coordinate V component.
      * @todo Understand and document properly.
      */
-    float ToTextureCoordV(uint8 coord, uint16 id){
+    static float ToTextureCoordV(uint8 coord, uint16 id){
         return (float(coord) - float(textures[id].v)) / float(textures[id].height);
         //return (float(textures[id].v) + float(coord)) / 640.0f;
     }
 
-    /**
-     * The list of loaded textures.
-     */
-    std::vector<TexturePtr> loaded_textures;
+    struct ImageDataDeleter{
 
-    /**
-     * The list of materials.
-     */
-    std::vector<MaterialPtr> materials;
+      void operator()(Ogre::ImageCodec::ImageData* image_data) const{
+        OGRE_DELETE image_data;
+      }
+    };
 
-    /**
-     * Map of texture IDs.
-     */
-    std::map<int, ManualObject*> texture_ids;
+    static TexturePtr LoadWorldMapTexture(uint16 texture_id){
+        const std::string texture_name
+          = std::string(textures[texture_id].name) + ".tex";
+        Ogre::DataStreamPtr stream
+          = ResourceGroupManager::getSingleton().openResource(
+              texture_name, WORLD_MAP_RESOURCE_GROUP, nullptr, true
+            );
+        VGears::TexFile tex_file;
+        tex_file.Read(stream);
+
+        Ogre::MemoryDataStreamPtr pixel_data(tex_file.GetPixelData());
+        std::unique_ptr<Ogre::ImageCodec::ImageData, ImageDataDeleter> image_data(
+          tex_file.GetImageData()
+        );
+        TexturePtr texture = Ogre::TextureManager::getSingleton().createManual(
+          std::string("WorldMapTex_") + textures[texture_id].name,
+          ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, TEX_TYPE_2D,
+          image_data->width, image_data->height, 0, image_data->format, TU_STATIC
+        );
+        texture->getBuffer()->blitFromMemory(Ogre::PixelBox(
+          image_data->width, image_data->height, 1, image_data->format,
+          pixel_data->getPtr()
+        ));
+        return texture;
+    }
 
-    /**
-     * Loads the map textures.
-     */
-    static void LoadTextures(){
-        if (loaded_textures.empty()){
-            for (int i = 0; i < sizeof(textures) / sizeof(textures[0]); i ++){
-                std::string texture_name = std::string(textures[i].name) + ".tex";
-                ResourceGroupManager::getSingleton().declareResource(
-                  texture_name.c_str(), "Texture", "TEST"
-                );
-                TexturePtr tmp = Ogre::TextureManager::getSingleton().load(
-                  texture_name.c_str(), "TEST", TEX_TYPE_2D, 1, 1.0f
-                );
-                loaded_textures.emplace_back(tmp);
-                Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create(
-                  textures[i].name, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-                );
-                material->getTechnique(0)->getPass(0)->createTextureUnitState(texture_name.c_str());
-                material->getTechnique(0)->getPass(0)->setTextureFiltering(TFO_NONE);
-                materials.emplace_back(material);
-            }
+    static MaterialPtr LoadTextureMaterial(uint16 texture_id){
+        TexturePtr texture = LoadWorldMapTexture(texture_id);
+        const std::string material_name
+          = std::string("WorldMap_") + textures[texture_id].name;
+        MaterialPtr material
+          = Ogre::MaterialManager::getSingleton().getByName(material_name);
+        if (material.isNull()){
+            material = Ogre::MaterialManager::getSingleton().create(
+              material_name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
+            );
+            Pass* pass = material->getTechnique(0)->getPass(0);
+            pass->createTextureUnitState(texture->getName());
+            pass->setTextureFiltering(TFO_NONE);
+            pass->setLightingEnabled(false);
         }
+        return material;
     }
 
     /**
@@ -558,117 +496,126 @@ namespace VGears {
      *
      * @param[in] scene_manager The scene manager.
      */
-    void CreateTestMap(SceneManager* scene_manager){
-        std::vector<VGears::WorldmapFileSerializer::Block> blocks;
-        CreateReferenceFileInstance([&](Ogre::DataStreamPtr& stream, VGears::WorldmapFile& file){
-            VGears::WorldmapFileSerializer s;
-            s.ImportWorldmapFile(stream, file);
-            blocks = s.blocks;
-        });
-
-        if (blocks.empty()) return; // Load failed
-        /*
-        // TODO: Get PSX textures working
-        VGears::TxzFileSerializer s;
-        createReferenceTextureFileInstance(s);
-
-        std::vector<std::vector<TxzFileSerializer::rgba>> data
-          = s.GetWorldmapTexture(19);
-        test(data);
-        */
-        LoadTextures();
-        int c = 0;
-        const auto SPACE = From16BitFixedPoint(8182);
-        size_t block_count = blocks.size();
-        int map_x = 0;
-        int map_y = 0;
-        for (unsigned int k = 0; k < block_count; k ++){
-            // 7x9 size.
-            VGears::WorldmapFileSerializer::Block& block = blocks[k];
-            int block_x = 0;
-            int block_y = 0;
-            for (size_t i = 0; i < block.meshes.size(); i ++){
-                VGears::WorldmapFileSerializer::BlockPart& part = block.meshes[i];
-                for (size_t j = 0; j < part.triangles.size(); j ++){
-                    VGears::WorldmapFileSerializer::BlockTriangle& tri
-                      = part.triangles.at(j);
+    void CreateWorldMap(SceneManager* scene_manager){
+        const std::vector<VGears::WorldmapFileSerializer::Block> blocks
+          = LoadWorldMap();
+        const size_t texture_count = sizeof(textures) / sizeof(textures[0]);
+        const size_t base_block_count = std::min(blocks.size(), size_t(63));
+        const float space = From16BitFixedPoint(8182);
+        std::map<uint16, ManualObject*> texture_objects;
+        std::set<uint16> unavailable_texture_ids;
+        unsigned int manual_index = 0;
+
+        for (size_t block_index = 0; block_index < base_block_count; ++block_index){
+            const VGears::WorldmapFileSerializer::Block& block = blocks[block_index];
+            const size_t map_x = block_index % 9;
+            const size_t map_y = block_index / 9;
+            for (size_t mesh_index = 0; mesh_index < block.meshes.size(); ++mesh_index){
+                const VGears::WorldmapFileSerializer::BlockPart& part
+                  = block.meshes[mesh_index];
+                const size_t mesh_x = mesh_index % 4;
+                const size_t mesh_y = mesh_index / 4;
+                const float pos_x = (mesh_x * space) + (map_x * space * 4);
+                const float pos_z = (mesh_y * space) + (map_y * space * 4);
+
+                for (const VGears::WorldmapFileSerializer::BlockTriangle& triangle
+                  : part.triangles){
+                    if (unavailable_texture_ids.find(triangle.TextureInfo)
+                      != unavailable_texture_ids.end()){
+                        continue;
+                    }
+                    if (triangle.TextureInfo >= texture_count){
+                        if (unavailable_texture_ids.insert(triangle.TextureInfo).second){
+                            LogManager::getSingleton().logMessage(
+                              "Skipping world-map texture ID "
+                                + std::to_string(triangle.TextureInfo)
+                                + ": no PC texture mapping is available."
+                            );
+                        }
+                        continue;
+                    }
+
                     ManualObject* manual = nullptr;
-                    auto it = texture_ids.find(tri.TextureInfo);
-                    if (it == texture_ids.end()){
+                    auto object = texture_objects.find(triangle.TextureInfo);
+                    if (object == texture_objects.end()){
+                        MaterialPtr material;
+                        try{
+                            material = LoadTextureMaterial(triangle.TextureInfo);
+                        }
+                        catch (const Ogre::Exception& exception){
+                            unavailable_texture_ids.insert(triangle.TextureInfo);
+                            LogManager::getSingleton().logMessage(
+                              "Failed to load world-map texture "
+                                + std::string(textures[triangle.TextureInfo].name)
+                                + ": " + exception.getDescription()
+                            );
+                            continue;
+                        }
+
                         manual = scene_manager->createManualObject(
-                          ("zzz_manual" + std::to_string(c++)).c_str()
-                        );
-                        manual->begin(
-                          textures[tri.TextureInfo].name, RenderOperation::OT_TRIANGLE_LIST
+                          "world_map_surface_" + std::to_string(++manual_index)
                         );
-                        manual->setDebugDisplayEnabled(true);
+                        manual->begin(material->getName(), RenderOperation::OT_TRIANGLE_LIST);
+                        manual->setDebugDisplayEnabled(false);
                         manual->setDynamic(false);
-                        texture_ids[tri.TextureInfo] = manual;
+                        texture_objects[triangle.TextureInfo] = manual;
                     }
-                    else manual = it->second;
-                    const float pos_x = (block_x * SPACE) + (map_x * SPACE * 4);
-                    const float pos_y = (block_y * SPACE) + (map_y * SPACE * 4);
+                    else manual = object->second;
 
-                    // v1
-                    VGears::WorldmapFileSerializer::Vertex v1
-                      = part.vertices.at(tri.vertex_2_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_2
+                      = part.vertices.at(triangle.vertex_2_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y);
+                      From16BitFixedPoint(vertex_2.x) + pos_x,
+                      From16BitFixedPoint(vertex_2.y),
+                      From16BitFixedPoint(vertex_2.z) + pos_z
+                    );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_2_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_2_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_2_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_2_v, triangle.TextureInfo)
                     );
 
-                    // v2
-                    v1 = part.vertices.at(tri.vertex_1_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_1
+                      = part.vertices.at(triangle.vertex_1_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y
+                      From16BitFixedPoint(vertex_1.x) + pos_x,
+                      From16BitFixedPoint(vertex_1.y),
+                      From16BitFixedPoint(vertex_1.z) + pos_z
                     );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_1_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_1_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_1_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_1_v, triangle.TextureInfo)
                     );
 
-                    // v3
-                    v1 = part.vertices.at(tri.vertex_0_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_0
+                      = part.vertices.at(triangle.vertex_0_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y
+                      From16BitFixedPoint(vertex_0.x) + pos_x,
+                      From16BitFixedPoint(vertex_0.y),
+                      From16BitFixedPoint(vertex_0.z) + pos_z
                     );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_0_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_0_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_0_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_0_v, triangle.TextureInfo)
                     );
-
-                }
-                block_x ++;
-                if (block_x >= 4){
-                    block_x = 0;
-                    block_y++;
                 }
             }
-            map_x ++;
-            if (map_x >= 9){
-                map_x = 0;
-                map_y++;
-            }
         }
-        for (auto& it : texture_ids){
-            it.second->end();
-            SceneNode* node = scene_manager->getRootSceneNode()->createChildSceneNode();
-            node->attachObject(it.second);
-            node->setPosition(0, 0, 0);
+
+        for (const auto& texture_object : texture_objects){
+            texture_object.second->end();
+            scene_manager->getRootSceneNode()->createChildSceneNode()
+              ->attachObject(texture_object.second);
         }
+
+        const float map_center_x = space * 18.0f;
+        const float map_center_z = space * 14.0f;
+        Ogre::Camera* camera = CameraManager::getSingleton().GetCurrentCamera();
+        camera->setPosition(Ogre::Vector3(map_center_x, 80.0f, map_center_z + 90.0f));
+        camera->lookAt(Ogre::Vector3(map_center_x, 0.0f, map_center_z));
     }
 
     void WorldmapModule::Init(){
-        CreateTestMap(Root::getSingleton().getSceneManager("Scene"));
-        CameraManager::getSingleton().EnableWireFrame(true);
+        CreateWorldMap(Root::getSingleton().getSceneManager("Scene"));
     }
 
 } // namespace VGears

+ 206 - 5
test/data/worldmap/MapFileSerializer.cpp

@@ -15,13 +15,214 @@
 
 #include <boost/test/unit_test.hpp>
 
+#include <algorithm>
+#include <vector>
+#include "data/worldmap/WorldmapFile.h"
 #include "../../../src/data/worldmap/WorldmapFileSerializer.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
+namespace {
 
-BOOST_AUTO_TEST_CASE(TestMapFileSerializer){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: MapFileSerializer");
-    BOOST_CHECK(0 == 0);
+        const size_t WORLD_MAP_BLOCK_SIZE = 0xB800;
+
+        class TestWorldmapFile : public VGears::WorldmapFile{
+
+                public:
+
+                        TestWorldmapFile() : VGears::WorldmapFile(nullptr, "", 0, "") {}
+        };
+
+        void AppendUInt16(std::vector<VGears::uint8>& data, VGears::uint16 value){
+                data.push_back(static_cast<VGears::uint8>(value & 0xFF));
+                data.push_back(static_cast<VGears::uint8>(value >> 8));
+        }
+
+        void AppendUInt32(std::vector<VGears::uint8>& data, VGears::uint32 value){
+                data.push_back(static_cast<VGears::uint8>(value & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 8) & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 16) & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 24) & 0xFF));
+        }
+
+        void WriteUInt32(
+            std::vector<VGears::uint8>& data, size_t offset, VGears::uint32 value
+        ){
+                data[offset] = static_cast<VGears::uint8>(value & 0xFF);
+                data[offset + 1] = static_cast<VGears::uint8>((value >> 8) & 0xFF);
+                data[offset + 2] = static_cast<VGears::uint8>((value >> 16) & 0xFF);
+                data[offset + 3] = static_cast<VGears::uint8>((value >> 24) & 0xFF);
+        }
+
+        VGears::uint32 ReadUInt32(
+            const std::vector<VGears::uint8>& data, size_t offset
+        ){
+                return static_cast<VGears::uint32>(data[offset])
+                    | (static_cast<VGears::uint32>(data[offset + 1]) << 8)
+                    | (static_cast<VGears::uint32>(data[offset + 2]) << 16)
+                    | (static_cast<VGears::uint32>(data[offset + 3]) << 24);
+        }
+
+        std::vector<VGears::uint8> CreateMeshData(
+            bool truncated, bool invalid_vertex
+        ){
+                std::vector<VGears::uint8> data;
+                AppendUInt16(data, 1);
+                AppendUInt16(data, 3);
+                if (truncated) return data;
+
+                data.push_back(invalid_vertex ? 3 : 0);
+                data.push_back(1);
+                data.push_back(2);
+                data.push_back(0xA5);
+                data.push_back(10);
+                data.push_back(20);
+                data.push_back(30);
+                data.push_back(40);
+                data.push_back(50);
+                data.push_back(60);
+                AppendUInt16(data, 0xAB55);
+
+                for (int vertex_index = 0; vertex_index < 3; ++vertex_index){
+                        AppendUInt16(data, static_cast<VGears::uint16>(-100 + vertex_index));
+                        AppendUInt16(data, static_cast<VGears::uint16>(200 + vertex_index));
+                        AppendUInt16(data, static_cast<VGears::uint16>(-300 + vertex_index));
+                        AppendUInt16(data, 0);
+                }
+                for (int normal_index = 0; normal_index < 3; ++normal_index){
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 1));
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 2));
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 3));
+                        AppendUInt16(data, 0);
+                }
+                return data;
+        }
+
+        std::vector<VGears::uint8> EncodeLzs(
+            const std::vector<VGears::uint8>& data
+        ){
+                std::vector<VGears::uint8> payload;
+                size_t data_offset = 0;
+                while (data_offset < data.size()){
+                        const size_t control_offset = payload.size();
+                        payload.push_back(0);
+                        VGears::uint8 control = 0;
+                        for (unsigned int bit = 0; bit < 8 && data_offset < data.size(); ++bit){
+                                control |= static_cast<VGears::uint8>(1u << bit);
+                                payload.push_back(data[data_offset++]);
+                        }
+                        payload[control_offset] = control;
+                }
+
+                std::vector<VGears::uint8> compressed;
+                AppendUInt32(compressed, static_cast<VGears::uint32>(payload.size()));
+                compressed.insert(compressed.end(), payload.begin(), payload.end());
+                return compressed;
+        }
+
+        std::vector<VGears::uint8> CreateMapFixture(
+            bool truncated_mesh = false, bool invalid_vertex = false
+        ){
+                std::vector<VGears::uint8> data(WORLD_MAP_BLOCK_SIZE, 0);
+                size_t write_offset = sizeof(VGears::uint32) * 16;
+                for (size_t mesh_index = 0; mesh_index < 16; ++mesh_index){
+                        const std::vector<VGears::uint8> mesh_data = mesh_index == 0
+                            ? CreateMeshData(truncated_mesh, invalid_vertex)
+                            : std::vector<VGears::uint8>{0, 0, 0, 0};
+                        const std::vector<VGears::uint8> compressed = EncodeLzs(mesh_data);
+                        write_offset = (write_offset + 3) & ~static_cast<size_t>(3);
+                        WriteUInt32(data, mesh_index * sizeof(VGears::uint32),
+                            static_cast<VGears::uint32>(write_offset));
+                        std::copy(compressed.begin(), compressed.end(), data.begin() + write_offset);
+                        write_offset += compressed.size();
+                }
+                return data;
+        }
+
+        void ImportFixture(std::vector<VGears::uint8>& data){
+                VGears::WorldmapFileSerializer serializer;
+                TestWorldmapFile file;
+                Ogre::DataStreamPtr stream(OGRE_NEW Ogre::MemoryDataStream(
+                    data.data(), data.size(), false, true
+                ));
+                serializer.ImportWorldmapFile(stream, file);
+        }
+
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerReadsPackedTriangleFields){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        VGears::WorldmapFileSerializer serializer;
+        TestWorldmapFile file;
+        Ogre::DataStreamPtr stream(OGRE_NEW Ogre::MemoryDataStream(
+            data.data(), data.size(), false, true
+        ));
+
+        serializer.ImportWorldmapFile(stream, file);
+
+        BOOST_REQUIRE_EQUAL(serializer.blocks.size(), 1u);
+        BOOST_REQUIRE_EQUAL(serializer.blocks[0].meshes.size(), 16u);
+        const VGears::WorldmapFileSerializer::BlockPart& part
+            = serializer.blocks[0].meshes[0];
+        BOOST_CHECK_EQUAL(part.header.triengle_count, 1);
+        BOOST_CHECK_EQUAL(part.header.vertex_count, 3);
+        BOOST_REQUIRE_EQUAL(part.triangles.size(), 1u);
+        const VGears::WorldmapFileSerializer::BlockTriangle& triangle
+            = part.triangles[0];
+        BOOST_CHECK_EQUAL(triangle.walkability_info_, 5);
+        BOOST_CHECK_EQUAL(triangle.unknown, 5);
+        BOOST_CHECK_EQUAL(triangle.vertex_0_u, 10);
+        BOOST_CHECK_EQUAL(triangle.vertex_0_v, 20);
+        BOOST_CHECK_EQUAL(triangle.vertex_1_u, 30);
+        BOOST_CHECK_EQUAL(triangle.vertex_1_v, 40);
+        BOOST_CHECK_EQUAL(triangle.vertex_2_u, 50);
+        BOOST_CHECK_EQUAL(triangle.vertex_2_v, 60);
+        BOOST_CHECK_EQUAL(triangle.TextureInfo, 0x155);
+        BOOST_CHECK_EQUAL(triangle.Location, 0x55);
+        BOOST_REQUIRE_EQUAL(part.vertices.size(), 3u);
+        BOOST_CHECK_EQUAL(part.vertices[0].x, -100);
+        BOOST_CHECK_EQUAL(part.vertices[0].y, 200);
+        BOOST_CHECK_EQUAL(part.vertices[0].z, -300);
+        BOOST_REQUIRE_EQUAL(part.normals.size(), 3u);
+        BOOST_CHECK_EQUAL(part.normals[0].x, 1);
+        BOOST_CHECK_EQUAL(part.normals[0].y, 2);
+        BOOST_CHECK_EQUAL(part.normals[0].z, 3);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsPartialBlocks){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        data.pop_back();
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsInvalidMeshOffsets){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        WriteUInt32(data, 0, static_cast<VGears::uint32>(WORLD_MAP_BLOCK_SIZE - 2));
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsMeshesOutsideBlock){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        const size_t mesh_offset = WORLD_MAP_BLOCK_SIZE - sizeof(VGears::uint32);
+        WriteUInt32(data, 0, static_cast<VGears::uint32>(mesh_offset));
+        WriteUInt32(data, mesh_offset, 1);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsTruncatedLzsTokens){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        const size_t mesh_offset = ReadUInt32(data, 0);
+        WriteUInt32(data, mesh_offset, 2);
+        data[mesh_offset + sizeof(VGears::uint32)] = 0;
+        data[mesh_offset + sizeof(VGears::uint32) + 1] = 0x10;
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsTruncatedMeshes){
+        std::vector<VGears::uint8> data = CreateMapFixture(true);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsInvalidVertexIndices){
+        std::vector<VGears::uint8> data = CreateMapFixture(false, true);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
 }