feat(场景管理): 添加UIScene支持并重构场景管理器

refactor(渲染器): 优化相机切换时的渲染批处理

feat(调试工具): 新增游戏调试UI场景和九宫格面板组件

fix(动画系统): 跳过缺失的动画资源加载并记录日志

perf(资源加载): 使用BinaryFileStreamReader优化NPK文件解析

feat(地图系统): 支持多边形可行走区域调试显示

style(代码格式): 清理多余空格和统一文件编码

docs(注释): 补充关键类和方法的文档说明

test(启动跟踪): 添加启动过程性能跟踪工具

chore(依赖): 添加SDL2_ttf库支持
This commit is contained in:
2026-04-06 01:18:21 +08:00
parent 6cd1b42fef
commit bcc285eed6
36 changed files with 2675 additions and 513 deletions

View File

@@ -0,0 +1,62 @@
#pragma once
#include <frostbite2D/types/type_alias.h>
#include <fstream>
#include <string>
#include <vector>
namespace frostbite2D {
class BinaryFileStreamReader {
public:
BinaryFileStreamReader() = default;
explicit BinaryFileStreamReader(const std::string& filePath);
~BinaryFileStreamReader();
bool open(const std::string& filePath);
void close();
bool isOpen() const;
size_t tell() const;
void seek(size_t pos);
void skip(size_t count);
bool eof() const;
size_t size() const;
size_t remaining() const;
size_t lastReadCount() const;
size_t read(char* buffer, size_t size);
std::vector<uint8> readBytes(size_t size);
template <typename T>
T read() {
T value{};
read(reinterpret_cast<char*>(&value), sizeof(T));
return value;
}
int8 readInt8();
int16 readInt16();
int32 readInt32();
int64 readInt64();
uint8 readUInt8();
uint16 readUInt16();
uint32 readUInt32();
uint64 readUInt64();
float readFloat();
double readDouble();
std::string readString(size_t length);
std::string readNullTerminatedString(size_t maxLength = 4096);
private:
std::ifstream stream_;
std::string filePath_;
size_t size_ = 0;
size_t position_ = 0;
size_t lastReadCount_ = 0;
};
} // namespace frostbite2D

View File

@@ -1,18 +1,19 @@
#pragma once #pragma once
#include <frostbite2D/types/type_alias.h> #include <frostbite2D/types/type_alias.h>
#include <frostbite2D/resource/binary_reader.h>
#include <frostbite2D/resource/asset.h> #include <frostbite2D/resource/asset.h>
#include <optional> #include <optional>
#include <string> #include <string>
#include <map>
#include <vector> #include <vector>
#include <list> #include <list>
#include <memory> #include <memory>
#include <unordered_map>
#include <zlib.h> #include <zlib.h>
namespace frostbite2D { namespace frostbite2D {
class BinaryFileStreamReader;
struct ImageFrame { struct ImageFrame {
int32 type = 0; int32 type = 0;
int32 compressionType = 0; int32 compressionType = 0;
@@ -74,29 +75,57 @@ public:
size_t getDefaultImgFrame() const; size_t getDefaultImgFrame() const;
private: private:
enum class IndexCacheValidationMode : uint32 {
TrustCache = 1,
StrictSourceState = 2
};
struct SourceFileState {
std::string fileName;
uint64 size = 0;
int64 writeTime = 0;
bool operator==(const SourceFileState& other) const {
return fileName == other.fileName && size == other.size &&
writeTime == other.writeTime;
}
};
NpkArchive() = default; NpkArchive() = default;
~NpkArchive() = default; ~NpkArchive() = default;
std::string normalizePath(const std::string& path) const; std::string normalizePath(const std::string& path) const;
std::string getCacheFilePath() const;
IndexCacheValidationMode getIndexCacheValidationMode() const;
std::vector<SourceFileState> collectSourceFileStates() const;
bool loadIndexCache(const std::vector<SourceFileState>& sourceFiles);
bool saveIndexCache(const std::vector<SourceFileState>& sourceFiles) const;
void scanNpkFiles(); void scanNpkFiles();
bool parseNpkFile(const std::string& npkPath); bool parseNpkFile(const std::string& npkPath);
bool loadImgData(ImgRef& img); bool loadImgData(ImgRef& img);
void parseColor(const uint8* tab, int type, uint8* saveByte, int offset); void parseColor(const uint8* tab, int type, uint8* saveByte, int offset);
void evictCacheIfNeeded(size_t requiredSize); void evictCacheIfNeeded(size_t requiredSize);
void updateCacheUsage(const std::string& imgPath); void updateCacheUsage(const std::string& imgPath);
std::string readNpkInfoString(BinaryReader& reader); std::string readNpkInfoString(BinaryFileStreamReader& reader);
static const uint8 NPK_KEY[256]; static const uint8 NPK_KEY[256];
std::string imagePackDirectory_ = "ImagePacks2"; std::string imagePackDirectory_ = "ImagePacks2";
bool initialized_ = false; bool initialized_ = false;
std::map<std::string, ImgRef> imgIndex_; std::unordered_map<std::string, ImgRef> imgIndex_;
std::map<std::string, CachedImageData> imageCache_; std::unordered_map<std::string, CachedImageData> imageCache_;
std::list<std::string> lruList_; std::list<std::string> lruList_;
std::unordered_map<std::string, std::list<std::string>::iterator> lruLookup_;
size_t maxCacheSize_ = 512 * 1024 * 1024; size_t maxCacheSize_ = 512 * 1024 * 1024;
size_t currentCacheSize_ = 0; size_t currentCacheSize_ = 0;
std::string defaultImgPath_; std::string defaultImgPath_;
size_t defaultImgFrame_ = 0; size_t defaultImgFrame_ = 0;
bool verboseFallbackLog_ =
#ifndef NDEBUG
true;
#else
false;
#endif
}; };
} }

View File

