From 7474d3f45e9fc42bedf164f7dd6b4e787aaafd77 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 5 Dec 2021 18:05:23 +0000 Subject: [PATCH 1/2] wip - reimport cam images (no fg1 yet) --- Source/Tools/relive_api/Base64.cpp | 100 ++++++++++++ Source/Tools/relive_api/Base64.hpp | 10 ++ Source/Tools/relive_api/CMakeLists.txt | 2 + Source/Tools/relive_api/CamConverter.cpp | 130 +++++++++++----- Source/Tools/relive_api/CamConverter.hpp | 4 +- Source/Tools/relive_api/JsonModelTypes.cpp | 55 +------ Source/Tools/relive_api/JsonModelTypes.hpp | 6 + Source/Tools/relive_api/JsonReadUtils.hpp | 11 ++ Source/Tools/relive_api/JsonReaderBase.cpp | 8 +- Source/Tools/relive_api/JsonWriterBase.cpp | 4 - Source/Tools/relive_api/JsonWriterBase.hpp | 5 + Source/Tools/relive_api/LvlReaderWriter.hpp | 15 ++ Source/Tools/relive_api/relive_api.cpp | 147 +++++++++++++++++- Source/Tools/relive_api/relive_api.hpp | 2 +- .../Tools/relive_api_test/relive_api_test.cpp | 8 +- 15 files changed, 391 insertions(+), 116 deletions(-) create mode 100644 Source/Tools/relive_api/Base64.cpp create mode 100644 Source/Tools/relive_api/Base64.hpp diff --git a/Source/Tools/relive_api/Base64.cpp b/Source/Tools/relive_api/Base64.cpp new file mode 100644 index 000000000..f20bde712 --- /dev/null +++ b/Source/Tools/relive_api/Base64.cpp @@ -0,0 +1,100 @@ +#include "Base64.hpp" + +namespace ReliveAPI { +static const unsigned char base64_table[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static std::string base64_encode(const u8* src, size_t len) +{ + unsigned char *out, *pos; + const unsigned char *end, *in; + + size_t olen = 4 * ((len + 2) / 3); /* 3-byte blocks to 4-byte */ + + if (olen < len) + { + return std::string(); /* integer overflow */ + } + + std::string outStr; + outStr.resize(olen); + out = (unsigned char*) &outStr[0]; + + end = src + len; + in = src; + pos = out; + while (end - in >= 3) + { + *pos++ = base64_table[in[0] >> 2]; + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; + *pos++ = base64_table[in[2] & 0x3f]; + in += 3; + } + + if (end - in) + { + *pos++ = base64_table[in[0] >> 2]; + if (end - in == 1) + { + *pos++ = base64_table[(in[0] & 0x03) << 4]; + *pos++ = '='; + } + else + { + *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; + *pos++ = base64_table[(in[1] & 0x0f) << 2]; + } + *pos++ = '='; + } + + return outStr; +} + +static const int B64index[256] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 63, 62, 62, 63, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, + 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51}; + +static std::vector b64decode(const u8* data, const size_t len) +{ + const u8* p = data; + int pad = len > 0 && (len % 4 || p[len - 1] == '='); + const size_t calcLen = ((len + 3) / 4 - pad) * 4; + std::vector str(calcLen / 4 * 3 + pad, '\0'); + + for (size_t i = 0, j = 0; i < calcLen; i += 4) + { + int n = B64index[p[i]] << 18 | B64index[p[i + 1]] << 12 | B64index[p[i + 2]] << 6 | B64index[p[i + 3]]; + str[j++] = (n >> 16) & 0xFF; + str[j++] = (n >> 8) & 0xFF; + str[j++] = n & 0xFF; + } + + if (pad) + { + int n = B64index[p[calcLen]] << 18 | B64index[p[calcLen + 1]] << 12; + str[str.size() - 1] = (n >> 16) & 0xFF; + + if (len > calcLen + 2 && p[calcLen + 2] != '=') + { + n |= B64index[p[calcLen + 2]] << 6; + str.push_back(n >> 8 & 0xFF); + } + } + + return str; +} + +std::string ToBase64(const std::vector& vec) +{ + return base64_encode(vec.data(), vec.size()); +} + +std::vector FromBase64(const std::string& vec) +{ + return b64decode(reinterpret_cast(vec.data()), vec.length()); +} +} // namespace ReliveAPI diff --git a/Source/Tools/relive_api/Base64.hpp b/Source/Tools/relive_api/Base64.hpp new file mode 100644 index 000000000..c7620ea10 --- /dev/null +++ b/Source/Tools/relive_api/Base64.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "../../AliveLibCommon/Types.hpp" +#include +#include + +namespace ReliveAPI { +std::string ToBase64(const std::vector& vec); +std::vector FromBase64(const std::string& vec); +} // namespace ReliveAPI diff --git a/Source/Tools/relive_api/CMakeLists.txt b/Source/Tools/relive_api/CMakeLists.txt index 9cb5b8fff..404b35eda 100644 --- a/Source/Tools/relive_api/CMakeLists.txt +++ b/Source/Tools/relive_api/CMakeLists.txt @@ -70,6 +70,8 @@ export(TARGETS AliveLibCommon_reliveapi FILE AliveLibCommon_reliveapi.cmake) set_property(TARGET AliveLibCommon_reliveapi PROPERTY FOLDER "ReliveAPI") set(relive_api_src + Base64.hpp + Base64.cpp CamConverter.hpp CamConverter.cpp JsonReadUtils.hpp diff --git a/Source/Tools/relive_api/CamConverter.cpp b/Source/Tools/relive_api/CamConverter.cpp index 0aed14d78..37aa38a3a 100644 --- a/Source/Tools/relive_api/CamConverter.cpp +++ b/Source/Tools/relive_api/CamConverter.cpp @@ -53,19 +53,13 @@ static void RGB565ToPngBuffer(const u16* camBuffer, std::vector& outPngData) { std::cout << "encoder error " << error << ": " << lodepng_error_text(error) << std::endl; } - /* - else - { - lodepng::save_file(buffer, pFileName); - } - */ } struct FG1Buffers { - void SetPixel(u32 layer, u32 x, u32 y, u16 pixel) + void MergePixel(u32 layer, u32 x, u32 y, u16 pixel) { - mFg1[layer][y][x] = pixel; + mFg1[layer][y][x] |= pixel; } u16 mFg1[4][240][640]; }; @@ -93,7 +87,7 @@ class ApiFG1Reader final : public BaseFG1Reader | (((pixel >> 10) & 31) << 0); } - void BltRect(u32 xpos, u32 ypos, u32 width, u32 height, u32 layer, const u16* pSrcPixels, const u32* pBitMask) + void BltRectMerged(u32 xpos, u32 ypos, u32 width, u32 height, u32 layer, const u16* pSrcPixels, const u32* pBitMask) { mUsedLayers[layer] = true; @@ -126,7 +120,7 @@ class ApiFG1Reader final : public BaseFG1Reader } } - mFg1Buffers->SetPixel(layer, x, y, pixelVal); + mFg1Buffers->MergePixel(layer, x, y, pixelVal); } } } @@ -144,7 +138,7 @@ class ApiFG1Reader final : public BaseFG1Reader { pBitMap = reinterpret_cast((&rChunk) + 1); } - BltRect(rChunk.field_4_xpos_or_compressed_size, + BltRectMerged(rChunk.field_4_xpos_or_compressed_size, rChunk.field_6_ypos, rChunk.field_8_width + rChunk.field_4_xpos_or_compressed_size, rChunk.field_A_height + rChunk.field_6_ypos, @@ -155,7 +149,7 @@ class ApiFG1Reader final : public BaseFG1Reader void OnFullChunk(const Fg1Chunk& rChunk) override { - BltRect(rChunk.field_4_xpos_or_compressed_size, + BltRectMerged(rChunk.field_4_xpos_or_compressed_size, rChunk.field_6_ypos, rChunk.field_8_width + rChunk.field_4_xpos_or_compressed_size, rChunk.field_A_height + rChunk.field_6_ypos, @@ -177,7 +171,7 @@ class ApiFG1Reader final : public BaseFG1Reader delete ptr; } - void SaveLayers(CameraImageAndLayers& outData) + void LayersToPng(CameraImageAndLayers& outData) { for (u32 i = 0; i < 4; i++) { @@ -189,6 +183,29 @@ class ApiFG1Reader final : public BaseFG1Reader } } + static void DebugSave(const CameraImageAndLayers& outData, u32 id) + { + if (!outData.mBackgroundLayer.empty()) + { + lodepng::save_file(outData.mBackgroundLayer, "bg_" + std::to_string(id) + ".png"); + } + + if (!outData.mForegroundLayer.empty()) + { + lodepng::save_file(outData.mForegroundLayer, "fg_" + std::to_string(id) + ".png"); + } + + if (!outData.mBackgroundWellLayer.empty()) + { + lodepng::save_file(outData.mBackgroundWellLayer, "bg_well_" + std::to_string(id) + ".png"); + } + + if (!outData.mForegroundWellLayer.empty()) + { + lodepng::save_file(outData.mForegroundWellLayer, "fg_well_" + std::to_string(id) + ".png"); + } + } + private: std::vector& BufferForLayer(CameraImageAndLayers& outData, u32 layer) { @@ -239,7 +256,25 @@ static void AppendCamSegment(s32 x, s32 y, s32 width, s32 height, u16* pDst, con } } -CamConverterAO::CamConverterAO(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData) +static void MergeFG1BlocksAndConvertToPng(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData, BaseFG1Reader::FG1Format format) +{ + // For some crazy reason there can be multiple FG1 blocks, here we squash them down into a single + // image for each "layer". + ApiFG1Reader reader(format); + for (u32 i = 0; i < camFile.ChunkCount(); i++) + { + if (camFile.ChunkAt(i).Header().field_8_type == ResourceManager::Resource_FG1) + { + reader.Iterate(reinterpret_cast(camFile.ChunkAt(i).Data().data())); + } + } + + reader.LayersToPng(outData); + + //ApiFG1Reader::DebugSave(outData, camFile.ChunkByType(ResourceManager::Resource_Bits)->Id()); +} + +CamConverterAO::CamConverterAO(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData, bool processFG1) { std::optional bitsRes = camFile.ChunkByType(ResourceManager::Resource_Bits); if (bitsRes) @@ -257,15 +292,26 @@ CamConverterAO::CamConverterAO(const ChunkedLvlFile& camFile, CameraImageAndLaye pIter += (slice_len / sizeof(s16)); } RGB565ToPngBuffer(camBuffer, outData.mCameraImage); + if (processFG1) + { + MergeFG1BlocksAndConvertToPng(camFile, outData, BaseFG1Reader::FG1Format::AO); + } } - std::optional fg1Res = camFile.ChunkByType(ResourceManager::Resource_FG1); - if (fg1Res) +} + +static bool AEcamIsAOCam(const LvlFileChunk& bitsRes) +{ + const u16* pIter = reinterpret_cast(bitsRes.Data().data()); + for (s16 xpos = 0; xpos < 640; xpos += 16) { - ApiFG1Reader reader(BaseFG1Reader::FG1Format::AO); - reader.Iterate(reinterpret_cast(fg1Res->Data().data())); - reader.SaveLayers(outData); + const u16 stripSize = *pIter; + if (stripSize != (16 * 240 * sizeof(u16))) + { + return false; + } } + return true; } CamConverterAE::CamConverterAE(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData) @@ -273,33 +319,33 @@ CamConverterAE::CamConverterAE(const ChunkedLvlFile& camFile, CameraImageAndLaye std::optional bitsRes = camFile.ChunkByType(ResourceManager::Resource_Bits); if (bitsRes) { - u16 camBuffer[640 * 240] = {}; - u8 vlcBuffer[0x7E00] = {}; - CamDecompressor decompressor; - const u16* pIter = reinterpret_cast(bitsRes->Data().data()); - for (s16 xpos = 0; xpos < 640; xpos += 16) + if (AEcamIsAOCam(*bitsRes)) { - const u16 stripSize = *pIter; - pIter++; - - if (stripSize > 0) + CamConverterAO aoCam(camFile, outData, false); + } + else + { + u16 camBuffer[640 * 240] = {}; + u8 vlcBuffer[0x7E00] = {}; + CamDecompressor decompressor; + const u16* pIter = reinterpret_cast(bitsRes->Data().data()); + for (s16 xpos = 0; xpos < 640; xpos += 16) { - decompressor.vlc_decode(pIter, reinterpret_cast(vlcBuffer)); - decompressor.process_segment(reinterpret_cast(vlcBuffer), 0); - AppendCamSegment(xpos, 0, 16, 240, camBuffer, decompressor.mDecompressedStrip); - } + const u16 stripSize = *pIter; + pIter++; - pIter += (stripSize / sizeof(u16)); - } - RGB565ToPngBuffer(camBuffer, outData.mCameraImage); - } + if (stripSize > 0) + { + decompressor.vlc_decode(pIter, reinterpret_cast(vlcBuffer)); + decompressor.process_segment(reinterpret_cast(vlcBuffer), 0); + AppendCamSegment(xpos, 0, 16, 240, camBuffer, decompressor.mDecompressedStrip); + } - std::optional fg1Res = camFile.ChunkByType(ResourceManager::Resource_FG1); - if (fg1Res) - { - ApiFG1Reader reader(BaseFG1Reader::FG1Format::AE); - reader.Iterate(reinterpret_cast(fg1Res->Data().data())); - reader.SaveLayers(outData); + pIter += (stripSize / sizeof(u16)); + } + RGB565ToPngBuffer(camBuffer, outData.mCameraImage); + MergeFG1BlocksAndConvertToPng(camFile, outData, BaseFG1Reader::FG1Format::AE); + } } } } // namespace ReliveAPI diff --git a/Source/Tools/relive_api/CamConverter.hpp b/Source/Tools/relive_api/CamConverter.hpp index 770088301..557525d0d 100644 --- a/Source/Tools/relive_api/CamConverter.hpp +++ b/Source/Tools/relive_api/CamConverter.hpp @@ -7,7 +7,7 @@ namespace ReliveAPI { class ChunkedLvlFile; -class CameraImageAndLayers +class CameraImageAndLayers final { public: std::vector mCameraImage; @@ -20,7 +20,7 @@ class CameraImageAndLayers class CamConverterAO final { public: - CamConverterAO(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData); + CamConverterAO(const ChunkedLvlFile& camFile, CameraImageAndLayers& outData, bool processFG1 = true); }; class CamConverterAE final diff --git a/Source/Tools/relive_api/JsonModelTypes.cpp b/Source/Tools/relive_api/JsonModelTypes.cpp index 6308f6a45..01fb444eb 100644 --- a/Source/Tools/relive_api/JsonModelTypes.cpp +++ b/Source/Tools/relive_api/JsonModelTypes.cpp @@ -1,63 +1,10 @@ #include "JsonModelTypes.hpp" #include #include "CamConverter.hpp" +#include "Base64.hpp" namespace ReliveAPI { -static const unsigned char base64_table[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -static std::string base64_encode(const u8* src, size_t len) -{ - unsigned char *out, *pos; - const unsigned char *end, *in; - - size_t olen = 4 * ((len + 2) / 3); /* 3-byte blocks to 4-byte */ - - if (olen < len) - { - return std::string(); /* integer overflow */ - } - - std::string outStr; - outStr.resize(olen); - out = (unsigned char*) &outStr[0]; - - end = src + len; - in = src; - pos = out; - while (end - in >= 3) - { - *pos++ = base64_table[in[0] >> 2]; - *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; - *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; - *pos++ = base64_table[in[2] & 0x3f]; - in += 3; - } - - if (end - in) - { - *pos++ = base64_table[in[0] >> 2]; - if (end - in == 1) - { - *pos++ = base64_table[(in[0] & 0x03) << 4]; - *pos++ = '='; - } - else - { - *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; - *pos++ = base64_table[(in[1] & 0x0f) << 2]; - } - *pos++ = '='; - } - - return outStr; -} - -static std::string ToBase64(const std::vector& vec) -{ - return base64_encode(vec.data(), vec.size()); -} - [[nodiscard]] jsonxx::Object CameraObject::ToJsonObject(jsonxx::Array mapObjectsArray, const CameraImageAndLayers& cameraImageAndLayers) const { jsonxx::Object obj; diff --git a/Source/Tools/relive_api/JsonModelTypes.hpp b/Source/Tools/relive_api/JsonModelTypes.hpp index 382e76455..37b2f1200 100644 --- a/Source/Tools/relive_api/JsonModelTypes.hpp +++ b/Source/Tools/relive_api/JsonModelTypes.hpp @@ -58,6 +58,12 @@ struct CameraNameAndTlvBlob final std::string mName; std::vector> mTlvBlobs; + std::string mCameraImage; + std::string mForegroundLayer; + std::string mBackgroundLayer; + std::string mForegroundWellLayer; + std::string mBackgroundWellLayer; + [[nodiscard]] std::size_t TotalTlvSize() const; }; diff --git a/Source/Tools/relive_api/JsonReadUtils.hpp b/Source/Tools/relive_api/JsonReadUtils.hpp index 7dcea9dd4..832482943 100644 --- a/Source/Tools/relive_api/JsonReadUtils.hpp +++ b/Source/Tools/relive_api/JsonReadUtils.hpp @@ -47,4 +47,15 @@ namespace ReliveAPI { return o.get(key); } + +[[nodiscard]] inline std::string ReadOptionalString(const jsonxx::Object& o, const std::string& key) +{ + if (!o.has(key)) + { + return ""; + } + + return o.get(key); +} + } // namespace ReliveAPI diff --git a/Source/Tools/relive_api/JsonReaderBase.cpp b/Source/Tools/relive_api/JsonReaderBase.cpp index db18b919f..6c1bb4b34 100644 --- a/Source/Tools/relive_api/JsonReaderBase.cpp +++ b/Source/Tools/relive_api/JsonReaderBase.cpp @@ -78,6 +78,12 @@ std::pair, jsonxx::Object> JsonReaderBase::Loa cameraNameBlob.x = x; cameraNameBlob.y = y; + cameraNameBlob.mCameraImage = ReadOptionalString(camera, "image"); + cameraNameBlob.mForegroundLayer = ReadOptionalString(camera, "foreground_layer"); + cameraNameBlob.mForegroundWellLayer = ReadOptionalString(camera, "foreground_well_layer"); + cameraNameBlob.mBackgroundLayer = ReadOptionalString(camera, "background_layer"); + cameraNameBlob.mBackgroundWellLayer = ReadOptionalString(camera, "background_well_layer"); + const jsonxx::Array& mapObjectsArray = ReadArray(camera, "map_objects"); for (auto j = 0u; j < mapObjectsArray.values().size(); j++) { @@ -96,6 +102,6 @@ std::pair, jsonxx::Object> JsonReaderBase::Loa mapData.push_back(std::move(cameraNameBlob)); } - return {std::move(mapData), std::move(map)}; + return {std::move(mapData), map}; } } // namespace ReliveAPI diff --git a/Source/Tools/relive_api/JsonWriterBase.cpp b/Source/Tools/relive_api/JsonWriterBase.cpp index 64e481182..e3b9ae4d4 100644 --- a/Source/Tools/relive_api/JsonWriterBase.cpp +++ b/Source/Tools/relive_api/JsonWriterBase.cpp @@ -7,10 +7,6 @@ #include "CamConverter.hpp" namespace ReliveAPI { -inline s32 To1dIndex(s32 width, s32 x, s32 y) -{ - return x + (y * width); -} JsonWriterBase::~JsonWriterBase() = default; diff --git a/Source/Tools/relive_api/JsonWriterBase.hpp b/Source/Tools/relive_api/JsonWriterBase.hpp index 14d35238e..cf46567af 100644 --- a/Source/Tools/relive_api/JsonWriterBase.hpp +++ b/Source/Tools/relive_api/JsonWriterBase.hpp @@ -11,6 +11,11 @@ namespace AO { namespace ReliveAPI { +inline s32 To1dIndex(s32 width, s32 x, s32 y) +{ + return x + (y * width); +} + class LvlReader; class JsonWriterBase diff --git a/Source/Tools/relive_api/LvlReaderWriter.hpp b/Source/Tools/relive_api/LvlReaderWriter.hpp index 024a7f28b..24aaa3c23 100644 --- a/Source/Tools/relive_api/LvlReaderWriter.hpp +++ b/Source/Tools/relive_api/LvlReaderWriter.hpp @@ -72,6 +72,11 @@ class LvlFileChunk final class ChunkedLvlFile final { public: + ChunkedLvlFile() + { + + } + explicit ChunkedLvlFile(const std::vector& data) { Read(data); @@ -151,6 +156,16 @@ class ChunkedLvlFile final return std::move(s).GetBuffer(); } + const u32 ChunkCount() const + { + return static_cast(mChunks.size()); + } + + const LvlFileChunk& ChunkAt(u32 idx) const + { + return mChunks[idx]; + } + private: void Read(const std::vector& data) { diff --git a/Source/Tools/relive_api/relive_api.cpp b/Source/Tools/relive_api/relive_api.cpp index 772d1d440..1f7c8b92f 100644 --- a/Source/Tools/relive_api/relive_api.cpp +++ b/Source/Tools/relive_api/relive_api.cpp @@ -13,11 +13,13 @@ #include "JsonWriterAE.hpp" #include "JsonWriterAO.hpp" #include "JsonMapRootInfoReader.hpp" -#include #include "TypesCollectionBase.hpp" +#include "Base64.hpp" +#include #include #include #include +#include bool RunningAsInjectedDll() { @@ -404,8 +406,135 @@ static void WriteCollisionLine(ByteStream& s, const ::PathLine& line) s.Write(line.field_12_line_length); } +static u32 CamBitsIdFromName(const std::string& pCamName) +{ + if (pCamName.length() < 7) + { + // todo: throw + LOG_WARNING("Bad camera name, can't get resource id " << pCamName); + return 0; + } + // Given R1P20C15 returns 2015 + return 1 * (pCamName[7] - '0') + 10 * (pCamName[6] - '0') + 100 * (pCamName[4] - '0') + 1000 * (pCamName[3] - '0'); +} + +static u16 RGB888ToRGB565(const u8* rgb888Pixel) +{ + const u8 red = rgb888Pixel[0]; + const u8 green = rgb888Pixel[1]; + const u8 blue = rgb888Pixel[2]; + + const u16 b = (blue >> 3) & 0x1f; + const u16 g = ((green >> 2) & 0x3f) << 5; + const u16 r = ((red >> 3) & 0x1f) << 11; + + return (r | g | b); +} + +static void ImportCamerasAndFG1(std::vector& fileDataBuffer, LvlWriter& inputLvl, const std::vector& camerasAndMapObjects) +{ + // Rebuild cameras/FG1 and embedded resource blocks + for (const CameraNameAndTlvBlob& camIter : camerasAndMapObjects) + { + // Get camera ID from the name for the Bits chunk + if (!camIter.mName.empty()) + { + const u32 bitsId = CamBitsIdFromName(camIter.mName); + + // Load existing .CAM if possible so existing additional resource blocks don't get removed by + // re-creating the CAM from scratch. + ChunkedLvlFile camFile; + if (inputLvl.ReadFileInto(fileDataBuffer, (camIter.mName + ".CAM").c_str())) + { + camFile = ChunkedLvlFile(fileDataBuffer); + } + + unsigned width = 0; + unsigned height = 0; + std::vector rawPixels; + std::vector pngData = FromBase64(camIter.mCameraImage); + const auto error = lodepng::decode(rawPixels, width, height, pngData, LCT_RGBA, 8); + if (error) + { + // todo: throw + } + + if (width != 640 || height != 240) + { + // todo: throw + } + + // Arrage pixel data in strips and RGB888 -> RGB565 + struct CamStrip final + { + u16 mStripLen = 16 * 240 * sizeof(u16); + u16 mStrip[240][16] = {}; + }; + + struct CamImageStrips final + { + public: + void SetPixel(u32 x, u32 y, u16 pixel) + { + const u32 stripNum = x / 16; + const u32 xPixel = x % 16; + mStrips[stripNum].mStrip[y][xPixel] = pixel; + } + + std::vector ToVector() const + { + std::vector r; + const u32 totalLengthDataSize = (640 / 16) * sizeof(u16); + const u32 totalStripDataSize = (640 / 16) * (16 * 240) * sizeof(u16); + r.resize(totalLengthDataSize + totalStripDataSize); + + u16* pStream = reinterpret_cast(r.data()); + + u32 pos = 0; + for (u32 i = 0; i < 640 / 16; i++) + { + pStream[pos] = mStrips[i].mStripLen; + pos++; + + memcpy(pStream + pos, &mStrips[i].mStrip[0][0], mStrips[i].mStripLen); + pos += mStrips[i].mStripLen / sizeof(u16); + } + return r; + } + + private: + CamStrip mStrips[640 / 16]; + }; + + CamImageStrips bitsData; + for (u32 x = 0; x < 640; x++) + { + for (u32 y = 0; y < 240; y++) + { + const u32* pPixel32 = &reinterpret_cast(rawPixels.data())[To1dIndex(640, x, y)]; + bitsData.SetPixel(x, y, RGB888ToRGB565(reinterpret_cast(pPixel32))); + } + } + + LvlFileChunk bitsChunk(bitsId, ResourceManager::Resource_Bits, bitsData.ToVector()); + camFile.AddChunk(std::move(bitsChunk)); + + // TODO: Remove FG1 blocks + + // TODO: FG1 blocks use id BitsId << 8 + idx + + // TODO: Iterate in 32x16 blocks, creating full or partial blocks as needed + + // TODO: Add reconstructed single FG1 block + + // Add or update the CAM file + inputLvl.AddFile((camIter.mName + ".CAM").c_str(), camFile.Data()); + } + } +} + template -static void SaveBinaryPathToLvl(std::vector& fileDataBuffer, Game gameType, const std::string& jsonInputFile, const std::string& inputLvlFile, const std::string& outputLvlFile) +static void SaveBinaryPathToLvl(std::vector& fileDataBuffer, Game gameType, const std::string& jsonInputFile, const std::string& inputLvlFile, const std::string& outputLvlFile, bool skipCamsAndFG1) { JsonReaderType doc; auto [camerasAndMapObjects, collisionLines] = doc.Load(jsonInputFile); @@ -570,8 +699,10 @@ static void SaveBinaryPathToLvl(std::vector& fileDataBuffer, Game gameType, // Add or replace the original path BND in the lvl inputLvl.AddFile(doc.mRootInfo.mPathBnd.c_str(), std::move(pathBndFile).Data()); - // TODO: Rebuild cameras/FG1 and embedded resource blocks - + if (!skipCamsAndFG1) + { + ImportCamerasAndFG1(fileDataBuffer, inputLvl, camerasAndMapObjects); + } // Write out the updated lvl to disk if (!inputLvl.Save(fileDataBuffer, outputLvlFile.c_str())) @@ -580,7 +711,7 @@ static void SaveBinaryPathToLvl(std::vector& fileDataBuffer, Game gameType, } } -void ImportPathJsonToBinary(std::vector& fileDataBuffer, const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& /*lvlResourceSources*/) +void ImportPathJsonToBinary(std::vector& fileDataBuffer, const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& /*lvlResourceSources*/, bool skipCamerasAndFG1) { JsonMapRootInfoReader rootInfo; rootInfo.Read(jsonInputFile); @@ -592,18 +723,18 @@ void ImportPathJsonToBinary(std::vector& fileDataBuffer, const std::string& if (rootInfo.mMapRootInfo.mGame == "AO") { - SaveBinaryPathToLvl(fileDataBuffer, Game::AO, jsonInputFile, inputLvl, outputLvlFile); + SaveBinaryPathToLvl(fileDataBuffer, Game::AO, jsonInputFile, inputLvl, outputLvlFile, skipCamerasAndFG1); } else { - SaveBinaryPathToLvl(fileDataBuffer, Game::AE, jsonInputFile, inputLvl, outputLvlFile); + SaveBinaryPathToLvl(fileDataBuffer, Game::AE, jsonInputFile, inputLvl, outputLvlFile, skipCamerasAndFG1); } } void ImportPathJsonToBinary(const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& lvlResourceSources) { std::vector buffer; - ImportPathJsonToBinary(buffer, jsonInputFile, inputLvl, outputLvlFile, lvlResourceSources); + ImportPathJsonToBinary(buffer, jsonInputFile, inputLvl, outputLvlFile, lvlResourceSources, false); } [[nodiscard]] EnumeratePathsResult EnumeratePaths(std::vector& fileDataBuffer, const std::string& inputLvlFile) diff --git a/Source/Tools/relive_api/relive_api.hpp b/Source/Tools/relive_api/relive_api.hpp index 9ddd80f2e..0f437104b 100644 --- a/Source/Tools/relive_api/relive_api.hpp +++ b/Source/Tools/relive_api/relive_api.hpp @@ -26,7 +26,7 @@ API_EXPORT void ExportPathBinaryToJson(const std::string& jsonOutputFile, const API_EXPORT void UpgradePathJson(const std::string& jsonFile); -API_EXPORT void ImportPathJsonToBinary(std::vector& fileDataBuffer, const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& lvlResourceSources); +API_EXPORT void ImportPathJsonToBinary(std::vector& fileDataBuffer, const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& lvlResourceSources, bool skipCamerasAndFG1); API_EXPORT void ImportPathJsonToBinary(const std::string& jsonInputFile, const std::string& inputLvl, const std::string& outputLvlFile, const std::vector& lvlResourceSources); API_EXPORT [[nodiscard]] EnumeratePathsResult EnumeratePaths(std::vector& fileDataBuffer, const std::string& inputLvlFile); diff --git a/Source/Tools/relive_api_test/relive_api_test.cpp b/Source/Tools/relive_api_test/relive_api_test.cpp index 1f05a2532..b3f1efd7b 100644 --- a/Source/Tools/relive_api_test/relive_api_test.cpp +++ b/Source/Tools/relive_api_test/relive_api_test.cpp @@ -109,7 +109,7 @@ TEST(alive_api, ExportPathBinaryToJsonAO) TEST(alive_api, ImportPathJsonToBinaryAO) { - ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), "OutputAO.json", AOPath("R1.LVL"), "newAO.lvl", {}); + ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), "OutputAO.json", AOPath("R1.LVL"), "newAO.lvl", {}, true); const auto ogR1 = readFileIntoStaticFileBuffer(AOPath("R1.LVL")); ASSERT_NE(ogR1.size(), 0u); @@ -122,7 +122,7 @@ TEST(alive_api, ImportPathJsonToBinaryAO) TEST(alive_api, ImportPathJsonToBinaryAE) { - ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), "OutputAE.json", AEPath(kAETestLvl), "newAE.lvl", {}); + ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), "OutputAE.json", AEPath(kAETestLvl), "newAE.lvl", {}, true); const auto ogLVL = readFileIntoStaticFileBuffer(AEPath(kAETestLvl)); ASSERT_NE(ogLVL.size(), 0u); @@ -160,7 +160,7 @@ TEST(alive_api, ReSaveAllPathsAO) const std::string lvlName = concat("OutputAO_", lvl, '_', path, ".lvl"); LOG_INFO("Resave " << lvlName); - ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), jsonName, AOPath(lvl), lvlName, {}); + ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), jsonName, AOPath(lvl), lvlName, {}, true); const auto originalLvlBytes = readFileIntoStaticFileBuffer(AOPath(lvl)); ASSERT_NE(originalLvlBytes.size(), 0u); @@ -191,7 +191,7 @@ TEST(alive_api, ReSaveAllPathsAE) const std::string lvlName = concat("OutputAE_", lvl, '_', path, ".lvl"); LOG_INFO("Resave " << lvlName); - ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), jsonName, AEPath(lvl), lvlName, {}); + ReliveAPI::ImportPathJsonToBinary(getStaticFileBuffer(), jsonName, AEPath(lvl), lvlName, {}, true); const auto originalLvlBytes = readFileIntoStaticFileBuffer(AEPath(lvl)); ASSERT_NE(originalLvlBytes.size(), 0u); From 6c6b432c94a0abb0c64edffdf85bd8312abd48b8 Mon Sep 17 00:00:00 2001 From: Paul Date: Sun, 5 Dec 2021 18:07:52 +0000 Subject: [PATCH 2/2] fix missing export of FG1 when using AO cam image format in AE --- Source/Tools/relive_api/CamConverter.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Tools/relive_api/CamConverter.cpp b/Source/Tools/relive_api/CamConverter.cpp index 37aa38a3a..a787a5e74 100644 --- a/Source/Tools/relive_api/CamConverter.cpp +++ b/Source/Tools/relive_api/CamConverter.cpp @@ -322,6 +322,9 @@ CamConverterAE::CamConverterAE(const ChunkedLvlFile& camFile, CameraImageAndLaye if (AEcamIsAOCam(*bitsRes)) { CamConverterAO aoCam(camFile, outData, false); + + // While its image data is AO format the FG1 is still AE format + MergeFG1BlocksAndConvertToPng(camFile, outData, BaseFG1Reader::FG1Format::AE); } else {