161 lines
9.1 KiB
C
161 lines
9.1 KiB
C
|
|
#pragma once
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
// Чтение библиотеки спрайтов, собранной SpriteForge (`sf build`).
|
|||
|
|
//
|
|||
|
|
// Игра не знает про YAML, генераторы и PNG. Она видит только каталог с файлами
|
|||
|
|
// `<asset_id>.sfa` и общей палитрой `palettes.sfp`, а обращается к ассетам по
|
|||
|
|
// числовым идентификаторам из generated/spriteforge_assets.h. Строковых ID
|
|||
|
|
// ассетов в коде игры быть не должно.
|
|||
|
|
//
|
|||
|
|
// Слой ровно один: .sfa (индексы палитры + RLE по строкам) -> игровой Sprite
|
|||
|
|
// (RGBA8 с альфа-тестом). Дальше по коду разницы между процедурным спрайтом
|
|||
|
|
// и собранным нет.
|
|||
|
|
//
|
|||
|
|
// Распаковка идёт целиком на Open() и только там. Ленивое декодирование стоило
|
|||
|
|
// бы RLE-прохода и аллокации холста на КАЖДУЮ анимированную сущность в КАЖДОМ
|
|||
|
|
// кадре игры; готовые Sprite лежат в памяти, наружу отдаются ссылки на них.
|
|||
|
|
// -----------------------------------------------------------------------------
|
|||
|
|
#include <array>
|
|||
|
|
#include <cstdint>
|
|||
|
|
#include <string>
|
|||
|
|
#include <unordered_map>
|
|||
|
|
#include <vector>
|
|||
|
|
|
|||
|
|
#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<uint32_t> Ids() const;
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
struct Asset
|
|||
|
|
{
|
|||
|
|
std::string name;
|
|||
|
|
std::vector<Sprite> frames; // уже RGBA8; индекс — как в .sfa
|
|||
|
|
std::vector<uint16_t> durationsMs; // длительность кадра, параллельно frames
|
|||
|
|
std::vector<AnimClip> clips;
|
|||
|
|
std::vector<int> 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<uint32_t, Asset> assets_;
|
|||
|
|
std::array<uint8_t, 768> palette_{}; // 256 RGB-троек, индекс 0 прозрачен
|
|||
|
|
std::string dir_;
|
|||
|
|
Stamp stamp_;
|
|||
|
|
bool open_ = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Первый каталог сборки, в котором лежит palettes.sfp: рядом с бинарником
|
|||
|
|
// (build/bin -> ../sprites) или относительно корня репозитория. Пустая строка —
|
|||
|
|
// библиотека не собрана.
|
|||
|
|
std::string FindSpriteLibraryDir();
|