@@ -1,51 +1,41 @@
#pragma once #pragma once
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <frostbite2D/types/type_alias.h> #include <frostbite2D/types/type_alias.h>
#include <frostbite2D/resource/binary_reader.h>
#include <map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <map>
#include <vector> #include <vector>
namespace frostbite2D { namespace frostbite2D {
/** /**
* @brief PVF 文件信息结构体 * @brief PVF ???????
*/ */
struct PvfFileInfo { struct PvfFileInfo {
size_t offset = 0; ///< 相对于数据起始位置的偏移 size_t offset = 0; ///< ????????????
uint32 crc32 = 0; ///< CRC32 校验值 uint32 crc32 = 0; ///< CRC32 ???
size_t length = 0; ///< 文件长度(字节) size_t length = 0; ///< ????????
bool decoded = false; ///< 是否已解码 bool decoded = false; ///< ?????
}; };
/** /**
* @brief 原始文件数据结构体 * @brief ?????????
*/ */
struct RawData { struct RawData {
std::unique_ptr<char[]> data; ///< 原始数据指针(智能指针自动管理内存) std::unique_ptr<char[]> data; ///< ??????????????????
size_t size; ///< 数据大小(字节) size_t size; ///< ????????
}; };
/** /**
* @brief PVF 资源包归档类 * @brief PVF ??????
* *
* 用于读取和解析 PVF 格式的游戏资源包文件。 * ??????? PVF ???????????
* 提供文件索引管理、字符串资源访问和文件内容读取功能。 * ??????????????????????????
*
* @example
* auto& archive = PvfArchive::get();
* if (archive.open("Script.pvf")) {
* archive.init();
* auto content = archive.getFileContent("script/example.txt");
* }
*/ */
class PvfArchive { class PvfArchive {
public: public:
/**
* @brief 获取单例实例
* @return PVF 归档实例引用
*/
static PvfArchive& get(); static PvfArchive& get();
PvfArchive(const PvfArchive&) = delete; PvfArchive(const PvfArchive&) = delete;
@@ -53,166 +43,53 @@ public:
PvfArchive(PvfArchive&&) = delete; PvfArchive(PvfArchive&&) = delete;
PvfArchive& operator=(PvfArchive&&) = delete; PvfArchive& operator=(PvfArchive&&) = delete;
// ---------------------------------------------------------------------------
// 文件操作
// ---------------------------------------------------------------------------
/**
* @brief 打开 PVF 文件
* @param filePath 文件路径
* @return 打开成功返回 true
*/
bool open(const std::string& filePath = "Script.pvf"); bool open(const std::string& filePath = "Script.pvf");
/**
* @brief 关闭并清空所有数据
*/
void close(); void close();
/**
* @brief 检查是否已打开
* @return 已打开返回 true
*/
bool isOpen() const; bool isOpen() const;
// ---------------------------------------------------------------------------
// 初始化
// ---------------------------------------------------------------------------
/**
* @brief 完整初始化(解析头部、加载字符串表)
*/
void init(); void init();
/**
* @brief 解析 PVF 文件头部并建立文件索引
*/
void initHeader(); void initHeader();
/**
* @brief 加载二进制字符串表 (stringtable.bin)
*/
void initBinStringTable(); void initBinStringTable();
/**
* @brief 加载本地化字符串 (n_string.lst)
*/
void initLoadStrings(); void initLoadStrings();
// ---------------------------------------------------------------------------
// 文件信息查询
// ---------------------------------------------------------------------------
/**
* @brief 检查文件是否存在
* @param path 文件路径
* @return 存在返回 true
*/
bool hasFile(const std::string& path) const; bool hasFile(const std::string& path) const;
/**
* @brief 获取文件信息
* @param path 文件路径
* @return 文件信息,不存在返回 std::nullopt
*/
std::optional<PvfFileInfo> getFileInfo(const std::string& path) const; std::optional<PvfFileInfo> getFileInfo(const std::string& path) const;
/**
* @brief 获取所有文件路径列表
* @return 文件路径列表
*/
std::vector<std::string> listFiles() const; std::vector<std::string> listFiles() const;
// ---------------------------------------------------------------------------
// 文件内容读取
// ---------------------------------------------------------------------------
/**
* @brief 获取文件内容作为字符串
* @param path 文件路径
* @return 文件内容,失败返回 std::nullopt
*/
std::optional<std::string> getFileContent(const std::string& path); std::optional<std::string> getFileContent(const std::string& path);
/**
* @brief 获取文件内容作为字节数组
* @param path 文件路径
* @return 字节数组,失败返回 std::nullopt
*/
std::optional<std::vector<uint8>> getFileBytes(const std::string& path); std::optional<std::vector<uint8>> getFileBytes(const std::string& path);
/**
* @brief 获取文件原始数据(使用智能指针管理内存,已包含 CRC 解密)
*
* 与原始 GetFileContentChar 功能相同,但使用智能指针避免内存泄漏。
* 数据会在首次访问时自动进行 CRC 解密。
*
* @param path 文件路径
* @return RawData 结构体,包含数据指针和大小;失败返回 std::nullopt
*/
std::optional<RawData> getFileRawData(const std::string& path); std::optional<RawData> getFileRawData(const std::string& path);
// ---------------------------------------------------------------------------
// 字符串资源访问
// ---------------------------------------------------------------------------
/**
* @brief 获取二进制字符串
* @param key 字符串键
* @return 字符串,不存在返回 std::nullopt
*/
std::optional<std::string> getBinString(int key) const; std::optional<std::string> getBinString(int key) const;
std::optional<std::string> getLoadString(const std::string& type,
/** const std::string& key) const;
* @brief 获取本地化字符串
* @param type 字符串类型
* @param key 字符串键
* @return 字符串,不存在返回 std::nullopt
*/
std::optional<std::string> getLoadString(const std::string& type, const std::string& key) const;
/**
* @brief 检查二进制字符串是否存在
* @param key 字符串键
* @return 存在返回 true
*/
bool hasBinString(int key) const; bool hasBinString(int key) const;
/**
* @brief 检查本地化字符串是否存在
* @param type 字符串类型
* @param key 字符串键
* @return 存在返回 true
*/
bool hasLoadString(const std::string& type, const std::string& key) const; bool hasLoadString(const std::string& type, const std::string& key) const;
std::string normalizePath(const std::string& path) const; std::string normalizePath(const std::string& path) const;
std::string resolvePath(const std::string& baseDir, const std::string& path) const; std::string resolvePath(const std::string& baseDir,
const std::string& path) const;
private: private:
PvfArchive() = default; PvfArchive() = default;
~PvfArchive() = default; ~PvfArchive() = default;
/** std::vector<std::string> splitString(const std::string& str,
* @brief 分割字符串 const std::string& delimiter) const;
* @param str 要分割的字符串 bool decodeFile(const std::string& normalizedPath, PvfFileInfo& info);
* @param delimiter 分隔符 void clearInitData();
* @return 分割后的字符串列表 std::optional<std::vector<uint8>> readArchiveBytes(size_t absoluteOffset,
*/ size_t length);
std::vector<std::string> splitString(const std::string& str, const std::string& delimiter) const; std::optional<std::vector<uint8>> readDecodedFileBytes(
const PvfFileInfo& info);
static void crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32);
/** BinaryFileStreamReader reader_;
* @brief 解码文件内容(内部使用) size_t dataStartPos_ = 0;
* @param info 文件信息(会修改 decoded 标志) std::map<std::string, PvfFileInfo> fileInfo_;
* @return 成功返回 true std::map<int, std::string> binStringTable_;
*/ std::map<std::string, std::map<std::string, std::string>> loadStrings_;
bool decodeFile(PvfFileInfo& info); std::map<std::string, std::vector<uint8>> decodedFileCache_;
BinaryReader reader_; ///< 二进制读取器
size_t dataStartPos_ = 0; ///< 数据起始位置
std::map<std::string, PvfFileInfo> fileInfo_; ///< 文件信息映射
std::map<int, std::string> binStringTable_; ///< 二进制字符串表
std::map<std::string, std::map<std::string, std::string>> loadStrings_; ///< 本地化字符串
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -12,6 +12,8 @@
namespace frostbite2D { namespace frostbite2D {
class BinaryFileStreamReader;
struct AudioRef { struct AudioRef {
std::string path; std::string path;
std::string npkFile; std::string npkFile;
@@ -65,7 +67,7 @@ private:
bool loadAudioData(AudioRef& audio); bool loadAudioData(AudioRef& audio);
void evictCacheIfNeeded(size_t requiredSize); void evictCacheIfNeeded(size_t requiredSize);
void updateCacheUsage(const std::string& audioPath); void updateCacheUsage(const std::string& audioPath);
std::string readNpkInfoString(BinaryReader& reader); std::string readNpkInfoString(BinaryFileStreamReader& reader);
static const uint8 NPK_KEY[256]; static const uint8 NPK_KEY[256];

View File

@@ -1,11 +1,13 @@
#pragma once #pragma once
#include <frostbite2D/event/event.h>
#include <frostbite2D/types/type_alias.h> #include <frostbite2D/types/type_alias.h>
#include <vector> #include <vector>
namespace frostbite2D { namespace frostbite2D {
class Scene; class Scene;
class UIScene;
class SceneManager { class SceneManager {
public: public:
@@ -17,20 +19,31 @@ public:
void PushScene(Ptr<Scene> scene); void PushScene(Ptr<Scene> scene);
void PopScene(); void PopScene();
void ReplaceScene(Ptr<Scene> scene); void ReplaceScene(Ptr<Scene> scene);
void PushUIScene(Ptr<UIScene> scene);
void PopUIScene();
void ReplaceUIScene(Ptr<UIScene> scene);
bool RemoveUIScene(UIScene* scene);
void ClearUIScenes();
void ClearAll(); void ClearAll();
void Update(float deltaTime); void Update(float deltaTime);
void UpdateUI(float deltaTime);
void Render(); void Render();
bool DispatchEvent(const Event& event);
bool DispatchUIEvent(const Event& event);
Scene* GetCurrentScene() const; Scene* GetCurrentScene() const;
UIScene* GetCurrentUIScene() const;
bool HasActiveScene() const; bool HasActiveScene() const;
bool HasActiveUIScene() const;
private: private:
SceneManager() = default; SceneManager() = default;
// ~SceneManager() 在 shutdown() 中手动调用销毁
std::vector<Ptr<Scene>> sceneStack_; std::vector<Ptr<Scene>> sceneStack_;
std::vector<Ptr<UIScene>> uiSceneStack_;
}; };
} } // namespace frostbite2D

View File

@@ -0,0 +1,20 @@
#pragma once
#include <frostbite2D/graphics/camera.h>
#include <frostbite2D/scene/scene.h>
namespace frostbite2D {
class UIScene : public Scene {
public:
UIScene();
~UIScene() override = default;
Camera* GetCamera() { return &camera_; }
const Camera* GetCamera() const { return &camera_; }
private:
Camera camera_;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,152 @@
#pragma once
#include <SDL2/SDL.h>
#include <fstream>
#include <mutex>
#include <string>
namespace frostbite2D {
class StartupTrace {
public:
static constexpr bool enabled() {
#ifndef NDEBUG
return true;
#else
return false;
#endif
}
static void reset(const char* label = nullptr) {
if (!enabled()) {
return;
}
Uint64 now = SDL_GetPerformanceCounter();
std::string line;
{
std::lock_guard<std::mutex> lock(mutex());
startCounter() = now;
if (label && label[0] != '\0') {
line = std::string("[Startup] reset: ") + label;
clearLogFileUnlocked();
writeLineUnlocked(line);
} else {
clearLogFileUnlocked();
}
}
if (!line.empty()) {
SDL_Log("%s", line.c_str());
}
}
static double totalElapsedMs() {
if (!enabled()) {
return 0.0;
}
Uint64 start = 0;
{
std::lock_guard<std::mutex> lock(mutex());
start = startCounter();
}
if (start == 0) {
return 0.0;
}
Uint64 now = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
if (frequency == 0) {
return 0.0;
}
return static_cast<double>(now - start) * 1000.0 /
static_cast<double>(frequency);
}
static void mark(const char* label) {
if (!enabled()) {
return;
}
char buffer[256] = {};
SDL_snprintf(buffer, sizeof(buffer),
"[Startup] %s reached at %.2f ms (thread=%u)",
label ? label : "<unnamed>", totalElapsedMs(), SDL_ThreadID());
logLine(buffer);
SDL_Log("%s", buffer);
}
static void logLine(const std::string& line) {
if (!enabled()) {
return;
}
std::lock_guard<std::mutex> lock(mutex());
writeLineUnlocked(line);
}
private:
static Uint64& startCounter() {
static Uint64 counter = 0;
return counter;
}
static std::mutex& mutex() {
static std::mutex value;
return value;
}
static const char* logFilePath() {
return "startup_trace.log";
}
static void clearLogFileUnlocked() {
std::ofstream stream(logFilePath(), std::ios::trunc);
}
static void writeLineUnlocked(const std::string& line) {
std::ofstream stream(logFilePath(), std::ios::app);
if (!stream) {
return;
}
stream << line << '\n';
}
};
class ScopedStartupTrace {
public:
explicit ScopedStartupTrace(const char* label)
: label_(label ? label : "<unnamed>"),
startCounter_(SDL_GetPerformanceCounter()) {}
~ScopedStartupTrace() {
if (!StartupTrace::enabled()) {
return;
}
Uint64 now = SDL_GetPerformanceCounter();
Uint64 frequency = SDL_GetPerformanceFrequency();
double elapsedMs = 0.0;
if (frequency != 0) {
elapsedMs = static_cast<double>(now - startCounter_) * 1000.0 /
static_cast<double>(frequency);
}
char buffer[256] = {};
SDL_snprintf(buffer, sizeof(buffer),
"[Startup] %s took %.2f ms (total %.2f ms, thread=%u)",
label_, elapsedMs, StartupTrace::totalElapsedMs(),
SDL_ThreadID());
StartupTrace::logLine(buffer);
SDL_Log("%s", buffer);
}
private:
const char* label_;
Uint64 startCounter_ = 0;
};
} // namespace frostbite2D

View File

@@ -3,9 +3,36 @@
#include <frostbite2D/graphics/texture.h> #include <frostbite2D/graphics/texture.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h> #include <SDL2/SDL_ttf.h>
#include <vector>
namespace frostbite2D { namespace frostbite2D {
namespace {
Uint32 readSurfacePixel(const SDL_Surface* surface, int x, int y) {
const uint8* row =
static_cast<const uint8*>(surface->pixels) + y * surface->pitch;
const uint8* pixel = row + x * surface->format->BytesPerPixel;
switch (surface->format->BytesPerPixel) {
case 1:
return *pixel;
case 2:
return *reinterpret_cast<const uint16*>(pixel);
case 3:
if (SDL_BYTEORDER == SDL_BIG_ENDIAN) {
return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
}
return pixel[0] | (pixel[1] << 8) | (pixel[2] << 16);
case 4:
return *reinterpret_cast<const uint32*>(pixel);
default:
return 0;
}
}
} // namespace
TextSprite::TextSprite() TextSprite::TextSprite()
: Sprite() { : Sprite() {
} }
@@ -79,27 +106,36 @@ void TextSprite::RenderText() {
return; return;
} }
SDL_PixelFormat* rgbaFormat = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
SDL_Surface* converted = SDL_ConvertSurface(surface, rgbaFormat, 0);
SDL_FreeSurface(surface);
surface = converted;
SDL_FreeFormat(rgbaFormat);
if (!surface) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to convert text surface: %s", SDL_GetError());
return;
}
int width = surface->w; int width = surface->w;
int height = surface->h; int height = surface->h;
std::vector<uint8> rgbaData(width * height * 4); std::vector<uint8> rgbaData(width * height * 4);
uint8* pixels = static_cast<uint8*>(surface->pixels); if (SDL_MUSTLOCK(surface) && SDL_LockSurface(surface) != 0) {
for (int i = 0; i < width * height; i++) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to lock text surface: %s",
rgbaData[i * 4 + 0] = pixels[i * 4 + 0]; SDL_GetError());
rgbaData[i * 4 + 1] = pixels[i * 4 + 1]; SDL_FreeSurface(surface);
rgbaData[i * 4 + 2] = pixels[i * 4 + 2]; return;
rgbaData[i * 4 + 3] = pixels[i * 4 + 3]; }
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
Uint8 r = 0;
Uint8 g = 0;
Uint8 b = 0;
Uint8 a = 0;
Uint32 pixel = readSurfacePixel(surface, x, y);
SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
size_t offset = static_cast<size_t>((y * width + x) * 4);
rgbaData[offset + 0] = r;
rgbaData[offset + 1] = g;
rgbaData[offset + 2] = b;
rgbaData[offset + 3] = a;
}
}
if (SDL_MUSTLOCK(surface)) {
SDL_UnlockSurface(surface);
} }
SDL_FreeSurface(surface); SDL_FreeSurface(surface);

View File

@@ -24,6 +24,7 @@
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/types/type_math.h> #include <frostbite2D/types/type_math.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D { namespace frostbite2D {
@@ -38,6 +39,8 @@ bool Application::init() {
} }
bool Application::init(const AppConfig& config) { bool Application::init(const AppConfig& config) {
ScopedStartupTrace startupTrace("Application::init internals");
if (initialized_) { if (initialized_) {
return true; return true;
} }
@@ -46,11 +49,17 @@ bool Application::init(const AppConfig& config) {
// 平台相关初始化 // 平台相关初始化
#ifdef __SWITCH__ #ifdef __SWITCH__
{
ScopedStartupTrace stageTrace("switchInit");
switchInit(); switchInit();
}
#endif #endif
#ifdef _WIN32 #ifdef _WIN32
{
ScopedStartupTrace stageTrace("windowsInit");
windowsInit(); windowsInit();
}
#endif #endif
// 初始化核心模块 // 初始化核心模块
@@ -122,6 +131,7 @@ Application::~Application() {
} }
bool Application::initCoreModules() { bool Application::initCoreModules() {
ScopedStartupTrace startupTrace("Application::initCoreModules");
// 初始化资产管理器 // 初始化资产管理器
auto &asset = Asset::get(); auto &asset = Asset::get();
@@ -129,33 +139,45 @@ bool Application::initCoreModules() {
// 平台相关 switch平台不可以获取当前工作目录 // 平台相关 switch平台不可以获取当前工作目录
#ifndef __SWITCH__ #ifndef __SWITCH__
// 获取程序工作目录 // 获取程序工作目录
{
ScopedStartupTrace stageTrace("Asset working directory setup");
std::string workingDir = SDL_GetBasePath(); std::string workingDir = SDL_GetBasePath();
asset.setWorkingDirectory(workingDir); asset.setWorkingDirectory(workingDir);
SDL_Log("Asset working directory: %s", workingDir.c_str()); SDL_Log("Asset working directory: %s", workingDir.c_str());
}
#else #else
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName); asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str()); SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
#endif #endif
{
ScopedStartupTrace stageTrace("SDL_Init video/events/controller");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return false; return false;
} }
}
// 打开第一个手柄(如果存在) // 打开第一个手柄(如果存在)
{
ScopedStartupTrace stageTrace("SDL_GameControllerOpen(0)");
if (SDL_GameController *controller = SDL_GameControllerOpen(0)) { if (SDL_GameController *controller = SDL_GameControllerOpen(0)) {
SDL_Log("GameController opened: %s", SDL_GameControllerName(controller)); SDL_Log("GameController opened: %s", SDL_GameControllerName(controller));
} else { } else {
SDL_Log("No GameController found"); SDL_Log("No GameController found");
} }
}
// 使用SDL2创建窗口 // 使用SDL2创建窗口
this->window_ = new Window(); this->window_ = new Window();
// 创建窗口会使用窗口配置 // 创建窗口会使用窗口配置
{
ScopedStartupTrace stageTrace("Window::create");
if (!window_->create(config_.windowConfig)) { if (!window_->create(config_.windowConfig)) {
SDL_Log("Failed to create window"); SDL_Log("Failed to create window");
return false; return false;
} }
}
window_->onResize([this](int width, int height) { window_->onResize([this](int width, int height) {
renderer_->setViewport(0, 0, width, height); renderer_->setViewport(0, 0, width, height);
@@ -165,25 +187,34 @@ bool Application::initCoreModules() {
// 初始化渲染器 // 初始化渲染器
renderer_ = &Renderer::get(); renderer_ = &Renderer::get();
{
ScopedStartupTrace stageTrace("Renderer::init");
if (!renderer_->init()) { if (!renderer_->init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer");
return false; return false;
} }
}
// 设置窗口清除颜色和视口 // 设置窗口清除颜色和视口
renderer_->setClearColor(0.0f, 0.0f, 0.0f); renderer_->setClearColor(0.0f, 0.0f, 0.0f);
renderer_->setViewport(0, 0, config_.windowConfig.width, config_.windowConfig.height); renderer_->setViewport(0, 0, config_.windowConfig.width, config_.windowConfig.height);
// 创建并设置相机 // 创建并设置相机
{
ScopedStartupTrace stageTrace("Camera setup");
camera_ = new Camera(); camera_ = new Camera();
camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height); camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height);
camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角 camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角
renderer_->setCamera(camera_); renderer_->setCamera(camera_);
}
{
ScopedStartupTrace stageTrace("TaskSystem::init");
if (!TaskSystem::get().init()) { if (!TaskSystem::get().init()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system");
return false; return false;
} }
}
return true; return true;
} }
@@ -215,6 +246,7 @@ void Application::run(StartCallback callback) {
callback(); callback();
} }
StartupTrace::mark("before Application::mainLoop");
mainLoop(); mainLoop();
running_ = false; running_ = false;
@@ -257,10 +289,7 @@ void Application::mainLoop() {
} }
TaskSystem::get().drainMainThreadTasks(); TaskSystem::get().drainMainThreadTasks();
if (!paused_) {
update(); update();
}
render(); render();
} }
@@ -383,10 +412,12 @@ std::unique_ptr<Event> Application::convertSDLEvent(const SDL_Event& sdlEvent) {
} }
void Application::dispatchEvent(const Event& event) { void Application::dispatchEvent(const Event& event) {
Scene* currentScene = SceneManager::get().GetCurrentScene(); if (paused_) {
if (currentScene) { SceneManager::get().DispatchUIEvent(event);
currentScene->OnEvent(event); return;
} }
SceneManager::get().DispatchEvent(event);
} }
void Application::update() { void Application::update() {
@@ -395,7 +426,10 @@ void Application::update() {
lastFrameTime_ = currentTime; lastFrameTime_ = currentTime;
totalTime_ += deltaTime_; totalTime_ += deltaTime_;
if (!paused_) {
SceneManager::get().Update(deltaTime_); SceneManager::get().Update(deltaTime_);
}
SceneManager::get().UpdateUI(deltaTime_);
frameCount_++; frameCount_++;
fpsTimer_ += deltaTime_; fpsTimer_ += deltaTime_;
@@ -407,6 +441,7 @@ void Application::update() {
} }
void Application::render() { void Application::render() {
static bool firstFramePresented = false;
Renderer& renderer = Renderer::get(); Renderer& renderer = Renderer::get();
renderer.beginFrame(); renderer.beginFrame();
@@ -416,6 +451,11 @@ void Application::render() {
if (window_) { if (window_) {
window_->swap(); window_->swap();
} }
if (!firstFramePresented) {
firstFramePresented = true;
StartupTrace::mark("first frame presented");
}
} }
const AppConfig& Application::getConfig() const { const AppConfig& Application::getConfig() const {

View File

@@ -93,10 +93,18 @@ void Renderer::clear(uint32_t flags) {
} }
void Renderer::setCamera(Camera* camera) { void Renderer::setCamera(Camera* camera) {
if (initialized_ && camera_ != camera) {
batch_.flush();
}
camera_ = camera; camera_ = camera;
if (camera) { if (camera) {
camera_->setViewport(viewportWidth_, viewportHeight_); camera_->setViewport(viewportWidth_, viewportHeight_);
} }
if (initialized_) {
updateUniforms();
}
} }
void Renderer::setupBlendMode(BlendMode mode) { void Renderer::setupBlendMode(BlendMode mode) {
@@ -159,6 +167,13 @@ void Renderer::updateUniforms() {
coloredShader->setMat4("u_view", camera_->getViewMatrix()); coloredShader->setMat4("u_view", camera_->getViewMatrix());
coloredShader->setMat4("u_projection", camera_->getProjectionMatrix()); coloredShader->setMat4("u_projection", camera_->getProjectionMatrix());
} }
auto* textShader = shaderManager_.getShader("text");
if (textShader) {
textShader->use();
textShader->setMat4("u_view", camera_->getViewMatrix());
textShader->setMat4("u_projection", camera_->getProjectionMatrix());
}
} }
} }

View File

@@ -0,0 +1,236 @@
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <SDL.h>
#include <algorithm>
#include <filesystem>
#include <frostbite2D/resource/asset.h>
namespace frostbite2D {
namespace {
namespace fs = std::filesystem;
fs::path toFsPath(const std::string& path) {
#ifdef _WIN32
return fs::u8path(path);
#else
return fs::path(path);
#endif
}
} // namespace
BinaryFileStreamReader::BinaryFileStreamReader(const std::string& filePath) {
open(filePath);
}
BinaryFileStreamReader::~BinaryFileStreamReader() {
close();
}
bool BinaryFileStreamReader::open(const std::string& filePath) {
close();
Asset& asset = Asset::get();
filePath_ = asset.resolvePath(filePath);
stream_.open(toFsPath(filePath_), std::ios::binary);
if (!stream_.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: unable to open file: %s",
filePath.c_str());
filePath_.clear();
return false;
}
stream_.seekg(0, std::ios::end);
std::streamoff endPos = stream_.tellg();
if (endPos < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: failed to determine file size: %s",
filePath_.c_str());
close();
return false;
}
size_ = static_cast<size_t>(endPos);
stream_.seekg(0, std::ios::beg);
position_ = 0;
lastReadCount_ = 0;
return true;
}
void BinaryFileStreamReader::close() {
if (stream_.is_open()) {
stream_.close();
}
filePath_.clear();
size_ = 0;
position_ = 0;
lastReadCount_ = 0;
}
bool BinaryFileStreamReader::isOpen() const {
return stream_.is_open();
}
size_t BinaryFileStreamReader::tell() const {
return position_;
}
void BinaryFileStreamReader::seek(size_t pos) {
if (!isOpen()) {
position_ = 0;
lastReadCount_ = 0;
return;
}
position_ = std::min(pos, size_);
lastReadCount_ = 0;
stream_.clear();
stream_.seekg(static_cast<std::streamoff>(position_), std::ios::beg);
}
void BinaryFileStreamReader::skip(size_t count) {
seek(position_ + count);
}
bool BinaryFileStreamReader::eof() const {
return position_ >= size_;
}
size_t BinaryFileStreamReader::size() const {
return size_;
}
size_t BinaryFileStreamReader::remaining() const {
return position_ >= size_ ? 0 : size_ - position_;
}
size_t BinaryFileStreamReader::lastReadCount() const {
return lastReadCount_;
}
size_t BinaryFileStreamReader::read(char* buffer, size_t size) {
if (!buffer || size == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return 0;
}
size_t bytesToRead = std::min(size, remaining());
stream_.clear();
stream_.read(buffer, static_cast<std::streamsize>(bytesToRead));
size_t bytesRead = static_cast<size_t>(stream_.gcount());
position_ += bytesRead;
lastReadCount_ = bytesRead;
if (bytesRead != bytesToRead) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BinaryFileStreamReader: incomplete read, expected %zu bytes, got %zu bytes from %s",
bytesToRead, bytesRead, filePath_.c_str());
}
return bytesRead;
}
std::vector<uint8> BinaryFileStreamReader::readBytes(size_t size) {
std::vector<uint8> result;
size_t bytesToRead = std::min(size, remaining());
if (bytesToRead == 0) {
lastReadCount_ = 0;
return result;
}
result.resize(bytesToRead);
size_t bytesRead = read(reinterpret_cast<char*>(result.data()), bytesToRead);
result.resize(bytesRead);
return result;
}
int8 BinaryFileStreamReader::readInt8() {
return read<int8>();
}
int16 BinaryFileStreamReader::readInt16() {
return read<int16>();
}
int32 BinaryFileStreamReader::readInt32() {
return read<int32>();
}
int64 BinaryFileStreamReader::readInt64() {
return read<int64>();
}
uint8 BinaryFileStreamReader::readUInt8() {
return read<uint8>();
}
uint16 BinaryFileStreamReader::readUInt16() {
return read<uint16>();
}
uint32 BinaryFileStreamReader::readUInt32() {
return read<uint32>();
}
uint64 BinaryFileStreamReader::readUInt64() {
return read<uint64>();
}
float BinaryFileStreamReader::readFloat() {
return read<float>();
}
double BinaryFileStreamReader::readDouble() {
return read<double>();
}
std::string BinaryFileStreamReader::readString(size_t length) {
if (length == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return {};
}
size_t bytesToRead = std::min(length, remaining());
std::string result(bytesToRead, '\0');
size_t bytesRead = read(result.data(), bytesToRead);
result.resize(bytesRead);
return result;
}
std::string BinaryFileStreamReader::readNullTerminatedString(size_t maxLength) {
if (maxLength == 0 || eof() || !isOpen()) {
lastReadCount_ = 0;
return {};
}
std::string result;
result.reserve(std::min(maxLength, remaining()));
size_t bytesRead = 0;
char ch = '\0';
while (bytesRead < maxLength && position_ < size_) {
stream_.clear();
if (!stream_.get(ch)) {
break;
}
++position_;
++bytesRead;
if (ch == '\0') {
lastReadCount_ = bytesRead;
return result;
}
result.push_back(ch);
}
lastReadCount_ = bytesRead;
return result;
}
} // namespace frostbite2D

