#pragma once // ----------------------------------------------------------------------------- // Чтение библиотеки спрайтов, собранной SpriteForge (`sf build`). // // Игра не знает про YAML, генераторы и PNG. Она видит только каталог с файлами // `.sfa` и общей палитрой `palettes.sfp`, а обращается к ассетам по // числовым идентификаторам из generated/spriteforge_assets.h. Строковых ID // ассетов в коде игры быть не должно. // // Слой ровно один: .sfa (индексы палитры + RLE по строкам) -> игровой Sprite // (RGBA8 с альфа-тестом). Дальше по коду разницы между процедурным спрайтом // и собранным нет. // // Распаковка идёт целиком на Open() и только там. Ленивое декодирование стоило // бы RLE-прохода и аллокации холста на КАЖДУЮ анимированную сущность в КАЖДОМ // кадре игры; готовые Sprite лежат в памяти, наружу отдаются ссылки на них. // ----------------------------------------------------------------------------- #include #include #include #include #include #include "engine/framebuffer.h" #include "spriteforge_assets.h" // sf_asset_id, SF_ASSET_*, SF_ANIM_* // Куда положить кадр .sfa на холст игрового спрайта. // // SpriteForge обрезает кадр по силуэту, поэтому размер кадра плывёт вместе с // содержимым. Тайлам это не подходит: ShadeQuad считается в координатах // спрайта и предполагает канонический холст с ромбом в известном месте // (Lighting::QuadFor). Placement кладёт кадр на такой холст: pivot из .sfa // совмещается с (pivotX, pivotY). struct FramePlacement { int canvasW = 0; // 0 — холст по размеру кадра, без дополнения int canvasH = 0; int pivotX = 0; // куда на холсте попадает pivot кадра int pivotY = 0; int anchorX = -1; // Sprite::ax; -1 — привязка в точке pivot int anchorY = -1; // Sprite::ay }; // Один клип внутри ассета. Имя приходит только хешем (SF_ANIM_*): строковых // имён анимаций в игре, как и строковых ID ассетов, быть не должно. struct AnimClip { uint32_t nameHash = 0; int firstFrame = 0; // индекс первого кадра клипа в плоском списке кадров int framesPerDir = 0; // кадров на одно направление }; class SpriteLibrary { public: // Открыть каталог сборки (обычно build/sprites). Файлы читаются целиком // и распаковываются сразу: битый ассет просто не попадает в набор. bool Open(const std::string& dir); void Close(); bool IsOpen() const { return open_; } const std::string& Dir() const { return dir_; } int AssetCount() const { return int(assets_.size()); } // Все кадры ассета скопом, по всем анимациям и направлениям. int FrameCount(uint32_t assetId) const; // Кадр как игровой Sprite. false — нет такого ассета или кадра; out в этом // случае не трогается. Копия: вызывающий волен её пережить и подшаманить // (так грузятся тайлы, им нужен канонический холст). bool Read(uint32_t assetId, int frame, const FramePlacement& place, Sprite& out) const; bool Read(uint32_t assetId, int frame, Sprite& out) const { return Read(assetId, frame, FramePlacement{}, out); } // --- анимации --- // // Раскладка кадров в .sfa НАПРАВЛЕНЧЕСКИ-МАЖОРНАЯ, клипы идут подряд: // index = clip.firstFrame + dirIndex * clip.framesPerDir + frameInClip // Поэтому firstFrame анимации — это не ноль, а конец предыдущей. int AnimationCount(uint32_t assetId) const; // Клип по хешу имени (SF_ANIM_* из generated/spriteforge_assets.h). bool FindAnimation(uint32_t assetId, uint32_t animNameHash, AnimClip& out) const; // Сколько направлений отрисовано у ассета; 1 — вид один на все стороны. int DirectionCount(uint32_t assetId) const; // Индекс направления, ближайшего к мировому углу (радианы, как Agent::facing). // Нет ассета — 0: рисовать всё равно нечего, а ветвиться у вызывающего лишнее. int DirectionOf(uint32_t assetId, float angleRad) const; // Кадр анимации: timeSec — время от её начала, loop — зациклить, иначе клип // замирает на последнем кадре (смерть так и остаётся лежать). // nullptr — нет такого ассета или анимации. // // ВНИМАНИЕ: указатель живёт до ближайшего PollReload(), вернувшего true. // После пересборки библиотеки все выданные раньше кадры недействительны, // держать их между кадрами игры нельзя. const Sprite* Frame(uint32_t assetId, uint32_t animNameHash, int dirIndex, float timeSec, bool loop) const; // Длительность клипа целиком, сек. 0 — нет такой анимации либо все кадры // нулевой длительности (статичная поза). float AnimationLength(uint32_t assetId, uint32_t animNameHash) const; // Watch-режим `sf watch` атомарно переписывает reload.json после успешной // пересборки. Вызов раз в кадр; true — библиотека перечитана и спрайты, // взятые из неё раньше, устарели. Смена набора анимаций переваривается // сама собой: пересобирается вся библиотека целиком, а не отдельные кадры. bool PollReload(); // Имя ассета (оно же имя файла) — только для отладочного отчёта. const char* NameOf(uint32_t assetId) const; // Все идентификаторы, отсортированные по имени. Тоже только для отчёта. std::vector Ids() const; private: struct Asset { std::string name; std::vector frames; // уже RGBA8; индекс — как в .sfa std::vector durationsMs; // длительность кадра, параллельно frames std::vector clips; std::vector dirs; // сотые доли градуса, приведённые к [0, 36000) }; // Отпечаток reload.json: время правки и размер. 0 — файла нет. struct Stamp { int64_t time = 0; uint64_t size = 0; bool operator!=(const Stamp& o) const { return time != o.time || size != o.size; } }; // Разобрать .sfa целиком: все кадры в Sprite, клипы, направления. // false — файл битый или его раскладка не сходится сама с собой. bool DecodeAsset(const uint8_t* bytes, size_t size, Asset& out) const; const Asset* FindAsset(uint32_t assetId) const; static const AnimClip* FindClip(const Asset& asset, uint32_t animNameHash); static float ClipLength(const Asset& asset, const AnimClip& clip); static int FrameInClip(const Asset& asset, const AnimClip& clip, float timeSec, bool loop); Stamp ReloadStamp() const; std::unordered_map assets_; std::array palette_{}; // 256 RGB-троек, индекс 0 прозрачен std::string dir_; Stamp stamp_; bool open_ = false; }; // Первый каталог сборки, в котором лежит palettes.sfp: рядом с бинарником // (build/bin -> ../sprites) или относительно корня репозитория. Пустая строка — // библиотека не собрана. std::string FindSpriteLibraryDir();