#pragma once /** * @file clx_sprite.hpp * * @brief CLX format sprites. * * CLX is a format used for DevilutionX graphics at runtime. * * It is identical to CL2, except we use the frame header to store the frame's width and height. * * CLX frame header (10 bytes, same as CL2): * * Bytes | Type | Value * :-----:|:--------:|------------------------------------ * 0..2 | uint16_t | offset to data start (same as CL2) * 2..4 | uint16_t | width * 4..6 | uint16_t | height * 6..10 | - | unused * * The CLX format is otherwise identical to CL2. * * Since the header is identical to CL2, CL2 can be converted to CLX without reallocation. * * CL2 reference: https://github.com/savagesteel/d1-file-formats/blob/master/PC-Mac/CL2.md#2-file-structure */ #include #include #include #include #include "appfat.h" #include "utils/endian.hpp" #include "utils/intrusive_optional.hpp" namespace devilution { class OptionalClxSprite; /** * @brief A single CLX sprite. */ class ClxSprite { static constexpr uint32_t HeaderSize = 10; public: explicit constexpr ClxSprite(const uint8_t *data, uint32_t dataSize) : data_(data) , pixel_data_size_(dataSize - HeaderSize) { assert(data != nullptr); } [[nodiscard]] constexpr uint16_t width() const { return LoadLE16(&data_[2]); } [[nodiscard]] constexpr uint16_t height() const { return LoadLE16(&data_[4]); } /** * @brief The raw pixel data (CL2 frame data). * * Format: https://github.com/savagesteel/d1-file-formats/blob/master/PC-Mac/CL2.md#42-cl2-frame-data */ [[nodiscard]] constexpr const uint8_t *pixelData() const { return &data_[HeaderSize]; } [[nodiscard]] constexpr uint32_t pixelDataSize() const { return pixel_data_size_; } constexpr bool operator==(const ClxSprite &other) const { return data_ == other.data_; } constexpr bool operator!=(const ClxSprite &other) const { return !(*this == other); } private: // For OptionalClxSprite. constexpr ClxSprite() : data_(nullptr) , pixel_data_size_(0) { } const uint8_t *data_; uint32_t pixel_data_size_; friend class OptionalClxSprite; }; class OwnedClxSpriteList; class OptionalClxSpriteList; class ClxSpriteListIterator; /** * @brief A list of `ClxSprite`s. */ class ClxSpriteList { public: explicit constexpr ClxSpriteList(const uint8_t *data) : data_(data) { assert(data != nullptr); } ClxSpriteList(const OwnedClxSpriteList &owned); [[nodiscard]] OwnedClxSpriteList clone() const; [[nodiscard]] constexpr uint32_t numSprites() const { return LoadLE32(data_); } [[nodiscard]] constexpr ClxSprite operator[](size_t spriteIndex) const { assert(spriteIndex < numSprites()); const uint32_t begin = spriteOffset(spriteIndex); const uint32_t end = spriteOffset(spriteIndex + 1); return ClxSprite { &data_[begin], end - begin }; } [[nodiscard]] constexpr uint32_t spriteOffset(size_t spriteIndex) const { return LoadLE32(&data_[4 + spriteIndex * 4]); } /** @brief The offset to the next sprite sheet, or file size if this is the last sprite sheet. */ [[nodiscard]] constexpr uint32_t nextSpriteSheetOffsetOrFileSize() const { return LoadLE32(&data_[4 + numSprites() * 4]); } [[nodiscard]] constexpr const uint8_t *data() const { return data_; } [[nodiscard]] constexpr ClxSpriteListIterator begin() const; [[nodiscard]] constexpr ClxSpriteListIterator end() const; private: // For OptionalClxSpriteList. constexpr ClxSpriteList() : data_(nullptr) { } const uint8_t *data_; friend class OptionalClxSpriteList; }; class ClxSpriteListIterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ClxSprite; using pointer = void; using reference = value_type &; constexpr ClxSpriteListIterator(ClxSpriteList list, size_t index) : list_(list) , index_(index) { } constexpr ClxSprite operator*() { return list_[index_]; } constexpr ClxSpriteListIterator &operator++() { ++index_; return *this; } constexpr ClxSpriteListIterator operator++(int) { auto copy = *this; ++(*this); return copy; } constexpr bool operator==(const ClxSpriteListIterator &other) const { return index_ == other.index_; } constexpr bool operator!=(const ClxSpriteListIterator &other) const { return !(*this == other); } private: ClxSpriteList list_; size_t index_; }; inline constexpr ClxSpriteListIterator ClxSpriteList::begin() const { return { *this, 0 }; } inline constexpr ClxSpriteListIterator ClxSpriteList::end() const { return { *this, numSprites() }; } class OwnedClxSpriteSheet; class OptionalClxSpriteSheet; class ClxSpriteSheetIterator; /** * @brief A sprite sheet is a list of `ClxSpriteList`s. */ class ClxSpriteSheet { public: explicit constexpr ClxSpriteSheet(const uint8_t *data, uint16_t numLists) : data_(data) , num_lists_(numLists) { assert(data != nullptr); assert(num_lists_ > 0); } ClxSpriteSheet(const OwnedClxSpriteSheet &owned); [[nodiscard]] constexpr uint16_t numLists() const { return num_lists_; } [[nodiscard]] constexpr ClxSpriteList operator[](size_t sheetIndex) const { return ClxSpriteList { &data_[sheetOffset(sheetIndex)] }; } [[nodiscard]] constexpr uint32_t sheetOffset(size_t sheetIndex) const { assert(sheetIndex < num_lists_); return LoadLE32(&data_[4 * sheetIndex]); } [[nodiscard]] constexpr const uint8_t *data() const { return data_; } [[nodiscard]] constexpr ClxSpriteSheetIterator begin() const; [[nodiscard]] constexpr ClxSpriteSheetIterator end() const; private: // For OptionalClxSpriteSheet. constexpr ClxSpriteSheet() : data_(nullptr) , num_lists_(0) { } const uint8_t *data_; uint16_t num_lists_; friend class OptionalClxSpriteSheet; }; class ClxSpriteSheetIterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ClxSpriteList; using pointer = void; using reference = value_type &; constexpr ClxSpriteSheetIterator(ClxSpriteSheet sheet, size_t index) : sheet_(sheet) , index_(index) { } constexpr ClxSpriteList operator*() { return sheet_[index_]; } constexpr ClxSpriteSheetIterator &operator++() { ++index_; return *this; } constexpr ClxSpriteSheetIterator operator++(int) { auto copy = *this; ++(*this); return copy; } constexpr bool operator==(const ClxSpriteSheetIterator &other) const { return index_ == other.index_; } constexpr bool operator!=(const ClxSpriteSheetIterator &other) const { return !(*this == other); } private: ClxSpriteSheet sheet_; size_t index_; }; inline constexpr ClxSpriteSheetIterator ClxSpriteSheet::begin() const { return { *this, 0 }; } inline constexpr ClxSpriteSheetIterator ClxSpriteSheet::end() const { return { *this, num_lists_ }; } class OptionalOwnedClxSpriteList; class OwnedClxSpriteListOrSheet; /** * @brief Implicitly convertible to `ClxSpriteList` and owns its data. */ class OwnedClxSpriteList { public: explicit OwnedClxSpriteList(std::unique_ptr &&data) : data_(std::move(data)) { assert(data_ != nullptr); } OwnedClxSpriteList(OwnedClxSpriteList &&) noexcept = default; OwnedClxSpriteList &operator=(OwnedClxSpriteList &&) noexcept = default; [[nodiscard]] OwnedClxSpriteList clone() const { return ClxSpriteList { *this }.clone(); } [[nodiscard]] ClxSprite operator[](size_t spriteIndex) const { return ClxSpriteList { *this }[spriteIndex]; } private: // For OptionalOwnedClxSpriteList. OwnedClxSpriteList() = default; std::unique_ptr data_; friend class ClxSpriteList; // for implicit conversion friend class OptionalOwnedClxSpriteList; friend class OwnedClxSpriteListOrSheet; }; inline ClxSpriteList::ClxSpriteList(const OwnedClxSpriteList &owned) : data_(owned.data_.get()) { } inline OwnedClxSpriteList ClxSpriteList::clone() const { const size_t dataSize = nextSpriteSheetOffsetOrFileSize(); std::unique_ptr data { new uint8_t[dataSize] }; memcpy(data.get(), data_, dataSize); return OwnedClxSpriteList { std::move(data) }; } /** * @brief Implicitly convertible to `ClxSpriteSheet` and owns its data. */ class OwnedClxSpriteSheet { public: OwnedClxSpriteSheet(std::unique_ptr &&data, uint16_t numLists) : data_(std::move(data)) , num_lists_(numLists) { assert(data_ != nullptr); assert(numLists > 0); } OwnedClxSpriteSheet(OwnedClxSpriteSheet &&) noexcept = default; OwnedClxSpriteSheet &operator=(OwnedClxSpriteSheet &&) noexcept = default; [[nodiscard]] ClxSpriteList operator[](size_t sheetIndex) const { return ClxSpriteSheet { *this }[sheetIndex]; } [[nodiscard]] ClxSpriteSheetIterator begin() const { return ClxSpriteSheet { *this }.begin(); } [[nodiscard]] ClxSpriteSheetIterator end() const { return ClxSpriteSheet { *this }.end(); } private: // For OptionalOwnedClxSpriteList. OwnedClxSpriteSheet() : data_(nullptr) , num_lists_(0) { } std::unique_ptr data_; uint16_t num_lists_; friend class ClxSpriteSheet; // for implicit conversion. friend class OptionalOwnedClxSpriteSheet; friend class OwnedClxSpriteListOrSheet; }; inline ClxSpriteSheet::ClxSpriteSheet(const OwnedClxSpriteSheet &owned) : data_(owned.data_.get()) , num_lists_(owned.num_lists_) { } class OwnedClxSpriteListOrSheet; class OptionalClxSpriteListOrSheet; inline uint16_t GetNumListsFromClxListOrSheetBuffer(const uint8_t *data, size_t size) { const uint32_t maybeNumFrames = LoadLE32(data); // If it is a number of frames, then the last frame offset will be equal to the size of the file. if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) return maybeNumFrames / 4; // Not a sprite sheet. return 0; } /** * @brief A CLX sprite list or a sprite sheet (list of lists). */ class ClxSpriteListOrSheet { public: constexpr ClxSpriteListOrSheet(const uint8_t *data, uint16_t numLists) : data_(data) , num_lists_(numLists) { } ClxSpriteListOrSheet(const OwnedClxSpriteListOrSheet &listOrSheet); [[nodiscard]] constexpr ClxSpriteList list() const { assert(num_lists_ == 0); return ClxSpriteList { data_ }; } [[nodiscard]] constexpr ClxSpriteSheet sheet() const { assert(num_lists_ != 0); return ClxSpriteSheet { data_, num_lists_ }; } [[nodiscard]] constexpr bool isSheet() const { return num_lists_ != 0; } private: const uint8_t *data_; uint16_t num_lists_; // For OptionalClxSpriteListOrSheet. constexpr ClxSpriteListOrSheet() : data_(nullptr) , num_lists_(0) { } friend class OptionalClxSpriteListOrSheet; }; class OptionalOwnedClxSpriteListOrSheet; /** * @brief A CLX sprite list or a sprite sheet (list of lists). */ class OwnedClxSpriteListOrSheet { public: static OwnedClxSpriteListOrSheet FromBuffer(std::unique_ptr &&data, size_t size) { const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(data.get(), size); return OwnedClxSpriteListOrSheet { std::move(data), numLists }; } explicit OwnedClxSpriteListOrSheet(std::unique_ptr &&data, uint16_t numLists) : data_(std::move(data)) , num_lists_(numLists) { } explicit OwnedClxSpriteListOrSheet(OwnedClxSpriteSheet &&sheet) : data_(std::move(sheet.data_)) , num_lists_(sheet.num_lists_) { } explicit OwnedClxSpriteListOrSheet(OwnedClxSpriteList &&list) : data_(std::move(list.data_)) , num_lists_(0) { } [[nodiscard]] ClxSpriteList list() const & { assert(num_lists_ == 0); return ClxSpriteList { data_.get() }; } [[nodiscard]] OwnedClxSpriteList list() && { assert(num_lists_ == 0); return OwnedClxSpriteList { std::move(data_) }; } [[nodiscard]] ClxSpriteSheet sheet() const & { assert(num_lists_ != 0); return ClxSpriteSheet { data_.get(), num_lists_ }; } [[nodiscard]] OwnedClxSpriteSheet sheet() && { assert(num_lists_ != 0); return OwnedClxSpriteSheet { std::move(data_), num_lists_ }; } [[nodiscard]] bool isSheet() const { return num_lists_ != 0; } private: std::unique_ptr data_; uint16_t num_lists_; // For OptionalOwnedClxSpriteListOrSheet. OwnedClxSpriteListOrSheet() : data_(nullptr) , num_lists_(0) { } friend class ClxSpriteListOrSheet; friend class OptionalOwnedClxSpriteListOrSheet; }; inline ClxSpriteListOrSheet::ClxSpriteListOrSheet(const OwnedClxSpriteListOrSheet &listOrSheet) : data_(listOrSheet.data_.get()) , num_lists_(listOrSheet.num_lists_) { } /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSprite { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSprite, ClxSprite, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteList { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSpriteList, ClxSpriteList, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteSheet { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSpriteSheet, ClxSpriteSheet, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteListOrSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalClxSpriteListOrSheet, ClxSpriteListOrSheet, data_, nullptr); }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalOwnedClxSpriteList { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteList, OwnedClxSpriteList, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalOwnedClxSpriteSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteSheet, OwnedClxSpriteSheet, data_, nullptr) }; class OptionalOwnedClxSpriteListOrSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteListOrSheet, OwnedClxSpriteListOrSheet, data_, nullptr); }; } // namespace devilution