View File

@@ -1,12 +1,108 @@
#include <frostbite2D/resource/npk_archive.h> #include <frostbite2D/resource/npk_archive.h>
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <system_error>
#include <SDL.h> #include <SDL.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D { namespace frostbite2D {
namespace { namespace {
namespace fs = std::filesystem;
constexpr char kIndexCacheFileName[] = ".frostbite_npk_index_cache.bin";
constexpr char kIndexCacheMagic[8] = {'F', 'B', 'N', 'P', 'K', 'I', 'D', 'X'};
constexpr uint32 kIndexCacheVersion = 2;
constexpr uint32 kMaxCacheStringLength = 4096;
constexpr uint32 kMaxCacheSourceFileCount = 20000;
constexpr uint32 kMaxCacheImageCount = 20000000;
fs::path toFsPath(const std::string& path) {
#ifdef _WIN32
return fs::u8path(path);
#else
return fs::path(path);
#endif
}
int64 toCacheTimestamp(const fs::file_time_type& value) {
auto duration =
std::chrono::duration_cast<std::chrono::nanoseconds>(
value.time_since_epoch())
.count();
return static_cast<int64>(duration);
}
bool appendBytes(std::vector<uint8>& buffer, const void* data, size_t size) {
if (size == 0) {
return true;
}
if (buffer.size() > buffer.max_size() - size) {
return false;
}
size_t oldSize = buffer.size();
buffer.resize(oldSize + size);
std::memcpy(buffer.data() + oldSize, data, size);
return true;
}
template <typename T>
bool appendPod(std::vector<uint8>& buffer, const T& value) {
return appendBytes(buffer, &value, sizeof(T));
}
template <typename T>
bool readPod(std::ifstream& stream, T& value) {
stream.read(reinterpret_cast<char*>(&value), sizeof(T));
return stream.good();
}
bool appendString(std::vector<uint8>& buffer, const std::string& value) {
if (value.size() > kMaxCacheStringLength) {
return false;
}
uint32 length = static_cast<uint32>(value.size());
if (!appendPod(buffer, length)) {
return false;
}
if (length == 0) {
return true;
}
return appendBytes(buffer, value.data(), length);
}
bool readString(std::ifstream& stream, std::string& value) {
uint32 length = 0;
if (!readPod(stream, length)) {
return false;
}
if (length > kMaxCacheStringLength) {
return false;
}
value.clear();
if (length == 0) {
return true;
}
value.resize(length);
stream.read(value.data(), length);
return stream.good();
}
const char* getZlibErrorName(int code) { const char* getZlibErrorName(int code) {
switch (code) { switch (code) {
case Z_OK: case Z_OK:
@@ -74,6 +170,7 @@ void NpkArchive::close() {
imgIndex_.clear(); imgIndex_.clear();
imageCache_.clear(); imageCache_.clear();
lruList_.clear(); lruList_.clear();
lruLookup_.clear();
currentCacheSize_ = 0; currentCacheSize_ = 0;
initialized_ = false; initialized_ = false;
} }
@@ -89,6 +186,291 @@ std::string NpkArchive::normalizePath(const std::string& path) const {
return result; return result;
} }
std::string NpkArchive::getCacheFilePath() const {
Asset& asset = Asset::get();
std::string relativePath =
asset.combinePath(imagePackDirectory_, kIndexCacheFileName);
return asset.resolvePath(relativePath);
}
NpkArchive::IndexCacheValidationMode NpkArchive::getIndexCacheValidationMode()
const {
#ifdef __SWITCH__
return IndexCacheValidationMode::TrustCache;
#else
return IndexCacheValidationMode::StrictSourceState;
#endif
}
std::vector<NpkArchive::SourceFileState> NpkArchive::collectSourceFileStates()
const {
Asset& asset = Asset::get();
std::string npkDir = asset.resolvePath(imagePackDirectory_);
std::vector<NpkArchive::SourceFileState> result;
if (!asset.isDirectory(npkDir)) {
return result;
}
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
result.reserve(files.size());
for (const auto& file : files) {
std::error_code error;
fs::path path = toFsPath(file);
if (!fs::is_regular_file(path, error)) {
continue;
}
SourceFileState state;
state.fileName = asset.getFileName(file);
state.size = static_cast<uint64>(fs::file_size(path, error));
if (error) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to read file size for %s",
file.c_str());
state.size = asset.getFileSize(file);
}
error.clear();
fs::file_time_type lastWriteTime = fs::last_write_time(path, error);
if (error) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to read write time for %s",
file.c_str());
state.writeTime = 0;
} else {
state.writeTime = toCacheTimestamp(lastWriteTime);
}
result.push_back(std::move(state));
}
std::sort(result.begin(), result.end(),
[](const SourceFileState& left, const SourceFileState& right) {
return left.fileName < right.fileName;
});
return result;
}
bool NpkArchive::loadIndexCache(
const std::vector<SourceFileState>& sourceFiles) {
Asset& asset = Asset::get();
std::string cachePath = getCacheFilePath();
if (!asset.isRegularFile(cachePath)) {
return false;
}
std::ifstream stream(toFsPath(cachePath), std::ios::binary);
if (!stream.is_open()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to open cache file %s",
cachePath.c_str());
return false;
}
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
uint32 version = 0;
uint32 cachedValidationMode = 0;
uint32 cachedSourceCount = 0;
uint32 cachedImageCount = 0;
{
ScopedStartupTrace stageTrace("NpkArchive cache header");
char magic[sizeof(kIndexCacheMagic)] = {};
stream.read(magic, sizeof(magic));
if (!stream.good() ||
std::memcmp(magic, kIndexCacheMagic, sizeof(kIndexCacheMagic)) != 0) {
SDL_Log("NpkArchive: cache magic mismatch, will rescan");
return false;
}
if (!readPod(stream, version) || !readPod(stream, cachedValidationMode) ||
!readPod(stream, cachedSourceCount) || !readPod(stream, cachedImageCount)) {
SDL_Log("NpkArchive: cache header incomplete, will rescan");
return false;
}
if (version != kIndexCacheVersion) {
SDL_Log("NpkArchive: cache version mismatch (%u != %u), will rescan",
version, kIndexCacheVersion);
return false;
}
if (cachedSourceCount > kMaxCacheSourceFileCount ||
cachedImageCount > kMaxCacheImageCount) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: cache counts look invalid (sources=%u, images=%u)",
cachedSourceCount, cachedImageCount);
return false;
}
if (cachedValidationMode != static_cast<uint32>(validationMode)) {
SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan",
cachedValidationMode, static_cast<uint32>(validationMode));
return false;
}
}
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
if (sourceFiles.empty()) {
SDL_Log("NpkArchive: strict cache validation requires source metadata, will rescan");
return false;
}
std::vector<SourceFileState> cachedSourceFiles;
{
ScopedStartupTrace stageTrace("NpkArchive cache sources");
cachedSourceFiles.reserve(cachedSourceCount);
for (uint32 i = 0; i < cachedSourceCount; ++i) {
SourceFileState state;
if (!readString(stream, state.fileName) || !readPod(stream, state.size) ||
!readPod(stream, state.writeTime)) {
SDL_Log("NpkArchive: cache source file section incomplete, will rescan");
return false;
}
cachedSourceFiles.push_back(std::move(state));
}
}
if (cachedSourceFiles != sourceFiles) {
SDL_Log("NpkArchive: cache invalidated by directory changes, will rescan");
return false;
}
} else if (cachedSourceCount != 0) {
SDL_Log("NpkArchive: trusted cache should not contain source metadata, will rescan");
return false;
}
std::unordered_map<std::string, ImgRef> cachedIndex;
{
ScopedStartupTrace stageTrace("NpkArchive cache images");
cachedIndex.reserve(cachedImageCount);
for (uint32 i = 0; i < cachedImageCount; ++i) {
std::string path;
std::string npkFile;
ImgRef img;
if (!readString(stream, path) || !readString(stream, npkFile) ||
!readPod(stream, img.frameCount) || !readPod(stream, img.offset) ||
!readPod(stream, img.size)) {
SDL_Log("NpkArchive: cache image section incomplete, will rescan");
return false;
}
img.path = path;
img.npkFile = std::move(npkFile);
img.loaded = false;
cachedIndex.emplace(std::move(path), std::move(img));
}
}
if (!stream.good() && !stream.eof()) {
SDL_Log("NpkArchive: cache read failed, will rescan");
return false;
}
{
ScopedStartupTrace stageTrace("NpkArchive cache commit");
imgIndex_.swap(cachedIndex);
}
SDL_Log("NpkArchive: loaded %zu image refs from cache %s", imgIndex_.size(),
cachePath.c_str());
return true;
}
bool NpkArchive::saveIndexCache(
const std::vector<SourceFileState>& sourceFiles) const {
std::string cachePath = getCacheFilePath();
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
uint32 sourceCount =
validationMode == IndexCacheValidationMode::StrictSourceState
? static_cast<uint32>(sourceFiles.size())
: 0;
uint32 imageCount = static_cast<uint32>(imgIndex_.size());
std::vector<uint8> buffer;
{
ScopedStartupTrace stageTrace("NpkArchive cache serialize");
size_t estimatedSize = sizeof(kIndexCacheMagic) + sizeof(uint32) * 4;
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
for (const auto& sourceFile : sourceFiles) {
estimatedSize += sizeof(uint32) + sourceFile.fileName.size();
estimatedSize += sizeof(sourceFile.size);
estimatedSize += sizeof(sourceFile.writeTime);
}
}
for (const auto& [path, img] : imgIndex_) {
estimatedSize += sizeof(uint32) + path.size();
estimatedSize += sizeof(uint32) + img.npkFile.size();
estimatedSize += sizeof(img.frameCount);
estimatedSize += sizeof(img.offset);
estimatedSize += sizeof(img.size);
}
buffer.reserve(estimatedSize);
if (!appendBytes(buffer, kIndexCacheMagic, sizeof(kIndexCacheMagic)) ||
!appendPod(buffer, kIndexCacheVersion) ||
!appendPod(buffer, static_cast<uint32>(validationMode)) ||
!appendPod(buffer, sourceCount) ||
!appendPod(buffer, imageCount)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache header %s",
cachePath.c_str());
return false;
}
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
for (const auto& sourceFile : sourceFiles) {
if (!appendString(buffer, sourceFile.fileName) ||
!appendPod(buffer, sourceFile.size) ||
!appendPod(buffer, sourceFile.writeTime)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache source data %s",
cachePath.c_str());
return false;
}
}
}
for (const auto& [path, img] : imgIndex_) {
if (!appendString(buffer, path) || !appendString(buffer, img.npkFile) ||
!appendPod(buffer, img.frameCount) || !appendPod(buffer, img.offset) ||
!appendPod(buffer, img.size)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to serialize cache image data %s",
cachePath.c_str());
return false;
}
}
}
{
ScopedStartupTrace stageTrace("NpkArchive cache flush");
std::ofstream stream(toFsPath(cachePath),
std::ios::binary | std::ios::trunc);
if (!stream.is_open()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: failed to create cache file %s",
cachePath.c_str());
return false;
}
if (!buffer.empty()) {
stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
}
if (!stream.good()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: cache flush failed %s", cachePath.c_str());
return false;
}
}
SDL_Log("NpkArchive: wrote index cache %s (mode=%u, %u source files, %u images)",
cachePath.c_str(), static_cast<uint32>(validationMode), sourceCount,
imageCount);
return true;
}
void NpkArchive::scanNpkFiles() { void NpkArchive::scanNpkFiles() {
Asset& asset = Asset::get(); Asset& asset = Asset::get();
std::string npkDir = asset.resolvePath(imagePackDirectory_); std::string npkDir = asset.resolvePath(imagePackDirectory_);
@@ -97,16 +479,58 @@ void NpkArchive::scanNpkFiles() {
return; return;
} }
imgIndex_.clear();
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
std::vector<SourceFileState> sourceFiles;
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
{
ScopedStartupTrace stageTrace("NpkArchive collect source states");
sourceFiles = collectSourceFileStates();
}
if (sourceFiles.empty()) {
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
return;
}
{
ScopedStartupTrace stageTrace("NpkArchive cache strict-load");
if (loadIndexCache(sourceFiles)) {
return;
}
}
} else {
{
ScopedStartupTrace stageTrace("NpkArchive cache fast-load");
if (loadIndexCache(sourceFiles)) {
return;
}
}
}
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk"); std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
std::sort(files.begin(), files.end());
if (files.empty()) {
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
return;
}
SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size())); SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
for (const auto &file : files) { {
ScopedStartupTrace stageTrace("NpkArchive parse files");
for (const auto& file : files) {
parseNpkFile(file); parseNpkFile(file);
} }
}
{
ScopedStartupTrace stageTrace("NpkArchive cache save");
saveIndexCache(sourceFiles);
}
} }
bool NpkArchive::parseNpkFile(const std::string& npkPath) { bool NpkArchive::parseNpkFile(const std::string& npkPath) {
Asset& asset = Asset::get(); Asset& asset = Asset::get();
BinaryReader reader(npkPath); BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) { if (!reader.isOpen()) {
SDL_Log("Failed to open NPK file: %s", npkPath.c_str()); SDL_Log("Failed to open NPK file: %s", npkPath.c_str());
@@ -158,6 +582,7 @@ std::vector<std::string> NpkArchive::listImgs() const {
for (const auto& pair : imgIndex_) { for (const auto& pair : imgIndex_) {
result.push_back(pair.first); result.push_back(pair.first);
} }
std::sort(result.begin(), result.end());
return result; return result;
} }
@@ -203,7 +628,7 @@ bool NpkArchive::loadImgData(ImgRef& img) {
std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile); std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile);
npkPath = asset.resolvePath(npkPath); npkPath = asset.resolvePath(npkPath);
BinaryReader reader(npkPath); BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) { if (!reader.isOpen()) {
SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str()); SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str());
return false; return false;
@@ -290,19 +715,26 @@ bool NpkArchive::loadImgData(ImgRef& img) {
frame.size == static_cast<uint32>(deSize)) { frame.size == static_cast<uint32>(deSize)) {
frame.data = std::move(compressedData); frame.data = std::move(compressedData);
cachedData.memoryUsage += frame.data.size(); cachedData.memoryUsage += frame.data.size();
if (verboseFallbackLog_) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, " "NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
"type=%d, compression=%d, size=%dx%d", "type=%d, compression=%d, size=%dx%d",
img.path.c_str(), i, frame.type, frame.compressionType, img.path.c_str(), i, frame.type, frame.compressionType,
frame.width, frame.height); frame.width, frame.height);
}
continue; continue;
} }
if (verboseFallbackLog_) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to uncompress image data: %d (%s), img=%s, npk=%s, frame=%d, " "Failed to uncompress image data: %d (%s), img=%s, "
"type=%d, compression=%d, size=%dx%d, offset=%u, packed=%u, expected=%d", "npk=%s, frame=%d, "
uncompressResult, getZlibErrorName(uncompressResult), img.path.c_str(), "type=%d, compression=%d, size=%dx%d, offset=%u, "
img.npkFile.c_str(), i, frame.type, frame.compressionType, frame.width, "packed=%u, expected=%d",
frame.height, frame.offset, frame.size, deSize); uncompressResult, getZlibErrorName(uncompressResult),
img.path.c_str(), img.npkFile.c_str(), i, frame.type,
frame.compressionType, frame.width, frame.height,
frame.offset, frame.size, deSize);
}
continue; continue;
} }
@@ -325,9 +757,16 @@ bool NpkArchive::loadImgData(ImgRef& img) {
cachedData.memoryUsage += frame.data.size(); cachedData.memoryUsage += frame.data.size();
} }
imageCache_[img.path] = std::move(cachedData); auto existingCacheIt = imageCache_.find(img.path);
currentCacheSize_ += imageCache_[img.path].memoryUsage; if (existingCacheIt != imageCache_.end()) {
lruList_.push_front(img.path); currentCacheSize_ -= existingCacheIt->second.memoryUsage;
imageCache_.erase(existingCacheIt);
}
evictCacheIfNeeded(cachedData.memoryUsage);
auto cacheIt = imageCache_.emplace(img.path, std::move(cachedData)).first;
currentCacheSize_ += cacheIt->second.memoryUsage;
updateCacheUsage(img.path);
img.loaded = true; img.loaded = true;
imgIndex_[img.path] = img; imgIndex_[img.path] = img;
@@ -372,6 +811,7 @@ void NpkArchive::setCacheSize(size_t maxBytes) {
void NpkArchive::clearCache() { void NpkArchive::clearCache() {
imageCache_.clear(); imageCache_.clear();
lruList_.clear(); lruList_.clear();
lruLookup_.clear();
currentCacheSize_ = 0; currentCacheSize_ = 0;
} }
@@ -383,6 +823,7 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) { while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
std::string oldest = lruList_.back(); std::string oldest = lruList_.back();
lruList_.pop_back(); lruList_.pop_back();
lruLookup_.erase(oldest);
auto it = imageCache_.find(oldest); auto it = imageCache_.find(oldest);
if (it != imageCache_.end()) { if (it != imageCache_.end()) {
@@ -393,11 +834,12 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
} }
void NpkArchive::updateCacheUsage(const std::string& imgPath) { void NpkArchive::updateCacheUsage(const std::string& imgPath) {
auto it = std::find(lruList_.begin(), lruList_.end(), imgPath); auto lruIt = lruLookup_.find(imgPath);
if (it != lruList_.end()) { if (lruIt != lruLookup_.end()) {
lruList_.erase(it); lruList_.erase(lruIt->second);
} }
lruList_.push_front(imgPath); lruList_.push_front(imgPath);
lruLookup_[imgPath] = lruList_.begin();
auto cacheIt = imageCache_.find(imgPath); auto cacheIt = imageCache_.find(imgPath);
if (cacheIt != imageCache_.end()) { if (cacheIt != imageCache_.end()) {
@@ -405,7 +847,7 @@ void NpkArchive::updateCacheUsage(const std::string& imgPath) {
} }
} }
std::string NpkArchive::readNpkInfoString(BinaryReader& reader) { std::string NpkArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
if (reader.eof()) { if (reader.eof()) {
return ""; return "";
} }

View File

@@ -1,11 +1,47 @@
#include <frostbite2D/resource/pvf_archive.h> #include <frostbite2D/resource/pvf_archive.h>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <SDL.h> #include <SDL.h>
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <sstream>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D { namespace frostbite2D {
namespace {
bool canReadSpan(const std::vector<uint8>& bytes, size_t offset, size_t length) {
return offset <= bytes.size() && length <= bytes.size() - offset;
}
template <typename T>
bool readPodAt(const std::vector<uint8>& bytes, size_t offset, T& value) {
if (!canReadSpan(bytes, offset, sizeof(T))) {
value = T{};
return false;
}
std::memcpy(&value, bytes.data() + offset, sizeof(T));
return true;
}
std::string readStringAt(const std::vector<uint8>& bytes, size_t offset,
size_t length) {
if (!canReadSpan(bytes, offset, length)) {
return {};
}
return std::string(reinterpret_cast<const char*>(bytes.data() + offset),
length);
}
} // namespace
PvfArchive& PvfArchive::get() { PvfArchive& PvfArchive::get() {
static PvfArchive instance; static PvfArchive instance;
return instance; return instance;
@@ -14,8 +50,10 @@ PvfArchive& PvfArchive::get() {
bool PvfArchive::open(const std::string& filePath) { bool PvfArchive::open(const std::string& filePath) {
close(); close();
ScopedStartupTrace stageTrace("PvfArchive::open stream");
if (!reader_.open(filePath)) { if (!reader_.open(filePath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 无法打开文件: %s", filePath.c_str()); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: unable to open file: %s", filePath.c_str());
return false; return false;
} }
@@ -24,10 +62,7 @@ bool PvfArchive::open(const std::string& filePath) {
void PvfArchive::close() { void PvfArchive::close() {
reader_.close(); reader_.close();
dataStartPos_ = 0; clearInitData();
fileInfo_.clear();
binStringTable_.clear();
loadStrings_.clear();
} }
bool PvfArchive::isOpen() const { bool PvfArchive::isOpen() const {
@@ -36,16 +71,39 @@ bool PvfArchive::isOpen() const {
void PvfArchive::init() { void PvfArchive::init() {
if (!isOpen()) { if (!isOpen()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 请先打开文件再初始化"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: open the archive before calling init");
return; return;
} }
clearInitData();
{
ScopedStartupTrace stageTrace("PvfArchive::initHeader");
initHeader(); initHeader();
}
if (fileInfo_.empty()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: header parse produced no file entries");
return;
}
{
ScopedStartupTrace stageTrace("PvfArchive::initBinStringTable");
initBinStringTable(); initBinStringTable();
}
{
ScopedStartupTrace stageTrace("PvfArchive::initLoadStrings");
initLoadStrings(); initLoadStrings();
}
} }
void PvfArchive::initHeader() { void PvfArchive::initHeader() {
fileInfo_.clear();
dataStartPos_ = 0;
decodedFileCache_.clear();
if (!isOpen()) { if (!isOpen()) {
return; return;
} }
@@ -53,7 +111,13 @@ void PvfArchive::initHeader() {
reader_.seek(0); reader_.seek(0);
int32 uuidLength = reader_.readInt32(); int32 uuidLength = reader_.readInt32();
std::string uuid = reader_.readString(uuidLength); if (uuidLength < 0 || static_cast<size_t>(uuidLength) > reader_.remaining()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid uuid length in archive header");
return;
}
std::string uuid = reader_.readString(static_cast<size_t>(uuidLength));
(void)uuid; (void)uuid;
int32 version = reader_.readInt32(); int32 version = reader_.readInt32();
@@ -63,126 +127,201 @@ void PvfArchive::initHeader() {
int32 indexHeaderCrc = reader_.readInt32(); int32 indexHeaderCrc = reader_.readInt32();
int32 indexSize = reader_.readInt32(); int32 indexSize = reader_.readInt32();
size_t firstPos = reader_.tell(); if (alignedIndexHeaderSize <= 0 || indexSize < 0) {
reader_.crcDecode(alignedIndexHeaderSize, indexHeaderCrc); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid index header metadata (size=%d, count=%d)",
alignedIndexHeaderSize, indexSize);
return;
}
dataStartPos_ = alignedIndexHeaderSize + 56; size_t indexHeaderPos = reader_.tell();
size_t currPos = 0; auto headerBytesOpt =
readArchiveBytes(indexHeaderPos, static_cast<size_t>(alignedIndexHeaderSize));
if (!headerBytesOpt.has_value()) {
return;
}
std::vector<uint8> headerBytes = std::move(headerBytesOpt.value());
crcDecodeBuffer(headerBytes, static_cast<uint32>(indexHeaderCrc));
dataStartPos_ = indexHeaderPos + static_cast<size_t>(alignedIndexHeaderSize);
size_t cursor = 0;
for (int32 i = 0; i < indexSize; ++i) { for (int32 i = 0; i < indexSize; ++i) {
reader_.seek(firstPos + currPos); int32 fileNumber = 0;
int32 filePathLength = 0;
int32 fileLength = 0;
int32 crc32 = 0;
int32 relativeOffset = 0;
int32 fileNumber = reader_.readInt32(); if (!readPodAt(headerBytes, cursor, fileNumber) ||
!readPodAt(headerBytes, cursor + 4, filePathLength)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated index header at entry %d", i);
break;
}
(void)fileNumber; (void)fileNumber;
cursor += 8;
int32 filePathLength = reader_.readInt32(); if (filePathLength < 0) {
std::string fileName = normalizePath(reader_.readString(filePathLength)); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: negative path length in index entry %d", i);
break;
}
int32 fileLength = reader_.readInt32(); size_t pathLength = static_cast<size_t>(filePathLength);
int32 crc32 = reader_.readInt32(); if (!canReadSpan(headerBytes, cursor, pathLength + 12)) {
int32 relativeOffset = reader_.readInt32(); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated file entry payload at index %d", i);
break;
}
if (fileLength > 0) { std::string fileName =
int32 realFileLength = (fileLength + 3) & 0xFFFFFFFC; normalizePath(readStringAt(headerBytes, cursor, pathLength));
cursor += pathLength;
readPodAt(headerBytes, cursor, fileLength);
readPodAt(headerBytes, cursor + 4, crc32);
readPodAt(headerBytes, cursor + 8, relativeOffset);
cursor += 12;
if (fileLength > 0 && relativeOffset >= 0) {
uint32 alignedLength = (static_cast<uint32>(fileLength) + 3u) & 0xFFFFFFFCu;
PvfFileInfo info; PvfFileInfo info;
info.offset = relativeOffset; info.offset = static_cast<size_t>(relativeOffset);
info.crc32 = static_cast<uint32>(crc32); info.crc32 = static_cast<uint32>(crc32);
info.length = realFileLength; info.length = static_cast<size_t>(alignedLength);
info.decoded = false; info.decoded = false;
fileInfo_[fileName] = info; fileInfo_[fileName] = info;
} }
currPos += 20;
currPos += filePathLength;
} }
} }
void PvfArchive::initBinStringTable() { void PvfArchive::initBinStringTable() {
std::string tablePath = "stringtable.bin"; binStringTable_.clear();
auto infoOpt = getFileInfo(tablePath);
auto infoOpt = getFileInfo("stringtable.bin");
if (!infoOpt.has_value()) { if (!infoOpt.has_value()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: stringtable.bin 文件不存在"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: stringtable.bin was not found");
return; return;
} }
PvfFileInfo info = infoOpt.value(); auto bytesOpt = readDecodedFileBytes(infoOpt.value());
reader_.seek(dataStartPos_ + info.offset); if (!bytesOpt.has_value()) {
reader_.crcDecode(info.length, info.crc32); return;
reader_.seek(dataStartPos_ + info.offset); }
size_t fileHeaderPos = reader_.tell(); const std::vector<uint8>& bytes = bytesOpt.value();
int32 count = reader_.readInt32(); int32 count = 0;
if (!readPodAt(bytes, 0, count) || count < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid stringtable.bin header");
return;
}
for (int32 i = 0; i < count; ++i) { for (int32 i = 0; i < count; ++i) {
reader_.seek(fileHeaderPos + i * 4 + 4); size_t offsetsPos = static_cast<size_t>(i) * 4 + 4;
int32 startPos = reader_.readInt32(); int32 startPos = 0;
int32 endPos = reader_.readInt32(); int32 endPos = 0;
int32 len = endPos - startPos; if (!readPodAt(bytes, offsetsPos, startPos) ||
!readPodAt(bytes, offsetsPos + 4, endPos)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: truncated string table at entry %d", i);
break;
}
reader_.seek(fileHeaderPos + startPos + 4); if (startPos < 0 || endPos < startPos) {
std::string str = reader_.readString(len); continue;
binStringTable_[i] = str; }
size_t stringPos = static_cast<size_t>(startPos) + 4;
size_t length = static_cast<size_t>(endPos - startPos);
if (!canReadSpan(bytes, stringPos, length)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: invalid string span at entry %d", i);
continue;
}
binStringTable_[i] = readStringAt(bytes, stringPos, length);
} }
} }
void PvfArchive::initLoadStrings() { void PvfArchive::initLoadStrings() {
std::string lstPath = "n_string.lst"; loadStrings_.clear();
auto infoOpt = getFileInfo(lstPath);
auto infoOpt = getFileInfo("n_string.lst");
if (!infoOpt.has_value()) { if (!infoOpt.has_value()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: n_string.lst 文件不存在"); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: n_string.lst was not found");
return; return;
} }
PvfFileInfo info = infoOpt.value(); auto listBytesOpt = readDecodedFileBytes(infoOpt.value());
reader_.seek(dataStartPos_ + info.offset); if (!listBytesOpt.has_value()) {
reader_.crcDecode(info.length, info.crc32); return;
reader_.seek(dataStartPos_ + info.offset); }
size_t fileHeaderPos = reader_.tell(); const std::vector<uint8>& listBytes = listBytesOpt.value();
int16 flag = reader_.readInt16(); if (listBytes.size() < sizeof(int16)) {
(void)flag; SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: n_string.lst is too small");
return;
}
size_t i = 2; std::map<std::string, std::string> stringFileCache;
while (i < info.length) { size_t cursor = sizeof(int16);
if ((info.length - i) >= 10) { while (cursor + 10 <= listBytes.size()) {
reader_.seek(fileHeaderPos + i + 6); int32 findKey = 0;
int32 findKey = reader_.readInt32(); if (!readPodAt(listBytes, cursor + 6, findKey)) {
break;
}
auto binStrOpt = getBinString(findKey); auto binStrOpt = getBinString(findKey);
if (!binStrOpt.has_value()) { if (!binStrOpt.has_value()) {
i += 10; cursor += 10;
continue;
}
std::string key = normalizePath(binStrOpt.value());
if (key.empty()) {
cursor += 10;
continue; continue;
} }
std::string key = binStrOpt.value();
size_t slashPos = key.find('/'); size_t slashPos = key.find('/');
std::string type = (slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos)) : ""; std::string type =
(slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos))
: std::string();
if (!key.empty()) { std::string content;
auto fileInfoOpt = getFileInfo(key); auto cacheIt = stringFileCache.find(key);
if (fileInfoOpt.has_value()) { if (cacheIt != stringFileCache.end()) {
auto contentOpt = getFileContent(key); content = cacheIt->second;
if (contentOpt.has_value()) { } else {
std::string content = contentOpt.value(); auto fileIt = fileInfo_.find(key);
if (fileIt != fileInfo_.end()) {
auto contentBytesOpt = readDecodedFileBytes(fileIt->second);
if (contentBytesOpt.has_value()) {
const std::vector<uint8>& contentBytes = contentBytesOpt.value();
content.assign(reinterpret_cast<const char*>(contentBytes.data()),
contentBytes.size());
stringFileCache[key] = content;
}
}
}
if (!content.empty()) {
auto& table = loadStrings_[type];
std::vector<std::string> lines = splitString(content, "\n"); std::vector<std::string> lines = splitString(content, "\n");
for (const auto& line : lines) { for (const auto& line : lines) {
size_t gtPos = line.find('>'); size_t gtPos = line.find('>');
if (gtPos != std::string::npos && gtPos + 1 < line.length()) { if (gtPos != std::string::npos && gtPos + 1 < line.length()) {
std::string strKey = line.substr(0, gtPos); table[line.substr(0, gtPos)] = line.substr(gtPos + 1);
std::string strValue = line.substr(gtPos + 1);
loadStrings_[type][strKey] = strValue;
} }
} }
} }
}
} cursor += 10;
} else {
break;
}
i += 10;
} }
} }
@@ -216,12 +355,19 @@ std::optional<std::string> PvfArchive::getFileContent(const std::string& path) {
} }
PvfFileInfo& info = it->second; PvfFileInfo& info = it->second;
if (!decodeFile(info)) { if (!decodeFile(normalizedPath, info)) {
return std::nullopt; return std::nullopt;
} }
reader_.seek(dataStartPos_ + info.offset); auto cacheIt = decodedFileCache_.find(normalizedPath);
std::vector<uint8> bytes = reader_.readBytes(info.length); if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
const std::vector<uint8>& bytes = cacheIt->second;
if (bytes.empty()) {
return std::string();
}
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size()); return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
} }
@@ -233,12 +379,16 @@ std::optional<std::vector<uint8>> PvfArchive::getFileBytes(const std::string& pa
} }
PvfFileInfo& info = it->second; PvfFileInfo& info = it->second;
if (!decodeFile(info)) { if (!decodeFile(normalizedPath, info)) {
return std::nullopt; return std::nullopt;
} }
reader_.seek(dataStartPos_ + info.offset); auto cacheIt = decodedFileCache_.find(normalizedPath);
return reader_.readBytes(info.length); if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
return cacheIt->second;
} }
std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) { std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
@@ -249,17 +399,22 @@ std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
} }
PvfFileInfo& info = it->second; PvfFileInfo& info = it->second;
if (!decodeFile(info)) { if (!decodeFile(normalizedPath, info)) {
return std::nullopt; return std::nullopt;
} }
reader_.seek(dataStartPos_ + info.offset); auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt == decodedFileCache_.end()) {
return std::nullopt;
}
const std::vector<uint8>& bytes = cacheIt->second;
RawData result; RawData result;
result.size = info.length; result.size = bytes.size();
result.data = std::make_unique<char[]>(info.length); result.data = std::make_unique<char[]>(bytes.size());
reader_.read(result.data.get(), info.length); if (!bytes.empty()) {
std::memcpy(result.data.get(), bytes.data(), bytes.size());
}
return result; return result;
} }
@@ -271,7 +426,8 @@ std::optional<std::string> PvfArchive::getBinString(int key) const {
return std::nullopt; return std::nullopt;
} }
std::optional<std::string> PvfArchive::getLoadString(const std::string& type, const std::string& key) const { std::optional<std::string> PvfArchive::getLoadString(const std::string& type,
const std::string& key) const {
std::string normalizedType = normalizePath(type); std::string normalizedType = normalizePath(type);
auto typeIt = loadStrings_.find(normalizedType); auto typeIt = loadStrings_.find(normalizedType);
if (typeIt != loadStrings_.end()) { if (typeIt != loadStrings_.end()) {
@@ -287,7 +443,8 @@ bool PvfArchive::hasBinString(int key) const {
return binStringTable_.find(key) != binStringTable_.end(); return binStringTable_.find(key) != binStringTable_.end();
} }
bool PvfArchive::hasLoadString(const std::string& type, const std::string& key) const { bool PvfArchive::hasLoadString(const std::string& type,
const std::string& key) const {
std::string normalizedType = normalizePath(type); std::string normalizedType = normalizePath(type);
auto typeIt = loadStrings_.find(normalizedType); auto typeIt = loadStrings_.find(normalizedType);
if (typeIt != loadStrings_.end()) { if (typeIt != loadStrings_.end()) {
@@ -334,7 +491,8 @@ std::string PvfArchive::normalizePath(const std::string& path) const {
return result; return result;
} }
std::string PvfArchive::resolvePath(const std::string& baseDir, const std::string& path) const { std::string PvfArchive::resolvePath(const std::string& baseDir,
const std::string& path) const {
if (path.empty()) { if (path.empty()) {
return {}; return {};
} }
@@ -345,9 +503,11 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin
[](unsigned char c) { return static_cast<char>(std::tolower(c)); }); [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
size_t slashPos = rawPath.find('/'); size_t slashPos = rawPath.find('/');
std::string root = slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos); std::string root =
static const char* kLogicalRoots[] = {"map", "sprite", "town", "sound", "audio", slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
"monster", "character", "ui"}; static const char* kLogicalRoots[] = {"map", "sprite", "town",
"sound", "audio", "monster",
"character", "ui"};
for (const char* logicalRoot : kLogicalRoots) { for (const char* logicalRoot : kLogicalRoots) {
if (root == logicalRoot) { if (root == logicalRoot) {
return normalizePath(rawPath); return normalizePath(rawPath);
@@ -361,7 +521,8 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin
return normalizePath(normalizedBaseDir + rawPath); return normalizePath(normalizedBaseDir + rawPath);
} }
std::vector<std::string> PvfArchive::splitString(const std::string& str, const std::string& delimiter) const { std::vector<std::string> PvfArchive::splitString(
const std::string& str, const std::string& delimiter) const {
std::vector<std::string> tokens; std::vector<std::string> tokens;
size_t pos = 0; size_t pos = 0;
size_t found; size_t found;
@@ -378,16 +539,94 @@ std::vector<std::string> PvfArchive::splitString(const std::string& str, const s
return tokens; return tokens;
} }
bool PvfArchive::decodeFile(PvfFileInfo& info) { bool PvfArchive::decodeFile(const std::string& normalizedPath,
if (info.decoded) { PvfFileInfo& info) {
auto cacheIt = decodedFileCache_.find(normalizedPath);
if (cacheIt != decodedFileCache_.end()) {
info.decoded = true;
return true; return true;
} }
reader_.seek(dataStartPos_ + info.offset); auto bytesOpt = readDecodedFileBytes(info);
reader_.crcDecode(info.length, info.crc32); if (!bytesOpt.has_value()) {
info.decoded = true; return false;
}
decodedFileCache_[normalizedPath] = std::move(bytesOpt.value());
info.decoded = true;
return true; return true;
} }
void PvfArchive::clearInitData() {
dataStartPos_ = 0;
fileInfo_.clear();
binStringTable_.clear();
loadStrings_.clear();
decodedFileCache_.clear();
}
std::optional<std::vector<uint8>> PvfArchive::readArchiveBytes(
size_t absoluteOffset, size_t length) {
if (!isOpen()) {
return std::nullopt;
}
if (length == 0) {
return std::vector<uint8>();
}
if (absoluteOffset > reader_.size() ||
length > reader_.size() - absoluteOffset) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: attempted to read beyond archive bounds (offset=%zu, size=%zu, archive=%zu)",
absoluteOffset, length, reader_.size());
return std::nullopt;
}
reader_.seek(absoluteOffset);
std::vector<uint8> bytes = reader_.readBytes(length);
if (bytes.size() != length) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PvfArchive: short read at offset %zu (expected %zu bytes, got %zu)",
absoluteOffset, length, bytes.size());
return std::nullopt;
}
return bytes;
}
std::optional<std::vector<uint8>> PvfArchive::readDecodedFileBytes(
const PvfFileInfo& info) {
auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length);
if (!bytesOpt.has_value()) {
return std::nullopt;
}
std::vector<uint8> bytes = std::move(bytesOpt.value());
crcDecodeBuffer(bytes, info.crc32);
return bytes;
}
void PvfArchive::crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32) {
if (data.empty()) {
return;
}
const uint32 key = 0x81A79011u;
for (size_t i = 0; i + 3 < data.size(); i += 4) {
uint32 value = static_cast<uint32>(data[i]) |
(static_cast<uint32>(data[i + 1]) << 8) |
(static_cast<uint32>(data[i + 2]) << 16) |
(static_cast<uint32>(data[i + 3]) << 24);
uint32 decoded = (value ^ key ^ crc32);
decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu);
data[i] = static_cast<uint8>((decoded >> 0) & 0xFFu);
data[i + 1] = static_cast<uint8>((decoded >> 8) & 0xFFu);
data[i + 2] = static_cast<uint8>((decoded >> 16) & 0xFFu);
data[i + 3] = static_cast<uint8>((decoded >> 24) & 0xFFu);
}
}
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -1,4 +1,5 @@
#include <frostbite2D/resource/sound_pack_archive.h> #include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/resource/binary_file_stream_reader.h>
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <SDL.h> #include <SDL.h>
@@ -75,7 +76,7 @@ void SoundPackArchive::scanNpkFiles() {
bool SoundPackArchive::parseNpkFile(const std::string& npkPath) { bool SoundPackArchive::parseNpkFile(const std::string& npkPath) {
Asset& asset = Asset::get(); Asset& asset = Asset::get();
BinaryReader reader(npkPath); BinaryFileStreamReader reader(npkPath);
if (!reader.isOpen()) { if (!reader.isOpen()) {
SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str()); SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str());
@@ -230,7 +231,7 @@ void SoundPackArchive::updateCacheUsage(const std::string& audioPath) {
} }
} }
std::string SoundPackArchive::readNpkInfoString(BinaryReader& reader) { std::string SoundPackArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
if (reader.eof()) { if (reader.eof()) {
return ""; return "";
} }

View File

@@ -6,9 +6,7 @@ namespace frostbite2D {
Scene* Scene::current_ = nullptr; Scene* Scene::current_ = nullptr;
Scene::Scene() { Scene::Scene() = default;
current_ = this;
}
Scene::~Scene() { Scene::~Scene() {
} }

View File

@@ -1,6 +1,7 @@
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <SDL2/SDL.h> #include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D { namespace frostbite2D {
@@ -9,8 +10,6 @@ SceneManager& SceneManager::get() {
return instance; return instance;
} }
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用
void SceneManager::PushScene(Ptr<Scene> scene) { void SceneManager::PushScene(Ptr<Scene> scene) {
if (!scene) { if (!scene) {
return; return;
@@ -21,6 +20,7 @@ void SceneManager::PushScene(Ptr<Scene> scene) {
} }
sceneStack_.push_back(scene); sceneStack_.push_back(scene);
Scene::current_ = scene.Get();
scene->onEnter(); scene->onEnter();
} }
@@ -32,6 +32,7 @@ void SceneManager::PopScene() {
sceneStack_.back()->onExit(); sceneStack_.back()->onExit();
sceneStack_.pop_back(); sceneStack_.pop_back();
Scene::current_ = sceneStack_.empty() ? nullptr : sceneStack_.back().Get();
if (!sceneStack_.empty()) { if (!sceneStack_.empty()) {
sceneStack_.back()->onEnter(); sceneStack_.back()->onEnter();
} }
@@ -42,15 +43,82 @@ void SceneManager::ReplaceScene(Ptr<Scene> scene) {
return; return;
} }
PopScene(); if (!sceneStack_.empty()) {
PushScene(scene); sceneStack_.back()->onExit();
sceneStack_.pop_back();
}
sceneStack_.push_back(scene);
Scene::current_ = scene.Get();
scene->onEnter();
}
void SceneManager::PushUIScene(Ptr<UIScene> scene) {
if (!scene) {
return;
}
uiSceneStack_.push_back(scene);
scene->onEnter();
}
void SceneManager::PopUIScene() {
if (uiSceneStack_.empty()) {
return;
}
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
void SceneManager::ReplaceUIScene(Ptr<UIScene> scene) {
if (!scene) {
return;
}
if (!uiSceneStack_.empty()) {
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
uiSceneStack_.push_back(scene);
scene->onEnter();
}
bool SceneManager::RemoveUIScene(UIScene* scene) {
if (!scene) {
return false;
}
for (auto it = uiSceneStack_.begin(); it != uiSceneStack_.end(); ++it) {
if (it->Get() != scene) {
continue;
}
(*it)->onExit();
uiSceneStack_.erase(it);
return true;
}
return false;
}
void SceneManager::ClearUIScenes() {
while (!uiSceneStack_.empty()) {
uiSceneStack_.back()->onExit();
uiSceneStack_.pop_back();
}
} }
void SceneManager::ClearAll() { void SceneManager::ClearAll() {
ClearUIScenes();
while (!sceneStack_.empty()) { while (!sceneStack_.empty()) {
sceneStack_.back()->onExit(); sceneStack_.back()->onExit();
sceneStack_.pop_back(); sceneStack_.pop_back();
} }
Scene::current_ = nullptr;
} }
void SceneManager::Update(float deltaTime) { void SceneManager::Update(float deltaTime) {
@@ -59,10 +127,53 @@ void SceneManager::Update(float deltaTime) {
} }
} }
void SceneManager::UpdateUI(float deltaTime) {
for (auto& scene : uiSceneStack_) {
if (scene) {
scene->Update(deltaTime);
}
}
}
void SceneManager::Render() { void SceneManager::Render() {
Renderer& renderer = Renderer::get();
Camera* worldCamera = renderer.getCamera();
if (!sceneStack_.empty()) { if (!sceneStack_.empty()) {
sceneStack_.back()->Render(); sceneStack_.back()->Render();
} }
for (auto& scene : uiSceneStack_) {
if (!scene) {
continue;
}
renderer.setCamera(scene->GetCamera());
scene->Render();
}
if (renderer.getCamera() != worldCamera) {
renderer.setCamera(worldCamera);
}
}
bool SceneManager::DispatchEvent(const Event& event) {
if (DispatchUIEvent(event)) {
return true;
}
Scene* currentScene = GetCurrentScene();
return currentScene ? currentScene->OnEvent(event) : false;
}
bool SceneManager::DispatchUIEvent(const Event& event) {
for (auto it = uiSceneStack_.rbegin(); it != uiSceneStack_.rend(); ++it) {
if (*it && (*it)->OnEvent(event)) {
return true;
}
}
return false;
} }
Scene* SceneManager::GetCurrentScene() const { Scene* SceneManager::GetCurrentScene() const {
@@ -72,9 +183,19 @@ Scene* SceneManager::GetCurrentScene() const {
return sceneStack_.back().Get(); return sceneStack_.back().Get();
} }
UIScene* SceneManager::GetCurrentUIScene() const {
if (uiSceneStack_.empty()) {
return nullptr;
}
return uiSceneStack_.back().Get();
}
bool SceneManager::HasActiveScene() const { bool SceneManager::HasActiveScene() const {
return !sceneStack_.empty(); return !sceneStack_.empty();
} }
bool SceneManager::HasActiveUIScene() const {
return !uiSceneStack_.empty();
} }
} // namespace frostbite2D

View File

@@ -0,0 +1,11 @@
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D {
UIScene::UIScene() {
camera_.setFlipY(true);
camera_.setZoom(1.0f);
camera_.setPosition(Vec2::Zero());
}
} // namespace frostbite2D

View File

@@ -8,6 +8,7 @@
#include <functional> #include <functional>
#include <map> #include <map>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
namespace frostbite2D { namespace frostbite2D {
@@ -44,6 +45,7 @@ private:
Animation* GetPrimaryAnimation(const std::string& actionName) const; Animation* GetPrimaryAnimation(const std::string& actionName) const;
Animation* GetCurrentPrimaryAnimation() const; Animation* GetCurrentPrimaryAnimation() const;
void RefreshRuntimeCallbacks(); void RefreshRuntimeCallbacks();
bool shouldSkipMissingAnimation(const std::string& aniPath);
void CreateAnimationBySlot(const std::string& actionName, void CreateAnimationBySlot(const std::string& actionName,
const std::string& slotName, const std::string& slotName,
@@ -57,6 +59,8 @@ private:
int direction_ = 1; int direction_ = 1;
ActionFrameFlagCallback actionFrameFlagCallback_; ActionFrameFlagCallback actionFrameFlagCallback_;
ActionEndCallback actionEndCallback_; ActionEndCallback actionEndCallback_;
std::unordered_set<std::string> missingAnimationPaths_;
size_t skippedMissingAnimationCount_ = 0;
}; };
} // namespace frostbite2D } // namespace frostbite2D

View File

@@ -0,0 +1,47 @@
#pragma once
#include <frostbite2D/2d/actor.h>
namespace frostbite2D {
class CharacterObject;
class GameMap;
class NineSliceActor;
class TextSprite;
/**
* @brief Reusable singleton debug actor for UI scenes.
*/
class GameDebugActor : public Actor {
public:
static GameDebugActor& get();
static Ptr<GameDebugActor> getPtr();
GameDebugActor(const GameDebugActor&) = delete;
GameDebugActor& operator=(const GameDebugActor&) = delete;
bool AttachToScene(Scene* scene);
bool AttachToParent(Actor* parent);
void DetachFromParent();
void SetDebugMap(GameMap* map);
void SetTrackedCharacter(CharacterObject* character);
void ClearDebugContext();
void OnUpdate(float deltaTime) override;
private:
void initOverlay();
void updateOverlay();
void setOverlayVisible(bool visible);
GameDebugActor();
~GameDebugActor() override = default;
GameMap* debugMap_ = nullptr;
CharacterObject* trackedCharacter_ = nullptr;
RefPtr<NineSliceActor> background_;
RefPtr<TextSprite> coordText_;
};
} // namespace frostbite2D

View File

@@ -63,7 +63,7 @@ struct MapConfig {
std::vector<BackgroundAnimationConfig> backgroundAnimations; std::vector<BackgroundAnimationConfig> backgroundAnimations;
std::vector<MapAnimationConfig> mapAnimations; std::vector<MapAnimationConfig> mapAnimations;
std::vector<std::string> soundIds; std::vector<std::string> soundIds;
std::vector<Rect> virtualMovableAreas; std::vector<Vec2> virtualMovablePolygon;
std::vector<Rect> townMovableAreas; std::vector<Rect> townMovableAreas;
std::vector<MoveAreaTarget> townMovableAreaTargets; std::vector<MoveAreaTarget> townMovableAreaTargets;
}; };

View File

@@ -49,6 +49,7 @@ public:
Rect GetMovablePositionArea(size_t index) const; Rect GetMovablePositionArea(size_t index) const;
int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; } int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
bool IsDebugModeEnabled() const { return debugMode_; }
private: private:
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。 /// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
@@ -75,14 +76,14 @@ private:
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。 /// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
RefPtr<Actor> tileRoot_; RefPtr<Actor> tileRoot_;
/// 角色可行走矩形区域。 /// 角色可行走矩形区域。
std::vector<Rect> movableArea_; std::vector<Vec2> movablePolygon_;
/// 进入后会触发切图/传送的矩形区域。 /// 进入后会触发切图/传送的矩形区域。
std::vector<Rect> moveArea_; std::vector<Rect> moveArea_;
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。 /// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
int backgroundRepeatWidth_ = 0; int backgroundRepeatWidth_ = 0;
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。 /// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
int mapOffsetY_ = 0; int mapOffsetY_ = 0;
bool debugMode_ = false; bool debugMode_ = true;
/// 当前地图正在播放的背景音乐。 /// 当前地图正在播放的背景音乐。
Ptr<Music> currentMusic_; Ptr<Music> currentMusic_;
}; };

