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 {
enum class TextureFormat : u8 {
kTextureFormat8Bpp = 0,
kTextureFormatUnknown = 1, // possibly 16bpp?
kTextureFormat24Bpp = 2,
kTextureFormat32Bpp = 3
kTextureFormatV1_8Bpp = 0,
kTextureFormatV1_24Bpp = 2,
kTextureFormatV1_32Bpp = 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;
u16 unkThing; // always 0x1
u16 version; // 0x1 for starfighter, 0x2 for jsf
TextureFormat format;
u8 unkThing2; // flags?
u8 unkThing2; // flags? some palbpp?
// Always zeroed.
u32 zero;
@ -40,7 +47,7 @@ namespace europa::structs {
u32 width;
[[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);
// 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;
switch(header.format) {
case kTextureFormat8Bpp: {
case kTextureFormatV1_8Bpp: {
util::Pixel palette[256] {};
util::UniqueArray<std::uint8_t> palettizedData(imageSize.Linear());
@ -51,7 +58,7 @@ namespace europa::io::yatf {
}
} break;
case kTextureFormat24Bpp: {
case kTextureFormatV1_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());
@ -65,16 +72,96 @@ namespace europa::io::yatf {
}
} break;
case kTextureFormat32Bpp:
case kTextureFormatV1_32Bpp:
// We can directly read data
stream.read(reinterpret_cast<char*>(surface.GetBuffer()), imageSize.LinearWithStride<util::Pixel>());
break;
case kTextureFormatUnknown:
default:
throw std::runtime_error(std::format("Unknown/unsupported texture format {:02x}!", (std::uint16_t)header.format));
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;
}

View file

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