diff --git a/CMakeLists.txt b/CMakeLists.txt index 8afcf8b..5426582 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,5 +9,7 @@ include(cmake/Policies.cmake) project(EuropaTools) +add_subdirectory(third_party/libpixel) + add_subdirectory(src/libeuropa) add_subdirectory(src/tools) diff --git a/include/europa/io/YatfReader.h b/include/europa/io/YatfReader.h new file mode 100644 index 0000000..57cfcc8 --- /dev/null +++ b/include/europa/io/YatfReader.h @@ -0,0 +1,51 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef EUROPA_IO_YATFREADER_H +#define EUROPA_IO_YATFREADER_H + +#include +#include +#include + +namespace europa::io { + + /** + * Reader for PS2 Europa .tex files. + */ + struct YatfReader { + explicit YatfReader(std::istream& is); + + void Init(std::istream& is); + + void ReadImage(); + + pixel::RgbaImage& GetImage(); + + const structs::YatfHeader& GetHeader() const; + + [[nodiscard]] bool Invalid() const { + return invalid; + } + + private: + std::istream& stream; + bool invalid {false}; + + + structs::YatfHeader header; + + /** + * converted image. + */ + pixel::RgbaImage image; + }; + +} + +#endif // EUROPA_IO_YATFREADER_H diff --git a/include/europa/structs/Yatf.h b/include/europa/structs/Yatf.h new file mode 100644 index 0000000..8da9d31 --- /dev/null +++ b/include/europa/structs/Yatf.h @@ -0,0 +1,52 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef EUROPA_STRUCTS_YATF_H +#define EUROPA_STRUCTS_YATF_H + +#include + +#include +#include + +namespace europa::structs { + + struct [[gnu::packed]] YatfHeader { + + constexpr static u32 TextureFlag_Unknown = 0x1; + + /** + * Texture does not have a palette + */ + constexpr static u32 TextureFlag_NoPalette = 0x30000; + + /** + * Texture uses alpha. + */ + constexpr static u32 TextureFlag_UsesAlpha = 0x1000000; + + constexpr static auto ValidMagic = util::FourCC<"YATF", std::endian::big>(); + + u32 magic; + + u32 flags; + + // Always zeroed. + u32 zero; + + u32 height; + u32 width; + + [[nodiscard]] constexpr bool IsValid() const { + return magic == ValidMagic; + } + }; + +} + +#endif // EUROPA_STRUCTS_YATF_H diff --git a/include/europa/util/FixedString.h b/include/europa/util/FixedString.h new file mode 100644 index 0000000..85813a3 --- /dev/null +++ b/include/europa/util/FixedString.h @@ -0,0 +1,40 @@ +// +// SSX 3 Lobby Server +// +// (C) 2021-2022 modeco80 +// +// This file is licensed under the GNU General Public License Version 3. +// Text is provided in LICENSE. +// + +#ifndef EUROPA_UTIL_FIXEDSTRING_H +#define EUROPA_UTIL_FIXEDSTRING_H + +#include + +namespace europa::util { + + template + struct FixedString { + char buf[N + 1]{}; + + constexpr FixedString(const char* s) { // NOLINT + for (unsigned i = 0; i != N; ++i) + buf[i] = s[i]; + } + + constexpr operator const char*() const { // NOLINT + return buf; + } + + [[nodiscard]] constexpr std::size_t Length() const { + return N; + } + }; + + template + FixedString(char const (&)[N]) -> FixedString; + +} + +#endif // EUROPA_UTIL_FIXEDSTRING_H diff --git a/include/europa/util/FourCC.h b/include/europa/util/FourCC.h new file mode 100644 index 0000000..df64635 --- /dev/null +++ b/include/europa/util/FourCC.h @@ -0,0 +1,42 @@ +// +// SSX 3 Lobby Server +// +// (C) 2021-2022 modeco80 +// +// This file is licensed under the GNU General Public License Version 3. +// Text is provided in LICENSE. +// + +#ifndef EUROPA_FOURCC_H +#define EUROPA_FOURCC_H + +#include + +#include + +namespace europa::util { + + /** + * A multi-endian, compile-time FourCC generator. + * You love to see it. + */ + template + consteval std::uint32_t FourCC() { + static_assert(fccString.Length() == 4, "Provided string is not a FourCC"); + + switch(Endian) { + case std::endian::little: + return (fccString[0]) | (fccString[1] << 8) | (fccString[2] << 16) | (fccString[3] << 24); + + case std::endian::big: + return (fccString[0] << 24) | (fccString[1] << 16) | (fccString[2] << 8) | fccString[3]; + } + + // Just return something with all possible bits set, if the user somehow + // got around the above switch (which shouldn't happen). + return 0xffffffff; + } + +} + +#endif // EUROPA_FOURCC_H diff --git a/src/libeuropa/CMakeLists.txt b/src/libeuropa/CMakeLists.txt index 00a3718..2448907 100644 --- a/src/libeuropa/CMakeLists.txt +++ b/src/libeuropa/CMakeLists.txt @@ -1,7 +1,9 @@ add_library(libeuropa + io/StreamUtils.cpp io/PakReader.cpp + io/YatfReader.cpp ) target_include_directories(libeuropa PUBLIC ${PROJECT_SOURCE_DIR}/include) @@ -10,3 +12,9 @@ set_target_properties(libeuropa PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON ) + +# Projects which libeuropa depends on +target_link_libraries(libeuropa PUBLIC + + pixel::libpixel +) diff --git a/src/libeuropa/io/PakReader.cpp b/src/libeuropa/io/PakReader.cpp index 5704748..a3575f5 100644 --- a/src/libeuropa/io/PakReader.cpp +++ b/src/libeuropa/io/PakReader.cpp @@ -21,7 +21,7 @@ namespace europa::io { void PakReader::ReadData() { auto ReadHeader = [&]() { - header = LameRead(stream); + header = impl::ReadStreamType(stream); }; auto ReadTocEntry = [&]() { @@ -29,10 +29,10 @@ namespace europa::io { // which we don't store inside the type (because we can't) // // Read this in first. - auto filename = ReadPString(stream); + auto filename = impl::ReadPString(stream); // Then read in the rest. - tocData[filename] = LameRead(stream); + tocData[filename] = impl::ReadStreamType(stream); }; ReadHeader(); diff --git a/src/libeuropa/io/StreamUtils.cpp b/src/libeuropa/io/StreamUtils.cpp new file mode 100644 index 0000000..1c9eb20 --- /dev/null +++ b/src/libeuropa/io/StreamUtils.cpp @@ -0,0 +1,63 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "StreamUtils.h" + +namespace europa::io::impl { + + namespace detail { + + void ReadStreamTypeImpl(std::istream& is, char* buffer, std::size_t size) { + if(!is) + throw std::runtime_error("stream is bad"); + + is.read(&buffer[0], size); + } + + } // namespace detail + + std::string ReadZeroTerminatedString(std::istream& is) { + std::string s; + char c; + + if(!is) + return ""; + + while(true) { + c = static_cast(is.get()); + + if(c == '\0') + return s; + + s.push_back(c); + } + } + + std::string ReadPString(std::istream& is) { + std::string s; + char c; + + if(!is) + return ""; + + // should be just resizing, and refactor this loop to not do this, + // but .... meh. I'll get to it if it's a problem + std::uint8_t length = is.get(); + s.reserve(length); + + while(true) { + c = static_cast(is.get()); + + if(c == '\0') + return s; + + s.push_back(c); + } + } + +} // namespace europa::io::impl \ No newline at end of file diff --git a/src/libeuropa/io/StreamUtils.h b/src/libeuropa/io/StreamUtils.h index b0c440e..6800d0b 100644 --- a/src/libeuropa/io/StreamUtils.h +++ b/src/libeuropa/io/StreamUtils.h @@ -12,59 +12,34 @@ #include #include -namespace europa::io { +namespace europa::io::impl { + + namespace detail { + void ReadStreamTypeImpl(std::istream& is, char* buffer, std::size_t size); + } + // This is lame. But it works :) template - T LameRead(std::istream& is) { - if(!is) - throw std::runtime_error("stream is bad"); + constexpr T ReadStreamType(std::istream& is) { + T object {}; - T t {}; - is.read(reinterpret_cast(&t), sizeof(T)); - return t; + // Absolutely UB. + union Hack { + T* t; + char* c; + } address { + .t = &object + }; + + detail::ReadStreamTypeImpl(is, address.c, sizeof(T)); + + return object; } - std::string ReadZeroTerminatedString(std::istream& is) { - std::string s; - char c; + std::string ReadZeroTerminatedString(std::istream& is); + std::string ReadPString(std::istream& is); - if(!is) - return ""; - - while(true) { - c = static_cast(is.get()); - - if(c == '\0') - return s; - - s.push_back(c); - } - } - - std::string ReadPString(std::istream& is) { - std::string s; - char c; - - if(!is) - return ""; - - // should be just resizing, and refactor this loop to not do this, - // but .... meh. I'll get to it if it's a problem - std::uint8_t length = is.get(); - s.reserve(length); - - while(true) { - c = static_cast(is.get()); - - if(c == '\0') - return s; - - s.push_back(c); - } - } - - -} +} // namespace europa::io::impl #endif // EUROPA_TOOLS_STREAMUTILS_H diff --git a/src/libeuropa/io/YatfReader.cpp b/src/libeuropa/io/YatfReader.cpp new file mode 100644 index 0000000..654dd15 --- /dev/null +++ b/src/libeuropa/io/YatfReader.cpp @@ -0,0 +1,63 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include +#include "StreamUtils.h" + +#include + +namespace europa::io { + + YatfReader::YatfReader(std::istream& is) + : stream(is) { + Init(stream); + } + + void YatfReader::Init(std::istream& is) { + // Read the image header. + header = impl::ReadStreamType(stream); + + if(!header.IsValid()) + invalid = true; + } + + void YatfReader::ReadImage() { + if(header.flags & structs::YatfHeader::TextureFlag_NoPalette) { + image.Resize({ static_cast(header.width), static_cast(header.height) }); + + stream.read(reinterpret_cast(image.GetBuffer()), (header.width * header.height) * sizeof(pixel::RgbaColor)); + } else { + pixel::RgbaColor palette[256]; + std::vector tempBuffer((header.width * header.height)); + + // NB: sizeof() does pre-multiplication, so it's 100% ok for us to do this. + stream.read(reinterpret_cast(&palette[0]), sizeof(palette)); + + stream.read(reinterpret_cast(&tempBuffer[0]), tempBuffer.size()); + + image.Resize({ static_cast(header.width), static_cast(header.height) }); + + auto* buffer = image.GetBuffer(); + const auto* data = &tempBuffer[0]; + + for(std::size_t i = 0; i < header.width * header.height; ++i) + *(buffer++) = palette[data[i]]; + } + } + + pixel::RgbaImage& YatfReader::GetImage() { + return image; + } + + const structs::YatfHeader& YatfReader::GetHeader() const { + return header; + } + + + +} \ No newline at end of file diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 15fad13..c3de1c3 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -6,3 +6,13 @@ set_target_properties(europa_pack_extractor PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON ) + + +add_executable(texdump texdump.cpp) + +target_link_libraries(texdump PUBLIC libeuropa) + +set_target_properties(texdump PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) \ No newline at end of file diff --git a/src/tools/texdump.cpp b/src/tools/texdump.cpp new file mode 100644 index 0000000..aeec82e --- /dev/null +++ b/src/tools/texdump.cpp @@ -0,0 +1,53 @@ +// +// EuropaTools +// +// (C) 2021-2022 modeco80 +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include +#include +#include + +#include + +#include + +namespace fs = std::filesystem; +int main(int argc, char** argv) { + if(argc != 2) { + std::cout << "Usage: " << argv[0] << " [path to Europa PAK file]"; + return 1; + } + + std::ifstream ifs(argv[1], std::ifstream::binary); + + if(!ifs) { + std::cout << "Invalid file \"" << argv[1] << "\"\n"; + return 1; + } + + europa::io::YatfReader reader(ifs); + + if(reader.Invalid()) { + std::cout << "Invalid YATF file \"" << argv[1] << "\"\n"; + return 1; + } + + reader.ReadImage(); + + + pixel::ImageWriter writer{}; + + auto outPath = fs::path(argv[1]).replace_extension(".png"); + + writer.SetImage(reader.GetImage()); + + writer.WritePng(outPath); + + std::cout << "Wrote image to " << outPath << '\n'; + + return 0; +} \ No newline at end of file