View File

@@ -9,11 +9,14 @@ class GameMapLayer : public Actor {
public: public:
void Render() override; void Render() override;
void AddDebugFeasibleAreaInfo(const Rect& rect, int type); void SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon);
void AddDebugMoveAreaInfo(const Rect& rect);
void ClearDebugAreaInfo();
void AddObject(RefPtr<Actor> obj); void AddObject(RefPtr<Actor> obj);
private: private:
std::vector<Rect> feasibleAreaInfoList_; std::vector<Vec2> feasibleAreaPolygon_;
std::vector<Rect> feasibleAreaFillRects_;
std::vector<Rect> moveAreaInfoList_; std::vector<Rect> moveAreaInfoList_;
}; };

View File

@@ -0,0 +1,22 @@
#pragma once
#include <frostbite2D/scene/ui_scene.h>
namespace frostbite2D {
class CharacterObject;
class GameMap;
class GameDebugUIScene : public UIScene {
public:
GameDebugUIScene() = default;
~GameDebugUIScene() override = default;
void onEnter() override;
void onExit() override;
void SetDebugContext(GameMap* map, CharacterObject* character);
void ClearDebugContext();
};
} // namespace frostbite2D

View File

@@ -1,8 +1,9 @@
#pragma once #pragma once
#include "camera/GameCameraController.h" #include "camera/GameCameraController.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include "map/GameMap.h" #include "map/GameMap.h"
#include "scene/GameDebugUIScene.h"
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
namespace frostbite2D { namespace frostbite2D {
@@ -19,6 +20,7 @@ public:
private: private:
GameCameraController cameraController_; GameCameraController cameraController_;
RefPtr<CharacterObject> character_; RefPtr<CharacterObject> character_;
RefPtr<GameDebugUIScene> debugScene_;
bool initialized_ = false; bool initialized_ = false;
RefPtr<GameMap> map_; RefPtr<GameMap> map_;
}; };

