diff --git a/README.md b/README.md index dffded4e..a8f640c8 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,14 @@ Integrating Sfall goodies is the top priority. Quality of life updates are OK to For current sfall compatibility status and the remaining work needed to close gaps, see [SFALL_COMPATIBILITY.md](SFALL_COMPATIBILITY.md). +## For modders + +* Robust (though not 100%) Sfall opcode and hook support +* .ogg/.wav support for map music +* .png support for static assets (must be 8bit indexed) +* .zip support for .dat archives +* Working BIS mapper + ## License The source code is this repository is available under the [Sustainable Use License](LICENSE.md). diff --git a/files/ce.dat/art/intrface/INVBOX2.FRM b/files/ce.dat/art/intrface/INVBOX2.FRM deleted file mode 100644 index 936b3e9e..00000000 Binary files a/files/ce.dat/art/intrface/INVBOX2.FRM and /dev/null differ diff --git a/files/ce.dat/art/intrface/LOOT2.frm b/files/ce.dat/art/intrface/LOOT2.frm deleted file mode 100644 index 5b3eb194..00000000 Binary files a/files/ce.dat/art/intrface/LOOT2.frm and /dev/null differ diff --git a/files/ce.dat/art/intrface/invbox2.png b/files/ce.dat/art/intrface/invbox2.png new file mode 100644 index 00000000..80d77f53 Binary files /dev/null and b/files/ce.dat/art/intrface/invbox2.png differ diff --git a/files/ce.dat/art/intrface/loot2.png b/files/ce.dat/art/intrface/loot2.png new file mode 100644 index 00000000..0b5bee69 Binary files /dev/null and b/files/ce.dat/art/intrface/loot2.png differ diff --git a/src/art.cc b/src/art.cc index 8552f955..3235b2c0 100644 --- a/src/art.cc +++ b/src/art.cc @@ -1,6 +1,9 @@ #include "art.h" +#include + #include +#include #include #include #include @@ -134,6 +137,7 @@ static int* gArtCritterFidShoudRunData; static std::unordered_map> gNamedArtCache; constexpr int kNamedCacheMaxBytes = 32 * 1024 * 1024; // 32MB soft limit +constexpr size_t kMaxNamedPngPixels = 16 * 1024 * 1024; static unsigned int gNamedArtCacheMruCounter = 0; static int gNamedArtCacheCurrentBytes = 0; @@ -1116,7 +1120,7 @@ static int artReadFrameData(unsigned char* data, File* stream, int count, int* p // 0x419E1C static int artReadHeader(Art* art, File* stream) { - if (fileReadInt32(stream, &(art->field_0)) == -1) return -1; + if (fileReadInt32(stream, &(art->version)) == -1) return -1; if (fileReadInt16(stream, &(art->framesPerSecond)) == -1) return -1; if (fileReadInt16(stream, &(art->actionFrame)) == -1) return -1; if (fileReadInt16(stream, &(art->frameCount)) == -1) return -1; @@ -1133,13 +1137,166 @@ static int artReadHeader(Art* art, File* stream) return 0; } -// NOTE: Original function was slightly different, but never used. Basically -// it's a memory allocating variant of `artRead` (which reads data into given -// buffer). This function is useful to load custom `frm` files since `Art` now -// needs more memory then it's on-disk size (due to memory padding). -// -// 0x419EC0 -Art* artLoad(const char* path) +static bool artPathHasExtension(const char* path, const char* extension) +{ + size_t pathLength = strlen(path); + size_t extensionLength = strlen(extension); + if (pathLength < extensionLength) { + return false; + } + + return compat_stricmp(path + pathLength - extensionLength, extension) == 0; +} + +static bool artReadFile(const char* path, std::vector& data) +{ + int size = 0; + if (dbGetFileSize(path, &size) != 0 || size <= 0) { + return false; + } + + data.resize(size); + return dbGetFileContents(path, data.data()) == 0; +} + +static bool artUnpackIndexedPngPixels(const std::vector& indexedData, unsigned width, unsigned height, unsigned bitdepth, unsigned char* output) +{ + if (bitdepth != 8) { + return false; + } + + size_t pixelCount = static_cast(width) * static_cast(height); + if (indexedData.size() < pixelCount) { + return false; + } + + memcpy(output, indexedData.data(), pixelCount); + return true; +} + +static bool artValidateIndexedPngHeader(const char* path, unsigned width, unsigned height, const LodePNGColorMode& color) +{ + if (color.colortype != LCT_PALETTE) { + debugPrint("ART: PNG is not palette-indexed: %s\n", path); + return false; + } + + if (color.bitdepth != 8) { + debugPrint("ART: indexed PNG bit depth must be 8: %s\n", path); + return false; + } + + if (width == 0 || height == 0 || width > SHRT_MAX || height > SHRT_MAX) { + debugPrint("ART: invalid indexed PNG dimensions for %s: %ux%u\n", path, width, height); + return false; + } + + size_t pixelCount = static_cast(width) * static_cast(height); + if (pixelCount > kMaxNamedPngPixels || pixelCount > INT_MAX) { + debugPrint("ART: indexed PNG is too large: %s\n", path); + return false; + } + + return true; +} + +static bool artIndexedPngHasSupportedTransparency(const LodePNGColorMode& color) +{ + if (!lodepng_has_palette_alpha(&color)) { + return true; + } + + for (size_t index = 0; index < color.palettesize; index++) { + unsigned char alpha = color.palette[index * 4 + 3]; + if (index == 0) { + // palette index 0 is allowed to be either fully transparent or fully opaque + if (alpha != 0 && alpha != 255) { + return false; + } + } else if (alpha != 255) { + return false; + } + } + + return true; +} + +static Art* artLoadIndexedPng(const char* path) +{ + std::vector encoded; + if (!artReadFile(path, encoded)) { + return nullptr; + } + + lodepng::State state; + state.decoder.color_convert = 0; + + unsigned width = 0; + unsigned height = 0; + unsigned error = lodepng_inspect(&width, &height, &state, encoded.data(), encoded.size()); + if (error != 0) { + debugPrint("ART: failed to inspect indexed PNG %s: %s\n", path, lodepng_error_text(error)); + return nullptr; + } + + if (!artValidateIndexedPngHeader(path, width, height, state.info_png.color)) { + return nullptr; + } + + std::vector indexedData; + error = lodepng::decode(indexedData, width, height, state, encoded); + if (error != 0) { + debugPrint("ART: failed to decode indexed PNG %s: %s\n", path, lodepng_error_text(error)); + return nullptr; + } + + if (!artIndexedPngHasSupportedTransparency(state.info_png.color)) { + debugPrint("ART: indexed PNG transparency is unsupported, reserve palette index 0 instead: %s\n", path); + return nullptr; + } + + size_t pixelCount = static_cast(width) * static_cast(height); + + Art header = {}; + header.version = 4; + header.framesPerSecond = 10; + header.actionFrame = 0; + header.frameCount = 1; + header.dataSize = sizeof(ArtFrame) + static_cast(pixelCount); + + int currentPadding = paddingForSize(sizeof(Art)); + for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) { + header.dataOffsets[rotation] = 0; + header.padding[rotation] = currentPadding; + } + + int dataSize = artGetDataSize(&header); + unsigned char* data = reinterpret_cast(internal_malloc(dataSize)); + if (data == nullptr) { + return nullptr; + } + + memset(data, 0, dataSize); + Art* art = reinterpret_cast(data); + *art = header; + + ArtFrame* frame = reinterpret_cast(data + sizeof(Art) + art->padding[0]); + frame->width = static_cast(width); + frame->height = static_cast(height); + frame->size = static_cast(pixelCount); + frame->x = 0; + frame->y = 0; + + if (!artUnpackIndexedPngPixels(indexedData, width, height, state.info_png.color.bitdepth, reinterpret_cast(frame) + sizeof(ArtFrame))) { + debugPrint("ART: failed to read indexed PNG pixels: %s\n", path); + internal_free(data); + return nullptr; + } + + return art; +} + +static Art* artLoadFrm(const char* path) { File* stream = fileOpen(path, "rb"); if (stream == nullptr) { @@ -1167,6 +1324,30 @@ Art* artLoad(const char* path) return reinterpret_cast(data); } +// NOTE: Original function was slightly different, but never used. Basically +// it's a memory allocating variant of `artRead` (which reads data into given +// buffer). This function is useful to load custom `frm` files since `Art` now +// needs more memory then it's on-disk size (due to memory padding). +// +// 0x419EC0 +Art* artLoad(const char* path) +{ + if (path == nullptr) { + return nullptr; + } + + if (artPathHasExtension(path, ".png")) { + return artLoadIndexedPng(path); + } + + Art* art = artLoadFrm(path); + if (art != nullptr) { + return art; + } + + return nullptr; +} + static Art* artLoadLocalized(const char* path) { const char* localizedPath; @@ -1239,7 +1420,7 @@ int artWriteFrameData(unsigned char* data, File* stream, int count) // 0x41A138 int artWriteHeader(Art* art, File* stream) { - if (fileWriteInt32(stream, art->field_0) == -1) return -1; + if (fileWriteInt32(stream, art->version) == -1) return -1; if (fileWriteInt16(stream, art->framesPerSecond) == -1) return -1; if (fileWriteInt16(stream, art->actionFrame) == -1) return -1; if (fileWriteInt16(stream, art->frameCount) == -1) return -1; diff --git a/src/art.h b/src/art.h index 9a40cc6d..a01fcd7f 100644 --- a/src/art.h +++ b/src/art.h @@ -69,7 +69,7 @@ typedef enum Background { } Background; typedef struct Art { - int field_0; + int version; short framesPerSecond; short actionFrame; short frameCount; diff --git a/src/inventory.cc b/src/inventory.cc index 4d918412..604cae87 100644 --- a/src/inventory.cc +++ b/src/inventory.cc @@ -779,9 +779,9 @@ static void inventoryLootApplyLayout(int columns) static void inventoryNormalLayoutUpdate() { - int columns = inventoryChooseColumns(inventoryFrmImage, INVENTORY_WINDOW_WIDTH + INVENTORY_SLOT_WIDTH, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.frm"); + int columns = inventoryChooseColumns(inventoryFrmImage, INVENTORY_WINDOW_WIDTH + INVENTORY_SLOT_WIDTH, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.png"); if (columns == 1) { - inventoryBackgroundLoad(inventoryFrmImage, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.frm", 1); + inventoryBackgroundLoad(inventoryFrmImage, INVENTORY_NORMAL_BACKGROUND_FRM_ID, "invbox2.png", 1); } inventoryNormalApplyLayout(columns); @@ -789,9 +789,9 @@ static void inventoryNormalLayoutUpdate() static void inventoryLootLayoutUpdate() { - int columns = inventoryChooseColumns(inventoryLootFrmImage, INVENTORY_LOOT_WINDOW_WIDTH_EXPANDED, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.frm"); + int columns = inventoryChooseColumns(inventoryLootFrmImage, INVENTORY_LOOT_WINDOW_WIDTH_EXPANDED, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.png"); if (columns == 1) { - inventoryBackgroundLoad(inventoryLootFrmImage, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.frm", 1); + inventoryBackgroundLoad(inventoryLootFrmImage, INVENTORY_LOOT_BACKGROUND_FRM_ID, "loot2.png", 1); } inventoryLootApplyLayout(columns);