libeuropa/io/yatd: Support YATF v2 (Jedi Starfighter)

Rather confusingly, it seems like they wanted to make this backwards compatible. In practice, due to the magic fourcc being written with a different endian, it's not really. (OK well the header is the same but you get my point)

Also, for some reason, they decided to I guess jumble format types around. Thanks guys.
This commit is contained in:
Lily Tsuru 2025-01-16 16:24:12 -05:00
parent ab496ed4aa
commit 69e9ecd35c
3 changed files with 136 additions and 39 deletions

View file

@ -17,21 +17,28 @@ namespace europa::structs {
struct [[gnu::packed]] YatfHeader { struct [[gnu::packed]] YatfHeader {
enum class TextureFormat : u8 { enum class TextureFormat : u8 {
kTextureFormat8Bpp = 0, kTextureFormatV1_8Bpp = 0,
kTextureFormatUnknown = 1, // possibly 16bpp? kTextureFormatV1_24Bpp = 2,
kTextureFormat24Bpp = 2, kTextureFormatV1_32Bpp = 3,
kTextureFormat32Bpp = 3
// V2/jsf uses these
kTextureFormatV2_8Bpp = 1,
kTextureFormatV2_24Bpp = 3,
kTextureFormatV2_32Bpp = 4,
kTextureFormatV2_4Bpp = 5
}; };
constexpr static auto ValidMagic = util::FourCC<"YATF", std::endian::big>(); // For some reason Jedi Starfighter YATFs use a different fourcc. ???
constexpr static auto ValidMagicSF = util::FourCC<"YATF", std::endian::big>();
constexpr static auto ValidMagicJSF = util::FourCC<"YATF", std::endian::little>();
u32 magic; u32 magic;
u16 unkThing; // always 0x1 u16 version; // 0x1 for starfighter, 0x2 for jsf
TextureFormat format; TextureFormat format;
u8 unkThing2; // flags? u8 unkThing2; // flags? some palbpp?
// Always zeroed. // Always zeroed.
u32 zero; u32 zero;
@ -40,7 +47,7 @@ namespace europa::structs {
u32 width; u32 width;
[[nodiscard]] constexpr bool IsValid() const { [[nodiscard]] constexpr bool IsValid() const {
return magic == ValidMagic; return magic == ValidMagicSF || magic == ValidMagicJSF;
} }
}; };

View file

@ -31,9 +31,16 @@ namespace europa::io::yatf {
surface.Resize(imageSize); surface.Resize(imageSize);
// FIXME: merge this code and make it better
// i.e: let's share de-palettization between the two.
// (or upgrade ImageSurface to support multiple formats, and allow conversion to a new one?
// that might preclude cleaning it up and making util::image namespace however)
auto v1 = [&]() {
using enum structs::YatfHeader::TextureFormat; using enum structs::YatfHeader::TextureFormat;
switch(header.format) { switch(header.format) {
case kTextureFormat8Bpp: { case kTextureFormatV1_8Bpp: {
util::Pixel palette[256] {}; util::Pixel palette[256] {};
util::UniqueArray<std::uint8_t> palettizedData(imageSize.Linear()); util::UniqueArray<std::uint8_t> palettizedData(imageSize.Linear());
@ -51,7 +58,7 @@ namespace europa::io::yatf {
} }
} break; } break;
case kTextureFormat24Bpp: { case kTextureFormatV1_24Bpp: {
util::UniqueArray<util::PixelRGB> rgbPixelData(imageSize.Linear()); util::UniqueArray<util::PixelRGB> rgbPixelData(imageSize.Linear());
stream.read(reinterpret_cast<char*>(&rgbPixelData[0]), imageSize.LinearWithStride<util::PixelRGB>()); stream.read(reinterpret_cast<char*>(&rgbPixelData[0]), imageSize.LinearWithStride<util::PixelRGB>());
auto* pDestBuffer = reinterpret_cast<util::Pixel*>(surface.GetBuffer()); auto* pDestBuffer = reinterpret_cast<util::Pixel*>(surface.GetBuffer());
@ -65,16 +72,96 @@ namespace europa::io::yatf {
} }
} break; } break;
case kTextureFormat32Bpp: case kTextureFormatV1_32Bpp:
// We can directly read data // We can directly read data
stream.read(reinterpret_cast<char*>(surface.GetBuffer()), imageSize.LinearWithStride<util::Pixel>()); stream.read(reinterpret_cast<char*>(surface.GetBuffer()), imageSize.LinearWithStride<util::Pixel>());
break; break;
case kTextureFormatUnknown:
default: default:
throw std::runtime_error(std::format("Unknown/unsupported texture format {:02x}!", (std::uint16_t)header.format)); throw std::runtime_error(std::format("Unknown/unsupported texture format {:02x}!", (std::uint16_t)header.format));
break; break;
} }
};
auto v2 = [&]() {
using enum structs::YatfHeader::TextureFormat;
switch(header.format) {
case kTextureFormatV2_4Bpp: {
util::Pixel palette[16] {};
util::UniqueArray<std::uint8_t> palettizedData(imageSize.Linear() / 2);
stream.read(reinterpret_cast<char*>(&palette[0]), sizeof(palette));
stream.read(reinterpret_cast<char*>(&palettizedData[0]), imageSize.Linear() / 2);
auto* pDestBuffer = reinterpret_cast<util::Pixel*>(surface.GetBuffer());
// can't really get a better loop to work, so i guess this has to do
for(std::size_t y = 0; y < (imageSize.width * imageSize.height) / 2; ++y) {
auto& pp = palettizedData[y];
for(std::size_t b = 0; b < 2; ++b) {
auto col = ((pp & (0x0F << (b * 4))) >> (b * 4));
(*pDestBuffer++) = palette[static_cast<std::size_t>(col)];
}
}
} break;
case kTextureFormatV2_8Bpp: {
util::Pixel palette[256] {};
util::UniqueArray<std::uint8_t> palettizedData(imageSize.Linear());
stream.read(reinterpret_cast<char*>(&palette[0]), sizeof(palette));
stream.read(reinterpret_cast<char*>(&palettizedData[0]), imageSize.Linear());
auto* pDestBuffer = reinterpret_cast<util::Pixel*>(surface.GetBuffer());
for(std::size_t y = 0; y < imageSize.height; ++y) {
for(std::size_t x = 0; x < imageSize.width; ++x) {
auto& pp = palettizedData[y * imageSize.width + x];
auto& dst = pDestBuffer[y * imageSize.width + x];
dst = palette[static_cast<std::size_t>(pp)];
}
}
} break;
case kTextureFormatV2_24Bpp: {
util::UniqueArray<util::PixelRGB> rgbPixelData(imageSize.Linear());
stream.read(reinterpret_cast<char*>(&rgbPixelData[0]), imageSize.LinearWithStride<util::PixelRGB>());
auto* pDestBuffer = reinterpret_cast<util::Pixel*>(surface.GetBuffer());
for(std::size_t y = 0; y < imageSize.height; ++y) {
for(std::size_t x = 0; x < imageSize.width; ++x) {
auto& pp = rgbPixelData[y * imageSize.width + x];
auto& dst = pDestBuffer[y * imageSize.width + x];
dst = util::Pixel::FromPixelRGB(pp);
}
}
} break;
case kTextureFormatV2_32Bpp:
// We can directly read data
stream.read(reinterpret_cast<char*>(surface.GetBuffer()), imageSize.LinearWithStride<util::Pixel>());
break;
default:
throw std::runtime_error(std::format("Unknown/unsupported texture format {:02x}!", (std::uint16_t)header.format));
break;
}
};
switch(header.version) {
case 1:
v1();
break;
case 2:
v2();
break;
default:
throw std::runtime_error(std::format("Unknown/unsupported YATF version {:02x}!", (std::uint16_t)header.version));
break;
}
return true; return true;
} }

View file

@ -40,6 +40,9 @@ int main(int argc, char** argv) {
europa::structs::YatfHeader yatfHeader; europa::structs::YatfHeader yatfHeader;
eutil::ImageSurface surface; eutil::ImageSurface surface;
std::cout << "Opening \"" << argv[1] << "\"\n";
if(!reader.ReadImage(yatfHeader, surface)) { if(!reader.ReadImage(yatfHeader, surface)) {
std::cout << "Invalid YATF file \"" << argv[1] << "\"\n"; std::cout << "Invalid YATF file \"" << argv[1] << "\"\n";
return 1; return 1;