View File

@@ -0,0 +1,54 @@
#pragma once
#include <array>
#include <string>
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/2d/sprite.h>
namespace frostbite2D {
/**
* @brief Composes 9 consecutive NPK frames into a resizable nine-slice panel.
*/
class NineSliceActor : public Actor {
public:
NineSliceActor();
~NineSliceActor() override = default;
static Ptr<NineSliceActor> createFromNpk(const std::string& imgPath,
size_t startIndex = 0);
bool SetSlices(const std::string& imgPath, size_t startIndex);
void SetSize(const Vec2& size);
void SetSize(float width, float height);
const std::string& GetImgPath() const { return imgPath_; }
size_t GetStartIndex() const { return startIndex_; }
private:
enum SliceIndex : size_t {
TopLeft = 0,
Top = 1,
TopRight = 2,
Left = 3,
Center = 4,
Right = 5,
BottomLeft = 6,
Bottom = 7,
BottomRight = 8,
SliceCount = 9
};
void updateLayout();
void applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
const Vec2& size);
Vec2 getNaturalSize() const;
std::string imgPath_;
size_t startIndex_ = 0;
std::array<Ptr<Sprite>, SliceCount> sliceSprites_;
std::array<Vec2, SliceCount> sliceNaturalSizes_ = {};
};
} // namespace frostbite2D

View File

@@ -19,6 +19,7 @@
#include <frostbite2D/resource/sound_pack_archive.h> #include <frostbite2D/resource/sound_pack_archive.h>
#include <frostbite2D/scene/scene.h> #include <frostbite2D/scene/scene.h>
#include <frostbite2D/scene/scene_manager.h> #include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/utils/startup_trace.h>
#include "scene/GameMapTestScene.h" #include "scene/GameMapTestScene.h"
#include "world/GameWorld.h" #include "world/GameWorld.h"
@@ -28,6 +29,9 @@ int main(int argc, char **argv) {
(void)argc; (void)argc;
(void)argv; (void)argv;
StartupTrace::reset("main entry");
StartupTrace::mark("main entry");
AppConfig config = AppConfig::createDefault(); AppConfig config = AppConfig::createDefault();
config.appName = "Frostbite2D Test App"; config.appName = "Frostbite2D Test App";
config.appVersion = "1.0.0"; config.appVersion = "1.0.0";
@@ -36,26 +40,40 @@ int main(int argc, char **argv) {
config.windowConfig.title = "Frostbite2D - Async Init Demo"; config.windowConfig.title = "Frostbite2D - Async Init Demo";
Application &app = Application::get(); Application &app = Application::get();
{
ScopedStartupTrace startupTrace("Application::init");
if (!app.init(config)) { if (!app.init(config)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize application!"); "Failed to initialize application!");
return -1; return -1;
} }
}
StartupTrace::mark("Application::init complete");
app.run([]() { app.run([]() {
ScopedStartupTrace startupTrace("Application::run startup callback");
{
ScopedStartupTrace stageTrace("FontManager startup");
auto &fontManager = FontManager::get(); auto &fontManager = FontManager::get();
fontManager.init(); fontManager.init();
fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf", fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf",
12); 12);
}
{
ScopedStartupTrace stageTrace("AudioSystem startup");
auto &audioSystem = AudioSystem::get(); auto &audioSystem = AudioSystem::get();
audioSystem.init(); audioSystem.init();
audioSystem.setMasterVolume(1.0f); audioSystem.setMasterVolume(1.0f);
audioSystem.setSoundVolume(0.8f); audioSystem.setSoundVolume(0.8f);
audioSystem.setMusicVolume(0.6f); audioSystem.setMusicVolume(0.6f);
}
SDL_Log("游戏启动"); SDL_Log("游戏启动");
{
ScopedStartupTrace stageTrace("Loading scene bootstrap");
auto LoadingScene = MakePtr<Scene>(); auto LoadingScene = MakePtr<Scene>();
SceneManager::get().PushScene(LoadingScene); SceneManager::get().PushScene(LoadingScene);
@@ -78,37 +96,61 @@ int main(int argc, char **argv) {
self.SetRotation(rotation + 180.0f * dt); self.SetRotation(rotation + 180.0f * dt);
}); });
LoadingScene->AddChild(LoadCircleSp); LoadingScene->AddChild(LoadCircleSp);
}
StartupTrace::mark("loading scene ready");
TaskSystem::get().submitThen( TaskSystem::get().submitThen(
[]() -> std::string { []() -> std::string {
ScopedStartupTrace startupTrace("Async resource bootstrap");
SDL_Log("Async init task on main thread: %s", SDL_Log("Async init task on main thread: %s",
TaskSystem::get().isMainThread() ? "true" : "false"); TaskSystem::get().isMainThread() ? "true" : "false");
{
ScopedStartupTrace stageTrace("PvfArchive open+init");
auto &pvf = PvfArchive::get(); auto &pvf = PvfArchive::get();
if (!pvf.open("assets/Script.pvf")) { if (!pvf.open("assets/Script.pvf")) {
throw std::runtime_error("Failed to open assets/Script.pvf"); throw std::runtime_error("Failed to open assets/Script.pvf");
} }
pvf.init(); pvf.init();
}
{
ScopedStartupTrace stageTrace("NpkArchive init");
auto &npk = NpkArchive::get(); auto &npk = NpkArchive::get();
npk.setImagePackDirectory("assets/ImagePacks2"); npk.setImagePackDirectory("assets/ImagePacks2");
npk.setDefaultImg("sprite/interface/base.img", 0); npk.setDefaultImg("sprite/interface/base.img", 0);
npk.init(); npk.init();
}
{
ScopedStartupTrace stageTrace("SoundPackArchive init");
auto &archive = SoundPackArchive::get(); auto &archive = SoundPackArchive::get();
archive.setSoundPackDirectory("assets/SoundPacks"); archive.setSoundPackDirectory("assets/SoundPacks");
archive.init(); archive.init();
}
{
ScopedStartupTrace stageTrace("AudioDatabase load");
auto &audioDatabase = AudioDatabase::get(); auto &audioDatabase = AudioDatabase::get();
audioDatabase.loadFromFile("assets/audio.xml"); audioDatabase.loadFromFile("assets/audio.xml");
}
return "后台资源加载成功"; return "后台资源加载成功";
}, },
[](std::string message) mutable { [](std::string message) mutable {
SDL_Log("后台资源加载成功"); ScopedStartupTrace startupTrace("Async completion main-thread scene switch");
SDL_Log("%s", message.c_str());
StartupTrace::mark("before SceneManager::ReplaceScene");
{
ScopedStartupTrace stageTrace(
"SceneManager::ReplaceScene(GameMapTestScene)");
auto testMapScene = MakePtr<GameMapTestScene>(); auto testMapScene = MakePtr<GameMapTestScene>();
SceneManager::get().ReplaceScene(testMapScene); SceneManager::get().ReplaceScene(testMapScene);
}
StartupTrace::mark("after SceneManager::ReplaceScene");
}, },
[](std::exception_ptr error) { [](std::exception_ptr error) {
try { try {

View File

@@ -1,5 +1,6 @@
#include "character/CharacterAnimation.h" #include "character/CharacterAnimation.h"
#include "character/CharacterObject.h" #include "character/CharacterObject.h"
#include <frostbite2D/resource/pvf_archive.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -38,6 +39,8 @@ bool CharacterAnimation::Init(CharacterObject* parent,
const CharacterEquipmentManager& equipmentManager) { const CharacterEquipmentManager& equipmentManager) {
parent_ = parent; parent_ = parent;
actionAnimations_.clear(); actionAnimations_.clear();
missingAnimationPaths_.clear();
skippedMissingAnimationCount_ = 0;
currentActionTag_.clear(); currentActionTag_.clear();
direction_ = 1; direction_ = 1;
actionFrameFlagCallback_ = nullptr; actionFrameFlagCallback_ = nullptr;
@@ -56,6 +59,12 @@ bool CharacterAnimation::Init(CharacterObject* parent,
return false; return false;
} }
if (skippedMissingAnimationCount_ > 0) {
SDL_Log("CharacterAnimation: skipped %zu missing animation loads across %zu unique paths for job %d",
skippedMissingAnimationCount_, missingAnimationPaths_.size(),
config.jobId);
}
return true; return true;
} }
@@ -77,6 +86,10 @@ void CharacterAnimation::CreateAnimationBySlot(
const character::CharacterConfig& config, const character::CharacterConfig& config,
const CharacterEquipmentManager& equipmentManager) { const CharacterEquipmentManager& equipmentManager) {
if (slotName == std::string("skin_avatar")) { if (slotName == std::string("skin_avatar")) {
if (shouldSkipMissingAnimation(actionPath)) {
return;
}
Animation::ReplaceData replaceData(0, 0); Animation::ReplaceData replaceData(0, 0);
if (const auto* equip = equipmentManager.GetEquip(slotName)) { if (const auto* equip = equipmentManager.GetEquip(slotName)) {
auto it = equip->jobAnimations.find(config.jobId); auto it = equip->jobAnimations.find(config.jobId);
@@ -114,6 +127,10 @@ void CharacterAnimation::CreateAnimationBySlot(
} }
std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail; std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail;
if (shouldSkipMissingAnimation(aniPath)) {
continue;
}
auto animation = MakePtr<Animation>( auto animation = MakePtr<Animation>(
aniPath, FormatImgPath, aniPath, FormatImgPath,
Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1])); Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1]));
@@ -128,6 +145,23 @@ void CharacterAnimation::CreateAnimationBySlot(
} }
} }
bool CharacterAnimation::shouldSkipMissingAnimation(const std::string& aniPath) {
PvfArchive& pvf = PvfArchive::get();
std::string normalizedPath = pvf.normalizePath(aniPath);
if (missingAnimationPaths_.find(normalizedPath) != missingAnimationPaths_.end()) {
++skippedMissingAnimationCount_;
return true;
}
if (pvf.hasFile(normalizedPath)) {
return false;
}
missingAnimationPaths_.insert(std::move(normalizedPath));
++skippedMissingAnimationCount_;
return true;
}
std::string CharacterAnimation::DescribeAvailableActions() const { std::string CharacterAnimation::DescribeAvailableActions() const {
if (actionAnimations_.empty()) { if (actionAnimations_.empty()) {
return "<none>"; return "<none>";

View File

@@ -4,6 +4,7 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <cmath> #include <cmath>
#include <frostbite2D/core/application.h> #include <frostbite2D/core/application.h>
#include <frostbite2D/utils/startup_trace.h>
#include <sstream> #include <sstream>
#include <utility> #include <utility>
@@ -49,6 +50,7 @@ int32 RoundWorldCoordinate(float value) {
} // namespace } // namespace
bool CharacterObject::Construction(int jobId) { bool CharacterObject::Construction(int jobId) {
ScopedStartupTrace startupTrace("CharacterObject::Construction");
// Reset all runtime state before rebuilding the character from config. // Reset all runtime state before rebuilding the character from config.
EnableEventReceive(); EnableEventReceive();
RemoveAllChildren(); RemoveAllChildren();
@@ -67,7 +69,10 @@ bool CharacterObject::Construction(int jobId) {
lastDeltaTime_ = 0.0f; lastDeltaTime_ = 0.0f;
inputEnabled_ = true; inputEnabled_ = true;
auto config = character::loadCharacterConfig(jobId); auto config = [&]() {
ScopedStartupTrace stageTrace("character::loadCharacterConfig");
return character::loadCharacterConfig(jobId);
}();
if (!config) { if (!config) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CharacterObject: failed to load job %d config", jobId); "CharacterObject: failed to load job %d config", jobId);
@@ -79,25 +84,37 @@ bool CharacterObject::Construction(int jobId) {
direction_ = 1; direction_ = 1;
config_ = *config; config_ = *config;
stateMachine_.Configure(*config_); stateMachine_.Configure(*config_);
{
ScopedStartupTrace stageTrace("CharacterEquipmentManager::Init");
equipmentManager_.Init(config_->baseJobConfig); equipmentManager_.Init(config_->baseJobConfig);
if (auto actionLibrary = loadCharacterActionLibrary(*config_)) { }
if (auto actionLibrary = [&]() {
ScopedStartupTrace stageTrace("loadCharacterActionLibrary");
return loadCharacterActionLibrary(*config_);
}()) {
actionLibrary_ = *actionLibrary; actionLibrary_ = *actionLibrary;
} }
animationManager_ = MakePtr<CharacterAnimation>(); animationManager_ = MakePtr<CharacterAnimation>();
{
ScopedStartupTrace stageTrace("CharacterAnimation::Init");
if (!animationManager_->Init(this, *config_, equipmentManager_)) { if (!animationManager_->Init(this, *config_, equipmentManager_)) {
ReportFatalCharacterError("CharacterObject::Construction", ReportFatalCharacterError("CharacterObject::Construction",
"no usable animation tags were loaded"); "no usable animation tags were loaded");
return false; return false;
} }
}
AddChild(animationManager_); AddChild(animationManager_);
{
ScopedStartupTrace stageTrace("CharacterObject initial action setup");
if (!RequireAction("idle", "CharacterObject::Construction")) { if (!RequireAction("idle", "CharacterObject::Construction")) {
return false; return false;
} }
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) { if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
return false; return false;
} }
}
SetWorldPosition({}); SetWorldPosition({});
SetFacing(1); SetFacing(1);

View File

@@ -0,0 +1,147 @@
#include "common/GameDebugActor.h"
#include "character/CharacterObject.h"
#include "map/GameMap.h"
#include "ui/NineSliceActor.h"
#include <SDL2/SDL.h>
#include <frostbite2D/2d/text_sprite.h>
#include <frostbite2D/scene/scene.h>
#include <limits>
namespace frostbite2D {
namespace {
constexpr float kDebugHudMarginX = 12.0f;
constexpr float kDebugHudMarginY = 12.0f;
constexpr float kDebugHudPaddingX = 8.0f;
constexpr float kDebugHudPaddingY = 6.0f;
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
} // namespace
GameDebugActor::GameDebugActor() {
SetName("GameDebugActor");
SetZOrder(std::numeric_limits<int>::max());
initOverlay();
}
GameDebugActor& GameDebugActor::get() {
return *getPtr();
}
Ptr<GameDebugActor> GameDebugActor::getPtr() {
static Ptr<GameDebugActor> instance(new GameDebugActor());
return instance;
}
bool GameDebugActor::AttachToScene(Scene* scene) {
return AttachToParent(scene);
}
bool GameDebugActor::AttachToParent(Actor* parent) {
if (!parent || parent == this) {
return false;
}
Ptr<GameDebugActor> self = getPtr();
if (GetParent() == parent) {
return true;
}
if (GetParent()) {
GetParent()->RemoveChild(self);
}
parent->AddChild(self);
return GetParent() == parent;
}
void GameDebugActor::DetachFromParent() {
Ptr<GameDebugActor> self = getPtr();
if (GetParent()) {
GetParent()->RemoveChild(self);
}
}
void GameDebugActor::SetDebugMap(GameMap* map) {
debugMap_ = map;
updateOverlay();
}
void GameDebugActor::SetTrackedCharacter(CharacterObject* character) {
trackedCharacter_ = character;
updateOverlay();
}
void GameDebugActor::ClearDebugContext() {
debugMap_ = nullptr;
trackedCharacter_ = nullptr;
setOverlayVisible(false);
}
void GameDebugActor::OnUpdate(float deltaTime) {
(void)deltaTime;
updateOverlay();
}
void GameDebugActor::initOverlay() {
if (background_ || coordText_) {
return;
}
background_ = NineSliceActor::createFromNpk(kDebugHudPopupImg, 0);
if (background_) {
background_->SetName("debugHudBackground");
AddChild(background_);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDebugActor: failed to load HUD background from %s",
kDebugHudPopupImg);
}
coordText_ = TextSprite::create();
coordText_->SetName("debugCoordText");
coordText_->SetFont("default");
coordText_->SetTextColor(1.0f, 1.0f, 1.0f, 1.0f);
coordText_->SetZOrder(1);
AddChild(coordText_);
setOverlayVisible(false);
}
void GameDebugActor::updateOverlay() {
if (!coordText_ || !debugMap_ || !trackedCharacter_ ||
!debugMap_->IsDebugModeEnabled()) {
setOverlayVisible(false);
return;
}
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
char textBuffer[64];
SDL_snprintf(textBuffer, sizeof(textBuffer), "角色坐标: (%d, %d, %d)",
worldPosition.x, worldPosition.y, worldPosition.z);
coordText_->SetText(textBuffer);
Vec2 textSize = coordText_->GetTextSize();
Vec2 panelSize(textSize.x + kDebugHudPaddingX * 2.0f,
textSize.y + kDebugHudPaddingY * 2.0f);
if (background_) {
background_->SetSize(panelSize);
}
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
SetScale(1.0f);
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
setOverlayVisible(true);
}
void GameDebugActor::setOverlayVisible(bool visible) {
if (background_) {
background_->SetVisible(visible);
}
if (coordText_) {
coordText_->SetVisible(visible);
}
}
} // namespace frostbite2D

View File

@@ -268,16 +268,39 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) {
outConfig.townMovableAreaTargets.push_back(target); outConfig.townMovableAreaTargets.push_back(target);
} }
} else if (segment == "[virtual movable area]") { } else if (segment == "[virtual movable area]") {
std::vector<int> polygonCoords;
while (!stream.isEnd()) { while (!stream.isEnd()) {
std::string token = stream.get(); std::string token = stream.get();
if (token == "[/virtual movable area]") { if (token == "[/virtual movable area]") {
break; break;
} }
Rect rect(static_cast<float>(toInt(token)), polygonCoords.push_back(toInt(token));
static_cast<float>(toInt(stream.get())), }
static_cast<float>(toInt(stream.get())),
static_cast<float>(toInt(stream.get()))); if (polygonCoords.empty()) {
outConfig.virtualMovableAreas.push_back(rect); continue;
}
if (polygonCoords.size() % 2 != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: map %s has odd [virtual movable area] coordinate count (%zu)",
outConfig.mapPath.c_str(), polygonCoords.size());
continue;
}
if (polygonCoords.size() < 6) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameDataLoader: map %s [virtual movable area] needs at least 3 vertices, got %zu",
outConfig.mapPath.c_str(), polygonCoords.size() / 2);
continue;
}
outConfig.virtualMovablePolygon.clear();
outConfig.virtualMovablePolygon.reserve(polygonCoords.size() / 2);
for (size_t i = 0; i + 1 < polygonCoords.size(); i += 2) {
outConfig.virtualMovablePolygon.emplace_back(
static_cast<float>(polygonCoords[i]),
static_cast<float>(polygonCoords[i + 1]));
} }
} }
} }

View File

@@ -6,6 +6,7 @@
#include <frostbite2D/core/application.h> #include <frostbite2D/core/application.h>
#include <frostbite2D/graphics/renderer.h> #include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/audio_database.h> #include <frostbite2D/resource/audio_database.h>
#include <frostbite2D/utils/startup_trace.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -23,6 +24,7 @@ static const int kLayerOrders[] = {
constexpr int kTileRootZOrder = -1000000; constexpr int kTileRootZOrder = -1000000;
constexpr float kExtendedTileStepY = 120.0f; constexpr float kExtendedTileStepY = 120.0f;
constexpr double kPolygonEpsilon = 0.001;
int RoundWorldCoordinate(float value) { int RoundWorldCoordinate(float value) {
return static_cast<int>(std::lround(value)); return static_cast<int>(std::lround(value));
@@ -37,6 +39,57 @@ Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
static_cast<float>(z)); static_cast<float>(z));
} }
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
Vec2 segment = end - start;
Vec2 toPoint = point - start;
double cross = static_cast<double>(segment.cross(toPoint));
if (std::abs(cross) > kPolygonEpsilon) {
return false;
}
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
return point.x >= minX && point.x <= maxX && point.y >= minY &&
point.y <= maxY;
}
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
if (polygon.size() < 3) {
return false;
}
bool inside = false;
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (IsPointOnSegment(point, start, end)) {
return true;
}
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
if (!crossesScanline) {
continue;
}
double intersectX =
static_cast<double>(start.x) +
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
(static_cast<double>(end.y) - static_cast<double>(start.y));
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
inside = !inside;
}
}
return inside;
}
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
}
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite // 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
// 占位,避免整张地图中断。 // 占位,避免整张地图中断。
Ptr<Sprite> createMapSprite(const std::string& path, int index) { Ptr<Sprite> createMapSprite(const std::string& path, int index) {
@@ -70,33 +123,49 @@ void GameMap::clearLayerChildren() {
for (auto& [_, layer] : layerMap_) { for (auto& [_, layer] : layerMap_) {
if (layer) { if (layer) {
layer->RemoveAllChildren(); layer->RemoveAllChildren();
layer->ClearDebugAreaInfo();
} }
} }
tileRoot_.Reset(); tileRoot_.Reset();
movableArea_.clear(); movablePolygon_.clear();
moveArea_.clear(); moveArea_.clear();
currentMusic_.Reset(); currentMusic_.Reset();
backgroundRepeatWidth_ = 0; backgroundRepeatWidth_ = 0;
} }
bool GameMap::LoadMap(const std::string &mapName) { bool GameMap::LoadMap(const std::string &mapName) {
// 清空所有图层子节点。 ScopedStartupTrace startupTrace("GameMap::LoadMap");
clearLayerChildren(); clearLayerChildren();
// 读取PVF地图配置。 {
ScopedStartupTrace stageTrace("game::loadMapConfig");
if (!game::loadMapConfig(mapName, mapConfig_)) { if (!game::loadMapConfig(mapName, mapConfig_)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s", SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
mapName.c_str()); "GameMap: failed to load map %s", mapName.c_str());
return false; return false;
} }
}
// backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。
mapOffsetY_ = mapConfig_.backgroundPos; mapOffsetY_ = mapConfig_.backgroundPos;
// 加载顺序基本就是地图的组装顺序:先地板,再背景,再对象,再可行走数据。 {
ScopedStartupTrace stageTrace("GameMap::InitTile");
InitTile(); InitTile();
}
{
ScopedStartupTrace stageTrace("GameMap::InitBackgroundAnimation");
InitBackgroundAnimation(); InitBackgroundAnimation();
}
{
ScopedStartupTrace stageTrace("GameMap::InitMapAnimation");
InitMapAnimation(); InitMapAnimation();
}
{
ScopedStartupTrace stageTrace("GameMap::InitVirtualMovableArea");
InitVirtualMovableArea(); InitVirtualMovableArea();
}
{
ScopedStartupTrace stageTrace("GameMap::InitMoveArea");
InitMoveArea(); InitMoveArea();
}
return true; return true;
} }
@@ -248,26 +317,48 @@ void GameMap::InitMapAnimation() {
} }
void GameMap::InitVirtualMovableArea() { void GameMap::InitVirtualMovableArea() {
movableArea_ = mapConfig_.virtualMovableAreas; movablePolygon_ = mapConfig_.virtualMovablePolygon;
// debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。 if (movablePolygon_.empty()) {
return;
}
float minX = movablePolygon_.front().x;
float minY = movablePolygon_.front().y;
float maxX = movablePolygon_.front().x;
float maxY = movablePolygon_.front().y;
for (const auto& point : movablePolygon_) {
minX = std::min(minX, point.x);
minY = std::min(minY, point.y);
maxX = std::max(maxX, point.x);
maxY = std::max(maxY, point.y);
}
SDL_Log("GameMap: movable polygon vertices=%zu bounds=(%.0f, %.0f)-(%.0f, %.0f)",
movablePolygon_.size(), minX, minY, maxX, maxY);
if (!debugMode_) { if (!debugMode_) {
return; return;
} }
for (const auto& rect : movableArea_) { auto layerIt = layerMap_.find("max");
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0); if (layerIt == layerMap_.end() || !layerIt->second) {
return;
} }
layerIt->second->SetDebugFeasibleAreaPolygon(movablePolygon_);
} }
void GameMap::InitMoveArea() { void GameMap::InitMoveArea() {
moveArea_ = mapConfig_.townMovableAreas; moveArea_ = mapConfig_.townMovableAreas;
// move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。
if (!debugMode_) { if (!debugMode_) {
return; return;
} }
auto layerIt = layerMap_.find("max");
if (layerIt == layerMap_.end() || !layerIt->second) {
return;
}
for (const auto& rect : moveArea_) { for (const auto& rect : moveArea_) {
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1); layerIt->second->AddDebugMoveAreaInfo(rect);
} }
} }
@@ -357,7 +448,7 @@ void GameMap::AddObject(RefPtr<Actor> object) {
if (!object) { if (!object) {
return; return;
} }
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。 // Keep dynamic objects on the normal layer and sort them by y.
object->SetZOrder(static_cast<int>(object->GetPosition().y)); object->SetZOrder(static_cast<int>(object->GetPosition().y));
layerMap_["normal"]->AddObject(object); layerMap_["normal"]->AddObject(object);
} }
@@ -365,42 +456,41 @@ void GameMap::AddObject(RefPtr<Actor> object) {
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
int currentX = RoundWorldCoordinate(curPos.x); int currentX = RoundWorldCoordinate(curPos.x);
int currentY = RoundWorldCoordinate(curPos.y); int currentY = RoundWorldCoordinate(curPos.y);
int currentZ = RoundWorldCoordinate(curPos.z);
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, currentZ);
if (movableArea_.empty()) {
return MakeIntegerWorldPosition(RoundWorldCoordinate(curPos.x + posOffset.x),
RoundWorldCoordinate(curPos.y + posOffset.y),
RoundWorldCoordinate(curPos.z + posOffset.z));
}
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x); int targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y); int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z);
// X/Y Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ);
// Check X and Y separately so edge sliding does not lock both axes. if (movablePolygon_.size() < 3) {
bool isXValid = false; return MakeIntegerWorldPosition(targetX, targetY, targetZ);
for (const auto& area : movableArea_) {
if (area.containsPoint(MakeIntegerWorldPoint(targetX, currentY))) {
isXValid = true;
break;
} }
// Prefer the full destination first; only fall back to single-axis sliding
// when the combined move would leave the movable polygon.
if (IsPointMovable(movablePolygon_, targetX, targetY)) {
result.x = static_cast<float>(targetX);
result.y = static_cast<float>(targetY);
return result;
} }
bool isYValid = false; bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY);
for (const auto& area : movableArea_) { bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY);
if (area.containsPoint(MakeIntegerWorldPoint(currentX, targetY))) {
isYValid = true; if (isXValid && isYValid) {
break; int moveX = std::abs(targetX - currentX);
int moveY = std::abs(targetY - currentY);
if (moveX >= moveY) {
result.x = static_cast<float>(targetX);
} else {
result.y = static_cast<float>(targetY);
} }
return result;
} }
if (isXValid) { if (isXValid) {
result.x = static_cast<float>(targetX); result.x = static_cast<float>(targetX);
} } else if (isYValid) {
if (isYValid) {
result.y = static_cast<float>(targetY); result.y = static_cast<float>(targetY);
} }
result.z = static_cast<float>(RoundWorldCoordinate(curPos.z + posOffset.z));
return result; return result;
} }

View File

@@ -1,32 +1,155 @@
#include "map/GameMapLayer.h" #include "map/GameMapLayer.h"
#include <algorithm>
#include <cmath>
#include <frostbite2D/graphics/renderer.h> #include <frostbite2D/graphics/renderer.h>
namespace frostbite2D { namespace frostbite2D {
namespace {
constexpr float kDebugAreaAlpha = 0.45f;
constexpr float kDebugOutlineAlpha = 0.85f;
constexpr float kDebugEdgePointSize = 5.0f;
constexpr float kDebugVertexSize = 9.0f;
constexpr float kDebugEdgeStep = 4.0f;
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) {
std::vector<Rect> fillRects;
if (polygon.size() < 3) {
return fillRects;
}
float minY = polygon.front().y;
float maxY = polygon.front().y;
for (const auto& point : polygon) {
minY = std::min(minY, point.y);
maxY = std::max(maxY, point.y);
}
int scanlineMinY = static_cast<int>(std::floor(minY));
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
if (scanlineMaxY < scanlineMinY) {
return fillRects;
}
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
float scanY = static_cast<float>(y) + 0.5f;
std::vector<float> intersections;
intersections.reserve(polygon.size());
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
const Vec2& start = polygon[j];
const Vec2& end = polygon[i];
if (start.y == end.y) {
continue;
}
float edgeMinY = std::min(start.y, end.y);
float edgeMaxY = std::max(start.y, end.y);
if (scanY < edgeMinY || scanY >= edgeMaxY) {
continue;
}
float t = (scanY - start.y) / (end.y - start.y);
intersections.push_back(start.x + (end.x - start.x) * t);
}
if (intersections.size() < 2) {
continue;
}
std::sort(intersections.begin(), intersections.end());
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
float left = std::floor(intersections[i]);
float right = std::ceil(intersections[i + 1]);
float width = right - left;
if (width > 0.0f) {
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
}
}
}
return fillRects;
}
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
const Color& color) {
if (polygon.size() < 2) {
return;
}
auto& renderer = Renderer::get();
for (size_t i = 0; i < polygon.size(); ++i) {
Vec2 start = worldOrigin + polygon[i];
Vec2 end = worldOrigin + polygon[(i + 1) % polygon.size()];
Vec2 delta = end - start;
float length = delta.length();
int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep)));
for (int step = 0; step <= steps; ++step) {
float t = static_cast<float>(step) / static_cast<float>(steps);
Vec2 point = start + delta * t;
renderer.drawQuad(
Rect(point.x - kDebugEdgePointSize * 0.5f,
point.y - kDebugEdgePointSize * 0.5f, kDebugEdgePointSize,
kDebugEdgePointSize),
color);
}
}
}
void DrawPolygonVertices(const std::vector<Vec2>& polygon,
const Vec2& worldOrigin, const Color& color) {
auto& renderer = Renderer::get();
for (const auto& vertex : polygon) {
Vec2 point = worldOrigin + vertex;
renderer.drawQuad(
Rect(point.x - kDebugVertexSize * 0.5f,
point.y - kDebugVertexSize * 0.5f, kDebugVertexSize,
kDebugVertexSize),
color);
}
}
} // namespace
void GameMapLayer::Render() { void GameMapLayer::Render() {
Actor::Render(); Actor::Render();
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero()); Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero());
for (const auto& rect : feasibleAreaInfoList_) { for (const auto& rect : feasibleAreaFillRects_) {
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
rect.width(), rect.height()); rect.width(), rect.height());
Renderer::get().drawQuad(drawRect, Color(1.0f, 0.0f, 0.0f, 0.35f)); Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha));
} }
for (const auto& rect : moveAreaInfoList_) { for (const auto& rect : moveAreaInfoList_) {
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
rect.width(), rect.height()); rect.width(), rect.height());
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, 0.35f)); Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha));
}
if (!feasibleAreaPolygon_.empty()) {
Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha);
DrawPolygonOutline(feasibleAreaPolygon_, worldOrigin, outlineColor);
DrawPolygonVertices(feasibleAreaPolygon_, worldOrigin, outlineColor);
} }
} }
void GameMapLayer::AddDebugFeasibleAreaInfo(const Rect& rect, int type) { void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
if (type == 0) { feasibleAreaPolygon_ = polygon;
feasibleAreaInfoList_.push_back(rect); feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_);
} else if (type == 1) { }
void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect) {
moveAreaInfoList_.push_back(rect); moveAreaInfoList_.push_back(rect);
} }
void GameMapLayer::ClearDebugAreaInfo() {
feasibleAreaPolygon_.clear();
feasibleAreaFillRects_.clear();
moveAreaInfoList_.clear();
} }
void GameMapLayer::AddObject(RefPtr<Actor> obj) { void GameMapLayer::AddObject(RefPtr<Actor> obj) {

View File

@@ -0,0 +1,28 @@
#include "scene/GameDebugUIScene.h"
#include "character/CharacterObject.h"
#include "common/GameDebugActor.h"
#include "map/GameMap.h"
namespace frostbite2D {
void GameDebugUIScene::onEnter() {
UIScene::onEnter();
GameDebugActor::get().AttachToScene(this);
}
void GameDebugUIScene::onExit() {
GameDebugActor::get().ClearDebugContext();
GameDebugActor::get().DetachFromParent();
UIScene::onExit();
}
void GameDebugUIScene::SetDebugContext(GameMap* map, CharacterObject* character) {
GameDebugActor::get().SetDebugMap(map);
GameDebugActor::get().SetTrackedCharacter(character);
}
void GameDebugUIScene::ClearDebugContext() {
GameDebugActor::get().ClearDebugContext();
}
} // namespace frostbite2D

View File

@@ -1,5 +1,7 @@
#include "scene/GameMapTestScene.h" #include "scene/GameMapTestScene.h"
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <frostbite2D/scene/scene_manager.h>
#include <frostbite2D/utils/startup_trace.h>
namespace frostbite2D { namespace frostbite2D {
@@ -12,32 +14,49 @@ constexpr char kTestMapPath[] = "map/elvengard/elvengard.map";
GameMapTestScene::GameMapTestScene() = default; GameMapTestScene::GameMapTestScene() = default;
void GameMapTestScene::onEnter() { void GameMapTestScene::onEnter() {
ScopedStartupTrace startupTrace("GameMapTestScene::onEnter");
Scene::onEnter(); Scene::onEnter();
if (!debugScene_) {
debugScene_ = MakePtr<GameDebugUIScene>();
}
SceneManager::get().RemoveUIScene(debugScene_.Get());
SceneManager::get().PushUIScene(debugScene_);
if (initialized_) { if (initialized_) {
debugScene_->SetDebugContext(map_.Get(), character_.Get());
return; return;
} }
{
ScopedStartupTrace stageTrace("GameMapTestScene map load");
map_ = MakePtr<GameMap>(); map_ = MakePtr<GameMap>();
if (!map_->LoadMap(kTestMapPath)) { if (!map_->LoadMap(kTestMapPath)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to load map %s", kTestMapPath); "GameMapTestScene: failed to load map %s", kTestMapPath);
map_.Reset(); map_.Reset();
debugScene_->ClearDebugContext();
return; return;
} }
}
AddChild(map_); AddChild(map_);
{
ScopedStartupTrace stageTrace("GameMapTestScene character construction");
character_ = MakePtr<CharacterObject>(); character_ = MakePtr<CharacterObject>();
if (!character_->Construction(0)) { if (!character_->Construction(0)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GameMapTestScene: failed to construct default character"); "GameMapTestScene: failed to construct default character");
character_.Reset(); character_.Reset();
} else { } else {
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f); Vec2 spawnPos =
map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
character_->SetCharacterPosition(spawnPos); character_->SetCharacterPosition(spawnPos);
character_->EnableEventReceive(); character_->EnableEventReceive();
character_->SetEventPriority(-100); character_->SetEventPriority(-100);
map_->AddObject(character_); map_->AddObject(character_);
} }
}
cameraController_.SetMap(map_.Get()); cameraController_.SetMap(map_.Get());
// cameraController_.SetZoom(1.2f); // cameraController_.SetZoom(1.2f);
@@ -49,11 +68,20 @@ void GameMapTestScene::onEnter() {
cameraController_.ClearTarget(); cameraController_.ClearTarget();
cameraController_.SnapToDefaultFocus(); cameraController_.SnapToDefaultFocus();
} }
{
ScopedStartupTrace stageTrace("GameMap::Enter");
map_->Enter(); map_->Enter();
}
debugScene_->SetDebugContext(map_.Get(), character_.Get());
initialized_ = true; initialized_ = true;
} }
void GameMapTestScene::onExit() { void GameMapTestScene::onExit() {
if (debugScene_) {
debugScene_->ClearDebugContext();
SceneManager::get().RemoveUIScene(debugScene_.Get());
}
Scene::onExit(); Scene::onExit();
} }

View File

@@ -0,0 +1,203 @@
#include "ui/NineSliceActor.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
#include <frostbite2D/resource/npk_archive.h>
namespace frostbite2D {
namespace {
constexpr std::array<const char*, 9> kSliceNames = {
"topLeft", "top", "topRight", "left", "center",
"right", "bottomLeft", "bottom", "bottomRight"};
struct AxisLayout {
float start = 0.0f;
float center = 0.0f;
float end = 0.0f;
};
AxisLayout resolveAxis(float totalSize, float startSize, float endSize) {
totalSize = std::max(totalSize, 0.0f);
startSize = std::max(startSize, 0.0f);
endSize = std::max(endSize, 0.0f);
AxisLayout layout;
float fixedSize = startSize + endSize;
if (totalSize >= fixedSize) {
layout.start = startSize;
layout.center = totalSize - fixedSize;
layout.end = endSize;
return layout;
}
if (fixedSize <= 0.0f) {
layout.center = totalSize;
return layout;
}
float scale = totalSize / fixedSize;
layout.start = startSize * scale;
layout.end = endSize * scale;
return layout;
}
} // namespace
NineSliceActor::NineSliceActor() {
SetName("NineSliceActor");
}
Ptr<NineSliceActor> NineSliceActor::createFromNpk(const std::string& imgPath,
size_t startIndex) {
auto actor = MakePtr<NineSliceActor>();
if (!actor->SetSlices(imgPath, startIndex)) {
return nullptr;
}
return actor;
}
bool NineSliceActor::SetSlices(const std::string& imgPath, size_t startIndex) {
NpkArchive& npk = NpkArchive::get();
auto imgOpt = npk.getImg(imgPath);
if (!imgOpt) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: img not found: %s", imgPath.c_str());
return false;
}
std::array<Ptr<Sprite>, SliceCount> newSlices;
std::array<Vec2, SliceCount> newNaturalSizes = {};
for (size_t i = 0; i < SliceCount; ++i) {
size_t frameIndex = startIndex + i;
auto frameOpt = npk.getImageFrame(*imgOpt, frameIndex);
if (!frameOpt || frameOpt->data.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: frame %u missing for img %s",
static_cast<unsigned>(frameIndex), imgPath.c_str());
return false;
}
auto sprite = Sprite::createFromNpk(imgPath, frameIndex);
if (!sprite || !sprite->GetTexture()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"NineSliceActor: failed to create sprite for frame %u of %s",
static_cast<unsigned>(frameIndex), imgPath.c_str());
return false;
}
sprite->SetName(kSliceNames[i]);
sprite->SetOffset(0.0f, 0.0f);
sprite->SetPosition(0.0f, 0.0f);
newNaturalSizes[i] = sprite->GetSize();
newSlices[i] = sprite;
}
for (auto& sprite : sliceSprites_) {
if (sprite && sprite->GetParent() == this) {
RemoveChild(sprite);
}
}
sliceSprites_ = newSlices;
sliceNaturalSizes_ = newNaturalSizes;
imgPath_ = imgPath;
startIndex_ = startIndex;
Vec2 targetSize = GetSize();
if (targetSize.x <= 0.0f && targetSize.y <= 0.0f) {
targetSize = getNaturalSize();
} else {
targetSize.x = std::max(targetSize.x, 0.0f);
targetSize.y = std::max(targetSize.y, 0.0f);
}
for (auto& sprite : sliceSprites_) {
AddChild(sprite);
}
Actor::SetSize(targetSize);
updateLayout();
return true;
}
void NineSliceActor::SetSize(const Vec2& size) {
SetSize(size.x, size.y);
}
void NineSliceActor::SetSize(float width, float height) {
Actor::SetSize(std::max(width, 0.0f), std::max(height, 0.0f));
updateLayout();
}
Vec2 NineSliceActor::getNaturalSize() const {
return Vec2(sliceNaturalSizes_[TopLeft].x + sliceNaturalSizes_[Center].x +
sliceNaturalSizes_[TopRight].x,
sliceNaturalSizes_[TopLeft].y + sliceNaturalSizes_[Center].y +
sliceNaturalSizes_[BottomLeft].y);
}
void NineSliceActor::updateLayout() {
if (imgPath_.empty()) {
return;
}
AxisLayout horizontal = resolveAxis(GetSize().x, sliceNaturalSizes_[TopLeft].x,
sliceNaturalSizes_[TopRight].x);
AxisLayout vertical = resolveAxis(GetSize().y, sliceNaturalSizes_[TopLeft].y,
sliceNaturalSizes_[BottomLeft].y);
float leftX = 0.0f;
float centerX = horizontal.start;
float rightX = horizontal.start + horizontal.center;
float topY = 0.0f;
float middleY = vertical.start;
float bottomY = vertical.start + vertical.center;
applySliceLayout(sliceSprites_[TopLeft], Vec2(leftX, topY),
Vec2(horizontal.start, vertical.start));
applySliceLayout(sliceSprites_[Top], Vec2(centerX, topY),
Vec2(horizontal.center, vertical.start));
applySliceLayout(sliceSprites_[TopRight], Vec2(rightX, topY),
Vec2(horizontal.end, vertical.start));
applySliceLayout(sliceSprites_[Left], Vec2(leftX, middleY),
Vec2(horizontal.start, vertical.center));
applySliceLayout(sliceSprites_[Center], Vec2(centerX, middleY),
Vec2(horizontal.center, vertical.center));
applySliceLayout(sliceSprites_[Right], Vec2(rightX, middleY),
Vec2(horizontal.end, vertical.center));
applySliceLayout(sliceSprites_[BottomLeft], Vec2(leftX, bottomY),
Vec2(horizontal.start, vertical.end));
applySliceLayout(sliceSprites_[Bottom], Vec2(centerX, bottomY),
Vec2(horizontal.center, vertical.end));
applySliceLayout(sliceSprites_[BottomRight], Vec2(rightX, bottomY),
Vec2(horizontal.end, vertical.end));
}
void NineSliceActor::applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
const Vec2& size) {
if (!sprite) {
return;
}
sprite->SetPosition(position);
Vec2 clampedSize(std::max(size.x, 0.0f), std::max(size.y, 0.0f));
if (clampedSize.x <= 0.0f || clampedSize.y <= 0.0f) {
sprite->SetSize(0.0f, 0.0f);
sprite->SetVisible(false);
return;
}
sprite->SetVisible(true);
sprite->SetSize(clampedSize);
}
} // namespace frostbite2D

View File

@@ -40,7 +40,7 @@ target("Frostbite2D")
-- 复制所有依赖的 DLL (Windows 平台) -- 复制所有依赖的 DLL (Windows 平台)
if is_plat("mingw") or is_plat("windows") then if is_plat("mingw") or is_plat("windows") then
-- 复制所有包的 DLL 文件 -- 复制所有包的 DLL 文件
local all_pkgs = {"libsdl2", "libsdl2_image", "libsdl2_mixer"} local all_pkgs = {"libsdl2", "libsdl2_image", "libsdl2_mixer", "libsdl2_ttf"}
for _, pkg_name in ipairs(all_pkgs) do for _, pkg_name in ipairs(all_pkgs) do
local pkg = target:pkg(pkg_name) local pkg = target:pkg(pkg_name)
if pkg then if pkg then