From bcc285eed62d2d19676c2c9cfbc69fb4b726df7b Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Mon, 6 Apr 2026 01:18:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9C=BA=E6=99=AF=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0UIScene=E6=94=AF=E6=8C=81=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=9C=BA=E6=99=AF=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(渲染器): 优化相机切换时的渲染批处理 feat(调试工具): 新增游戏调试UI场景和九宫格面板组件 fix(动画系统): 跳过缺失的动画资源加载并记录日志 perf(资源加载): 使用BinaryFileStreamReader优化NPK文件解析 feat(地图系统): 支持多边形可行走区域调试显示 style(代码格式): 清理多余空格和统一文件编码 docs(注释): 补充关键类和方法的文档说明 test(启动跟踪): 添加启动过程性能跟踪工具 chore(依赖): 添加SDL2_ttf库支持 --- .../resource/binary_file_stream_reader.h | 62 +++ .../frostbite2D/resource/npk_archive.h | 39 +- .../frostbite2D/resource/pvf_archive.h | 189 ++----- .../frostbite2D/resource/sound_pack_archive.h | 4 +- .../include/frostbite2D/scene/scene_manager.h | 21 +- .../include/frostbite2D/scene/ui_scene.h | 20 + .../include/frostbite2D/utils/startup_trace.h | 152 ++++++ .../src/frostbite2D/2d/text_sprite.cpp | 70 ++- .../src/frostbite2D/core/application.cpp | 106 ++-- .../src/frostbite2D/graphics/renderer.cpp | 15 + .../resource/binary_file_stream_reader.cpp | 236 +++++++++ .../src/frostbite2D/resource/npk_archive.cpp | 486 +++++++++++++++++- .../src/frostbite2D/resource/pvf_archive.cpp | 475 ++++++++++++----- .../resource/sound_pack_archive.cpp | 5 +- Frostbite2D/src/frostbite2D/scene/scene.cpp | 4 +- .../src/frostbite2D/scene/scene_manager.cpp | 133 ++++- .../src/frostbite2D/scene/ui_scene.cpp | 11 + Game/include/character/CharacterAnimation.h | 4 + Game/include/common/GameDebugActor.h | 47 ++ Game/include/map/GameDataLoader.h | 2 +- Game/include/map/GameMap.h | 5 +- Game/include/map/GameMapLayer.h | 7 +- Game/include/scene/GameDebugUIScene.h | 22 + Game/include/scene/GameMapTestScene.h | 4 +- Game/include/ui/NineSliceActor.h | 54 ++ Game/src/bootstrap/main.cpp | 138 +++-- Game/src/character/CharacterAnimation.cpp | 34 ++ Game/src/character/CharacterObject.cpp | 41 +- Game/src/common/GameDebugActor.cpp | 147 ++++++ Game/src/map/GameDataLoader.cpp | 33 +- Game/src/map/GameMap.cpp | 182 +++++-- Game/src/map/GameMapLayer.cpp | 141 ++++- Game/src/scene/GameDebugUIScene.cpp | 28 + Game/src/scene/GameMapTestScene.cpp | 66 ++- Game/src/ui/NineSliceActor.cpp | 203 ++++++++ platform/mingw.lua | 2 +- 36 files changed, 2675 insertions(+), 513 deletions(-) create mode 100644 Frostbite2D/include/frostbite2D/resource/binary_file_stream_reader.h create mode 100644 Frostbite2D/include/frostbite2D/scene/ui_scene.h create mode 100644 Frostbite2D/include/frostbite2D/utils/startup_trace.h create mode 100644 Frostbite2D/src/frostbite2D/resource/binary_file_stream_reader.cpp create mode 100644 Frostbite2D/src/frostbite2D/scene/ui_scene.cpp create mode 100644 Game/include/common/GameDebugActor.h create mode 100644 Game/include/scene/GameDebugUIScene.h create mode 100644 Game/include/ui/NineSliceActor.h create mode 100644 Game/src/common/GameDebugActor.cpp create mode 100644 Game/src/scene/GameDebugUIScene.cpp create mode 100644 Game/src/ui/NineSliceActor.cpp diff --git a/Frostbite2D/include/frostbite2D/resource/binary_file_stream_reader.h b/Frostbite2D/include/frostbite2D/resource/binary_file_stream_reader.h new file mode 100644 index 0000000..855e716 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/resource/binary_file_stream_reader.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include +#include +#include + +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 readBytes(size_t size); + + template + T read() { + T value{}; + read(reinterpret_cast(&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 diff --git a/Frostbite2D/include/frostbite2D/resource/npk_archive.h b/Frostbite2D/include/frostbite2D/resource/npk_archive.h index 6df7284..82b8a84 100644 --- a/Frostbite2D/include/frostbite2D/resource/npk_archive.h +++ b/Frostbite2D/include/frostbite2D/resource/npk_archive.h @@ -1,18 +1,19 @@ #pragma once #include -#include #include #include #include -#include #include #include #include +#include #include namespace frostbite2D { +class BinaryFileStreamReader; + struct ImageFrame { int32 type = 0; int32 compressionType = 0; @@ -74,29 +75,57 @@ public: size_t getDefaultImgFrame() const; 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; std::string normalizePath(const std::string& path) const; + std::string getCacheFilePath() const; + IndexCacheValidationMode getIndexCacheValidationMode() const; + std::vector collectSourceFileStates() const; + bool loadIndexCache(const std::vector& sourceFiles); + bool saveIndexCache(const std::vector& sourceFiles) const; void scanNpkFiles(); bool parseNpkFile(const std::string& npkPath); bool loadImgData(ImgRef& img); void parseColor(const uint8* tab, int type, uint8* saveByte, int offset); void evictCacheIfNeeded(size_t requiredSize); void updateCacheUsage(const std::string& imgPath); - std::string readNpkInfoString(BinaryReader& reader); + std::string readNpkInfoString(BinaryFileStreamReader& reader); static const uint8 NPK_KEY[256]; std::string imagePackDirectory_ = "ImagePacks2"; bool initialized_ = false; - std::map imgIndex_; - std::map imageCache_; + std::unordered_map imgIndex_; + std::unordered_map imageCache_; std::list lruList_; + std::unordered_map::iterator> lruLookup_; size_t maxCacheSize_ = 512 * 1024 * 1024; size_t currentCacheSize_ = 0; std::string defaultImgPath_; size_t defaultImgFrame_ = 0; + bool verboseFallbackLog_ = +#ifndef NDEBUG + true; +#else + false; +#endif }; } diff --git a/Frostbite2D/include/frostbite2D/resource/pvf_archive.h b/Frostbite2D/include/frostbite2D/resource/pvf_archive.h index c689175..d34354c 100644 --- a/Frostbite2D/include/frostbite2D/resource/pvf_archive.h +++ b/Frostbite2D/include/frostbite2D/resource/pvf_archive.h @@ -1,51 +1,41 @@ #pragma once +#include #include -#include + +#include #include #include -#include #include namespace frostbite2D { /** - * @brief PVF 文件信息结构体 + * @brief PVF ??????? */ struct PvfFileInfo { - size_t offset = 0; ///< 相对于数据起始位置的偏移 - uint32 crc32 = 0; ///< CRC32 校验值 - size_t length = 0; ///< 文件长度(字节) - bool decoded = false; ///< 是否已解码 + size_t offset = 0; ///< ???????????? + uint32 crc32 = 0; ///< CRC32 ??? + size_t length = 0; ///< ???????? + bool decoded = false; ///< ????? }; /** - * @brief 原始文件数据结构体 + * @brief ????????? */ struct RawData { - std::unique_ptr data; ///< 原始数据指针(智能指针自动管理内存) - size_t size; ///< 数据大小(字节) + std::unique_ptr data; ///< ?????????????????? + size_t size; ///< ???????? }; /** - * @brief PVF 资源包归档类 + * @brief PVF ?????? * - * 用于读取和解析 PVF 格式的游戏资源包文件。 - * 提供文件索引管理、字符串资源访问和文件内容读取功能。 - * - * @example - * auto& archive = PvfArchive::get(); - * if (archive.open("Script.pvf")) { - * archive.init(); - * auto content = archive.getFileContent("script/example.txt"); - * } + * ??????? PVF ??????????? + * ?????????????????????????? */ class PvfArchive { public: - /** - * @brief 获取单例实例 - * @return PVF 归档实例引用 - */ static PvfArchive& get(); PvfArchive(const PvfArchive&) = delete; @@ -53,166 +43,53 @@ public: PvfArchive(PvfArchive&&) = delete; PvfArchive& operator=(PvfArchive&&) = delete; - // --------------------------------------------------------------------------- - // 文件操作 - // --------------------------------------------------------------------------- - - /** - * @brief 打开 PVF 文件 - * @param filePath 文件路径 - * @return 打开成功返回 true - */ bool open(const std::string& filePath = "Script.pvf"); - - /** - * @brief 关闭并清空所有数据 - */ void close(); - - /** - * @brief 检查是否已打开 - * @return 已打开返回 true - */ bool isOpen() const; - // --------------------------------------------------------------------------- - // 初始化 - // --------------------------------------------------------------------------- - - /** - * @brief 完整初始化(解析头部、加载字符串表) - */ void init(); - - /** - * @brief 解析 PVF 文件头部并建立文件索引 - */ void initHeader(); - - /** - * @brief 加载二进制字符串表 (stringtable.bin) - */ void initBinStringTable(); - - /** - * @brief 加载本地化字符串 (n_string.lst) - */ void initLoadStrings(); - // --------------------------------------------------------------------------- - // 文件信息查询 - // --------------------------------------------------------------------------- - - /** - * @brief 检查文件是否存在 - * @param path 文件路径 - * @return 存在返回 true - */ bool hasFile(const std::string& path) const; - - /** - * @brief 获取文件信息 - * @param path 文件路径 - * @return 文件信息,不存在返回 std::nullopt - */ std::optional getFileInfo(const std::string& path) const; - - /** - * @brief 获取所有文件路径列表 - * @return 文件路径列表 - */ std::vector listFiles() const; - // --------------------------------------------------------------------------- - // 文件内容读取 - // --------------------------------------------------------------------------- - - /** - * @brief 获取文件内容作为字符串 - * @param path 文件路径 - * @return 文件内容,失败返回 std::nullopt - */ std::optional getFileContent(const std::string& path); - - /** - * @brief 获取文件内容作为字节数组 - * @param path 文件路径 - * @return 字节数组,失败返回 std::nullopt - */ std::optional> getFileBytes(const std::string& path); - - /** - * @brief 获取文件原始数据(使用智能指针管理内存,已包含 CRC 解密) - * - * 与原始 GetFileContentChar 功能相同,但使用智能指针避免内存泄漏。 - * 数据会在首次访问时自动进行 CRC 解密。 - * - * @param path 文件路径 - * @return RawData 结构体,包含数据指针和大小;失败返回 std::nullopt - */ std::optional getFileRawData(const std::string& path); - // --------------------------------------------------------------------------- - // 字符串资源访问 - // --------------------------------------------------------------------------- - - /** - * @brief 获取二进制字符串 - * @param key 字符串键 - * @return 字符串,不存在返回 std::nullopt - */ std::optional getBinString(int key) const; - - /** - * @brief 获取本地化字符串 - * @param type 字符串类型 - * @param key 字符串键 - * @return 字符串,不存在返回 std::nullopt - */ - std::optional getLoadString(const std::string& type, const std::string& key) const; - - /** - * @brief 检查二进制字符串是否存在 - * @param key 字符串键 - * @return 存在返回 true - */ + std::optional getLoadString(const std::string& type, + const std::string& 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; 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: PvfArchive() = default; ~PvfArchive() = default; - /** - * @brief 分割字符串 - * @param str 要分割的字符串 - * @param delimiter 分隔符 - * @return 分割后的字符串列表 - */ - std::vector splitString(const std::string& str, const std::string& delimiter) const; + std::vector splitString(const std::string& str, + const std::string& delimiter) const; + bool decodeFile(const std::string& normalizedPath, PvfFileInfo& info); + void clearInitData(); + std::optional> readArchiveBytes(size_t absoluteOffset, + size_t length); + std::optional> readDecodedFileBytes( + const PvfFileInfo& info); + static void crcDecodeBuffer(std::vector& data, uint32 crc32); - /** - * @brief 解码文件内容(内部使用) - * @param info 文件信息(会修改 decoded 标志) - * @return 成功返回 true - */ - bool decodeFile(PvfFileInfo& info); - - BinaryReader reader_; ///< 二进制读取器 - size_t dataStartPos_ = 0; ///< 数据起始位置 - std::map fileInfo_; ///< 文件信息映射 - std::map binStringTable_; ///< 二进制字符串表 - std::map> loadStrings_; ///< 本地化字符串 + BinaryFileStreamReader reader_; + size_t dataStartPos_ = 0; + std::map fileInfo_; + std::map binStringTable_; + std::map> loadStrings_; + std::map> decodedFileCache_; }; } // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h b/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h index 0f015ad..f6fe3c0 100644 --- a/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h +++ b/Frostbite2D/include/frostbite2D/resource/sound_pack_archive.h @@ -12,6 +12,8 @@ namespace frostbite2D { +class BinaryFileStreamReader; + struct AudioRef { std::string path; std::string npkFile; @@ -65,7 +67,7 @@ private: bool loadAudioData(AudioRef& audio); void evictCacheIfNeeded(size_t requiredSize); void updateCacheUsage(const std::string& audioPath); - std::string readNpkInfoString(BinaryReader& reader); + std::string readNpkInfoString(BinaryFileStreamReader& reader); static const uint8 NPK_KEY[256]; diff --git a/Frostbite2D/include/frostbite2D/scene/scene_manager.h b/Frostbite2D/include/frostbite2D/scene/scene_manager.h index ed8bd69..e13c0a2 100644 --- a/Frostbite2D/include/frostbite2D/scene/scene_manager.h +++ b/Frostbite2D/include/frostbite2D/scene/scene_manager.h @@ -1,11 +1,13 @@ -#pragma once +#pragma once +#include #include #include namespace frostbite2D { class Scene; +class UIScene; class SceneManager { public: @@ -17,20 +19,31 @@ public: void PushScene(Ptr scene); void PopScene(); void ReplaceScene(Ptr scene); + + void PushUIScene(Ptr scene); + void PopUIScene(); + void ReplaceUIScene(Ptr scene); + bool RemoveUIScene(UIScene* scene); + void ClearUIScenes(); void ClearAll(); void Update(float deltaTime); + void UpdateUI(float deltaTime); void Render(); + bool DispatchEvent(const Event& event); + bool DispatchUIEvent(const Event& event); + Scene* GetCurrentScene() const; + UIScene* GetCurrentUIScene() const; bool HasActiveScene() const; + bool HasActiveUIScene() const; private: SceneManager() = default; - // ~SceneManager() 在 shutdown() 中手动调用销毁 std::vector> sceneStack_; + std::vector> uiSceneStack_; }; -} - +} // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/scene/ui_scene.h b/Frostbite2D/include/frostbite2D/scene/ui_scene.h new file mode 100644 index 0000000..5d58edc --- /dev/null +++ b/Frostbite2D/include/frostbite2D/scene/ui_scene.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +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 diff --git a/Frostbite2D/include/frostbite2D/utils/startup_trace.h b/Frostbite2D/include/frostbite2D/utils/startup_trace.h new file mode 100644 index 0000000..f039132 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/utils/startup_trace.h @@ -0,0 +1,152 @@ +#pragma once + +#include + +#include +#include +#include + +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 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 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(now - start) * 1000.0 / + static_cast(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 : "", totalElapsedMs(), SDL_ThreadID()); + logLine(buffer); + SDL_Log("%s", buffer); + } + + static void logLine(const std::string& line) { + if (!enabled()) { + return; + } + + std::lock_guard 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 : ""), + 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(now - startCounter_) * 1000.0 / + static_cast(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 diff --git a/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp b/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp index ef2cc2b..ec8c561 100644 --- a/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp +++ b/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp @@ -3,9 +3,36 @@ #include #include #include +#include namespace frostbite2D { +namespace { + +Uint32 readSurfacePixel(const SDL_Surface* surface, int x, int y) { + const uint8* row = + static_cast(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(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(pixel); + default: + return 0; + } +} + +} // namespace + TextSprite::TextSprite() : Sprite() { } @@ -79,27 +106,36 @@ void TextSprite::RenderText() { 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 height = surface->h; std::vector rgbaData(width * height * 4); - uint8* pixels = static_cast(surface->pixels); - for (int i = 0; i < width * height; i++) { - rgbaData[i * 4 + 0] = pixels[i * 4 + 0]; - rgbaData[i * 4 + 1] = pixels[i * 4 + 1]; - rgbaData[i * 4 + 2] = pixels[i * 4 + 2]; - rgbaData[i * 4 + 3] = pixels[i * 4 + 3]; + if (SDL_MUSTLOCK(surface) && SDL_LockSurface(surface) != 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to lock text surface: %s", + SDL_GetError()); + SDL_FreeSurface(surface); + return; + } + + 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((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); diff --git a/Frostbite2D/src/frostbite2D/core/application.cpp b/Frostbite2D/src/frostbite2D/core/application.cpp index 7028466..423ed1c 100644 --- a/Frostbite2D/src/frostbite2D/core/application.cpp +++ b/Frostbite2D/src/frostbite2D/core/application.cpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace frostbite2D { @@ -38,6 +39,8 @@ bool Application::init() { } bool Application::init(const AppConfig& config) { + ScopedStartupTrace startupTrace("Application::init internals"); + if (initialized_) { return true; } @@ -46,11 +49,17 @@ bool Application::init(const AppConfig& config) { // 平台相关初始化 #ifdef __SWITCH__ - switchInit(); + { + ScopedStartupTrace stageTrace("switchInit"); + switchInit(); + } #endif #ifdef _WIN32 - windowsInit(); + { + ScopedStartupTrace stageTrace("windowsInit"); + windowsInit(); + } #endif // 初始化核心模块 @@ -122,6 +131,7 @@ Application::~Application() { } bool Application::initCoreModules() { + ScopedStartupTrace startupTrace("Application::initCoreModules"); // 初始化资产管理器 auto &asset = Asset::get(); @@ -129,32 +139,44 @@ bool Application::initCoreModules() { // 平台相关 switch平台不可以获取当前工作目录 #ifndef __SWITCH__ // 获取程序工作目录 - std::string workingDir = SDL_GetBasePath(); - asset.setWorkingDirectory(workingDir); - SDL_Log("Asset working directory: %s", workingDir.c_str()); + { + ScopedStartupTrace stageTrace("Asset working directory setup"); + std::string workingDir = SDL_GetBasePath(); + asset.setWorkingDirectory(workingDir); + SDL_Log("Asset working directory: %s", workingDir.c_str()); + } #else asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName); SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str()); #endif - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) { - SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); - return false; + { + ScopedStartupTrace stageTrace("SDL_Init video/events/controller"); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) { + SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); + return false; + } } // 打开第一个手柄(如果存在) - if (SDL_GameController *controller = SDL_GameControllerOpen(0)) { - SDL_Log("GameController opened: %s", SDL_GameControllerName(controller)); - } else { - SDL_Log("No GameController found"); + { + ScopedStartupTrace stageTrace("SDL_GameControllerOpen(0)"); + if (SDL_GameController *controller = SDL_GameControllerOpen(0)) { + SDL_Log("GameController opened: %s", SDL_GameControllerName(controller)); + } else { + SDL_Log("No GameController found"); + } } // 使用SDL2创建窗口 this->window_ = new Window(); // 创建窗口会使用窗口配置 - if (!window_->create(config_.windowConfig)) { - SDL_Log("Failed to create window"); - return false; + { + ScopedStartupTrace stageTrace("Window::create"); + if (!window_->create(config_.windowConfig)) { + SDL_Log("Failed to create window"); + return false; + } } window_->onResize([this](int width, int height) { @@ -165,9 +187,12 @@ bool Application::initCoreModules() { // 初始化渲染器 renderer_ = &Renderer::get(); - if (!renderer_->init()) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer"); - return false; + { + ScopedStartupTrace stageTrace("Renderer::init"); + if (!renderer_->init()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer"); + return false; + } } // 设置窗口清除颜色和视口 @@ -175,14 +200,20 @@ bool Application::initCoreModules() { renderer_->setViewport(0, 0, config_.windowConfig.width, config_.windowConfig.height); // 创建并设置相机 - camera_ = new Camera(); - camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height); - camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角 - renderer_->setCamera(camera_); + { + ScopedStartupTrace stageTrace("Camera setup"); + camera_ = new Camera(); + camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height); + camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角 + renderer_->setCamera(camera_); + } - if (!TaskSystem::get().init()) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system"); - return false; + { + ScopedStartupTrace stageTrace("TaskSystem::init"); + if (!TaskSystem::get().init()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system"); + return false; + } } return true; @@ -215,6 +246,7 @@ void Application::run(StartCallback callback) { callback(); } + StartupTrace::mark("before Application::mainLoop"); mainLoop(); running_ = false; @@ -257,10 +289,7 @@ void Application::mainLoop() { } TaskSystem::get().drainMainThreadTasks(); - - if (!paused_) { - update(); - } + update(); render(); } @@ -383,10 +412,12 @@ std::unique_ptr Application::convertSDLEvent(const SDL_Event& sdlEvent) { } void Application::dispatchEvent(const Event& event) { - Scene* currentScene = SceneManager::get().GetCurrentScene(); - if (currentScene) { - currentScene->OnEvent(event); + if (paused_) { + SceneManager::get().DispatchUIEvent(event); + return; } + + SceneManager::get().DispatchEvent(event); } void Application::update() { @@ -395,7 +426,10 @@ void Application::update() { lastFrameTime_ = currentTime; totalTime_ += deltaTime_; - SceneManager::get().Update(deltaTime_); + if (!paused_) { + SceneManager::get().Update(deltaTime_); + } + SceneManager::get().UpdateUI(deltaTime_); frameCount_++; fpsTimer_ += deltaTime_; @@ -407,6 +441,7 @@ void Application::update() { } void Application::render() { + static bool firstFramePresented = false; Renderer& renderer = Renderer::get(); renderer.beginFrame(); @@ -416,6 +451,11 @@ void Application::render() { if (window_) { window_->swap(); } + + if (!firstFramePresented) { + firstFramePresented = true; + StartupTrace::mark("first frame presented"); + } } const AppConfig& Application::getConfig() const { diff --git a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp index 9d22d95..93bb23e 100644 --- a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp @@ -93,10 +93,18 @@ void Renderer::clear(uint32_t flags) { } void Renderer::setCamera(Camera* camera) { + if (initialized_ && camera_ != camera) { + batch_.flush(); + } + camera_ = camera; if (camera) { camera_->setViewport(viewportWidth_, viewportHeight_); } + + if (initialized_) { + updateUniforms(); + } } void Renderer::setupBlendMode(BlendMode mode) { @@ -159,6 +167,13 @@ void Renderer::updateUniforms() { coloredShader->setMat4("u_view", camera_->getViewMatrix()); 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()); + } } } diff --git a/Frostbite2D/src/frostbite2D/resource/binary_file_stream_reader.cpp b/Frostbite2D/src/frostbite2D/resource/binary_file_stream_reader.cpp new file mode 100644 index 0000000..8e49c88 --- /dev/null +++ b/Frostbite2D/src/frostbite2D/resource/binary_file_stream_reader.cpp @@ -0,0 +1,236 @@ +#include + +#include + +#include +#include + +#include + +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(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(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(bytesToRead)); + size_t bytesRead = static_cast(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 BinaryFileStreamReader::readBytes(size_t size) { + std::vector result; + size_t bytesToRead = std::min(size, remaining()); + if (bytesToRead == 0) { + lastReadCount_ = 0; + return result; + } + + result.resize(bytesToRead); + size_t bytesRead = read(reinterpret_cast(result.data()), bytesToRead); + result.resize(bytesRead); + return result; +} + +int8 BinaryFileStreamReader::readInt8() { + return read(); +} + +int16 BinaryFileStreamReader::readInt16() { + return read(); +} + +int32 BinaryFileStreamReader::readInt32() { + return read(); +} + +int64 BinaryFileStreamReader::readInt64() { + return read(); +} + +uint8 BinaryFileStreamReader::readUInt8() { + return read(); +} + +uint16 BinaryFileStreamReader::readUInt16() { + return read(); +} + +uint32 BinaryFileStreamReader::readUInt32() { + return read(); +} + +uint64 BinaryFileStreamReader::readUInt64() { + return read(); +} + +float BinaryFileStreamReader::readFloat() { + return read(); +} + +double BinaryFileStreamReader::readDouble() { + return read(); +} + +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 diff --git a/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp b/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp index d5820fb..e6cb0d0 100644 --- a/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp +++ b/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp @@ -1,12 +1,108 @@ #include +#include #include #include +#include +#include +#include +#include +#include #include +#include + namespace frostbite2D { 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( + value.time_since_epoch()) + .count(); + return static_cast(duration); +} + +bool appendBytes(std::vector& 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 +bool appendPod(std::vector& buffer, const T& value) { + return appendBytes(buffer, &value, sizeof(T)); +} + +template +bool readPod(std::ifstream& stream, T& value) { + stream.read(reinterpret_cast(&value), sizeof(T)); + return stream.good(); +} + +bool appendString(std::vector& buffer, const std::string& value) { + if (value.size() > kMaxCacheStringLength) { + return false; + } + + uint32 length = static_cast(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) { switch (code) { case Z_OK: @@ -74,6 +170,7 @@ void NpkArchive::close() { imgIndex_.clear(); imageCache_.clear(); lruList_.clear(); + lruLookup_.clear(); currentCacheSize_ = 0; initialized_ = false; } @@ -89,6 +186,291 @@ std::string NpkArchive::normalizePath(const std::string& path) const { 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::collectSourceFileStates() + const { + Asset& asset = Asset::get(); + std::string npkDir = asset.resolvePath(imagePackDirectory_); + std::vector result; + + if (!asset.isDirectory(npkDir)) { + return result; + } + + std::vector 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(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& 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(validationMode)) { + SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan", + cachedValidationMode, static_cast(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 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 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& sourceFiles) const { + std::string cachePath = getCacheFilePath(); + IndexCacheValidationMode validationMode = getIndexCacheValidationMode(); + uint32 sourceCount = + validationMode == IndexCacheValidationMode::StrictSourceState + ? static_cast(sourceFiles.size()) + : 0; + uint32 imageCount = static_cast(imgIndex_.size()); + std::vector 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(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(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(validationMode), sourceCount, + imageCount); + return true; +} + void NpkArchive::scanNpkFiles() { Asset& asset = Asset::get(); std::string npkDir = asset.resolvePath(imagePackDirectory_); @@ -97,16 +479,58 @@ void NpkArchive::scanNpkFiles() { return; } + imgIndex_.clear(); + IndexCacheValidationMode validationMode = getIndexCacheValidationMode(); + + std::vector 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 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(files.size())); - for (const auto &file : files) { - parseNpkFile(file); + { + ScopedStartupTrace stageTrace("NpkArchive parse files"); + for (const auto& file : files) { + parseNpkFile(file); + } + } + + { + ScopedStartupTrace stageTrace("NpkArchive cache save"); + saveIndexCache(sourceFiles); } } bool NpkArchive::parseNpkFile(const std::string& npkPath) { Asset& asset = Asset::get(); - BinaryReader reader(npkPath); + BinaryFileStreamReader reader(npkPath); if (!reader.isOpen()) { SDL_Log("Failed to open NPK file: %s", npkPath.c_str()); @@ -158,6 +582,7 @@ std::vector NpkArchive::listImgs() const { for (const auto& pair : imgIndex_) { result.push_back(pair.first); } + std::sort(result.begin(), result.end()); return result; } @@ -203,7 +628,7 @@ bool NpkArchive::loadImgData(ImgRef& img) { std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile); npkPath = asset.resolvePath(npkPath); - BinaryReader reader(npkPath); + BinaryFileStreamReader reader(npkPath); if (!reader.isOpen()) { SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str()); return false; @@ -290,19 +715,26 @@ bool NpkArchive::loadImgData(ImgRef& img) { frame.size == static_cast(deSize)) { frame.data = std::move(compressedData); cachedData.memoryUsage += frame.data.size(); - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, " - "type=%d, compression=%d, size=%dx%d", - img.path.c_str(), i, frame.type, frame.compressionType, - frame.width, frame.height); + if (verboseFallbackLog_) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, " + "type=%d, compression=%d, size=%dx%d", + img.path.c_str(), i, frame.type, frame.compressionType, + frame.width, frame.height); + } continue; } - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Failed to uncompress image data: %d (%s), img=%s, npk=%s, frame=%d, " - "type=%d, compression=%d, size=%dx%d, offset=%u, packed=%u, expected=%d", - 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); + if (verboseFallbackLog_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to uncompress image data: %d (%s), img=%s, " + "npk=%s, frame=%d, " + "type=%d, compression=%d, size=%dx%d, offset=%u, " + "packed=%u, expected=%d", + 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; } @@ -325,9 +757,16 @@ bool NpkArchive::loadImgData(ImgRef& img) { cachedData.memoryUsage += frame.data.size(); } - imageCache_[img.path] = std::move(cachedData); - currentCacheSize_ += imageCache_[img.path].memoryUsage; - lruList_.push_front(img.path); + auto existingCacheIt = imageCache_.find(img.path); + if (existingCacheIt != imageCache_.end()) { + 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; imgIndex_[img.path] = img; @@ -372,6 +811,7 @@ void NpkArchive::setCacheSize(size_t maxBytes) { void NpkArchive::clearCache() { imageCache_.clear(); lruList_.clear(); + lruLookup_.clear(); currentCacheSize_ = 0; } @@ -383,6 +823,7 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) { while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) { std::string oldest = lruList_.back(); lruList_.pop_back(); + lruLookup_.erase(oldest); auto it = imageCache_.find(oldest); if (it != imageCache_.end()) { @@ -393,11 +834,12 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) { } void NpkArchive::updateCacheUsage(const std::string& imgPath) { - auto it = std::find(lruList_.begin(), lruList_.end(), imgPath); - if (it != lruList_.end()) { - lruList_.erase(it); + auto lruIt = lruLookup_.find(imgPath); + if (lruIt != lruLookup_.end()) { + lruList_.erase(lruIt->second); } lruList_.push_front(imgPath); + lruLookup_[imgPath] = lruList_.begin(); auto cacheIt = imageCache_.find(imgPath); 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()) { return ""; } diff --git a/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp b/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp index 4ad4584..789b313 100644 --- a/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp +++ b/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp @@ -1,11 +1,47 @@ #include -#include -#include -#include + #include +#include +#include +#include +#include +#include +#include + +#include + namespace frostbite2D { +namespace { + +bool canReadSpan(const std::vector& bytes, size_t offset, size_t length) { + return offset <= bytes.size() && length <= bytes.size() - offset; +} + +template +bool readPodAt(const std::vector& 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& bytes, size_t offset, + size_t length) { + if (!canReadSpan(bytes, offset, length)) { + return {}; + } + + return std::string(reinterpret_cast(bytes.data() + offset), + length); +} + +} // namespace + PvfArchive& PvfArchive::get() { static PvfArchive instance; return instance; @@ -14,8 +50,10 @@ PvfArchive& PvfArchive::get() { bool PvfArchive::open(const std::string& filePath) { close(); + ScopedStartupTrace stageTrace("PvfArchive::open stream"); 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; } @@ -24,10 +62,7 @@ bool PvfArchive::open(const std::string& filePath) { void PvfArchive::close() { reader_.close(); - dataStartPos_ = 0; - fileInfo_.clear(); - binStringTable_.clear(); - loadStrings_.clear(); + clearInitData(); } bool PvfArchive::isOpen() const { @@ -36,16 +71,39 @@ bool PvfArchive::isOpen() const { void PvfArchive::init() { if (!isOpen()) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 请先打开文件再初始化"); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: open the archive before calling init"); return; } - initHeader(); - initBinStringTable(); - initLoadStrings(); + clearInitData(); + + { + ScopedStartupTrace stageTrace("PvfArchive::initHeader"); + initHeader(); + } + if (fileInfo_.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: header parse produced no file entries"); + return; + } + + { + ScopedStartupTrace stageTrace("PvfArchive::initBinStringTable"); + initBinStringTable(); + } + + { + ScopedStartupTrace stageTrace("PvfArchive::initLoadStrings"); + initLoadStrings(); + } } void PvfArchive::initHeader() { + fileInfo_.clear(); + dataStartPos_ = 0; + decodedFileCache_.clear(); + if (!isOpen()) { return; } @@ -53,7 +111,13 @@ void PvfArchive::initHeader() { reader_.seek(0); int32 uuidLength = reader_.readInt32(); - std::string uuid = reader_.readString(uuidLength); + if (uuidLength < 0 || static_cast(uuidLength) > reader_.remaining()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: invalid uuid length in archive header"); + return; + } + + std::string uuid = reader_.readString(static_cast(uuidLength)); (void)uuid; int32 version = reader_.readInt32(); @@ -63,126 +127,201 @@ void PvfArchive::initHeader() { int32 indexHeaderCrc = reader_.readInt32(); int32 indexSize = reader_.readInt32(); - size_t firstPos = reader_.tell(); - reader_.crcDecode(alignedIndexHeaderSize, indexHeaderCrc); + if (alignedIndexHeaderSize <= 0 || indexSize < 0) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: invalid index header metadata (size=%d, count=%d)", + alignedIndexHeaderSize, indexSize); + return; + } - dataStartPos_ = alignedIndexHeaderSize + 56; - size_t currPos = 0; + size_t indexHeaderPos = reader_.tell(); + auto headerBytesOpt = + readArchiveBytes(indexHeaderPos, static_cast(alignedIndexHeaderSize)); + if (!headerBytesOpt.has_value()) { + return; + } + std::vector headerBytes = std::move(headerBytesOpt.value()); + crcDecodeBuffer(headerBytes, static_cast(indexHeaderCrc)); + dataStartPos_ = indexHeaderPos + static_cast(alignedIndexHeaderSize); + + size_t cursor = 0; 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; + cursor += 8; - int32 filePathLength = reader_.readInt32(); - std::string fileName = normalizePath(reader_.readString(filePathLength)); + if (filePathLength < 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: negative path length in index entry %d", i); + break; + } - int32 fileLength = reader_.readInt32(); - int32 crc32 = reader_.readInt32(); - int32 relativeOffset = reader_.readInt32(); + size_t pathLength = static_cast(filePathLength); + if (!canReadSpan(headerBytes, cursor, pathLength + 12)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: truncated file entry payload at index %d", i); + break; + } - if (fileLength > 0) { - int32 realFileLength = (fileLength + 3) & 0xFFFFFFFC; + std::string fileName = + 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(fileLength) + 3u) & 0xFFFFFFFCu; PvfFileInfo info; - info.offset = relativeOffset; + info.offset = static_cast(relativeOffset); info.crc32 = static_cast(crc32); - info.length = realFileLength; + info.length = static_cast(alignedLength); info.decoded = false; fileInfo_[fileName] = info; } - - currPos += 20; - currPos += filePathLength; } } void PvfArchive::initBinStringTable() { - std::string tablePath = "stringtable.bin"; - auto infoOpt = getFileInfo(tablePath); + binStringTable_.clear(); + auto infoOpt = getFileInfo("stringtable.bin"); 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; } - PvfFileInfo info = infoOpt.value(); - reader_.seek(dataStartPos_ + info.offset); - reader_.crcDecode(info.length, info.crc32); - reader_.seek(dataStartPos_ + info.offset); + auto bytesOpt = readDecodedFileBytes(infoOpt.value()); + if (!bytesOpt.has_value()) { + return; + } - size_t fileHeaderPos = reader_.tell(); - int32 count = reader_.readInt32(); + const std::vector& bytes = bytesOpt.value(); + 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) { - reader_.seek(fileHeaderPos + i * 4 + 4); - int32 startPos = reader_.readInt32(); - int32 endPos = reader_.readInt32(); - int32 len = endPos - startPos; + size_t offsetsPos = static_cast(i) * 4 + 4; + int32 startPos = 0; + int32 endPos = 0; + 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); - std::string str = reader_.readString(len); - binStringTable_[i] = str; + if (startPos < 0 || endPos < startPos) { + continue; + } + + size_t stringPos = static_cast(startPos) + 4; + size_t length = static_cast(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() { - std::string lstPath = "n_string.lst"; - auto infoOpt = getFileInfo(lstPath); + loadStrings_.clear(); + auto infoOpt = getFileInfo("n_string.lst"); 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; } - PvfFileInfo info = infoOpt.value(); - reader_.seek(dataStartPos_ + info.offset); - reader_.crcDecode(info.length, info.crc32); - reader_.seek(dataStartPos_ + info.offset); + auto listBytesOpt = readDecodedFileBytes(infoOpt.value()); + if (!listBytesOpt.has_value()) { + return; + } - size_t fileHeaderPos = reader_.tell(); - int16 flag = reader_.readInt16(); - (void)flag; + const std::vector& listBytes = listBytesOpt.value(); + if (listBytes.size() < sizeof(int16)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "PvfArchive: n_string.lst is too small"); + return; + } - size_t i = 2; - while (i < info.length) { - if ((info.length - i) >= 10) { - reader_.seek(fileHeaderPos + i + 6); - int32 findKey = reader_.readInt32(); - - auto binStrOpt = getBinString(findKey); - if (!binStrOpt.has_value()) { - i += 10; - continue; - } - - std::string key = binStrOpt.value(); - size_t slashPos = key.find('/'); - std::string type = (slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos)) : ""; - - if (!key.empty()) { - auto fileInfoOpt = getFileInfo(key); - if (fileInfoOpt.has_value()) { - auto contentOpt = getFileContent(key); - if (contentOpt.has_value()) { - std::string content = contentOpt.value(); - std::vector lines = splitString(content, "\n"); - - for (const auto& line : lines) { - size_t gtPos = line.find('>'); - if (gtPos != std::string::npos && gtPos + 1 < line.length()) { - std::string strKey = line.substr(0, gtPos); - std::string strValue = line.substr(gtPos + 1); - loadStrings_[type][strKey] = strValue; - } - } - } - } - } - } else { + std::map stringFileCache; + size_t cursor = sizeof(int16); + while (cursor + 10 <= listBytes.size()) { + int32 findKey = 0; + if (!readPodAt(listBytes, cursor + 6, findKey)) { break; } - i += 10; + + auto binStrOpt = getBinString(findKey); + if (!binStrOpt.has_value()) { + cursor += 10; + continue; + } + + std::string key = normalizePath(binStrOpt.value()); + if (key.empty()) { + cursor += 10; + continue; + } + + size_t slashPos = key.find('/'); + std::string type = + (slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos)) + : std::string(); + + std::string content; + auto cacheIt = stringFileCache.find(key); + if (cacheIt != stringFileCache.end()) { + content = cacheIt->second; + } else { + auto fileIt = fileInfo_.find(key); + if (fileIt != fileInfo_.end()) { + auto contentBytesOpt = readDecodedFileBytes(fileIt->second); + if (contentBytesOpt.has_value()) { + const std::vector& contentBytes = contentBytesOpt.value(); + content.assign(reinterpret_cast(contentBytes.data()), + contentBytes.size()); + stringFileCache[key] = content; + } + } + } + + if (!content.empty()) { + auto& table = loadStrings_[type]; + std::vector lines = splitString(content, "\n"); + for (const auto& line : lines) { + size_t gtPos = line.find('>'); + if (gtPos != std::string::npos && gtPos + 1 < line.length()) { + table[line.substr(0, gtPos)] = line.substr(gtPos + 1); + } + } + } + + cursor += 10; } } @@ -216,12 +355,19 @@ std::optional PvfArchive::getFileContent(const std::string& path) { } PvfFileInfo& info = it->second; - if (!decodeFile(info)) { + if (!decodeFile(normalizedPath, info)) { return std::nullopt; } - reader_.seek(dataStartPos_ + info.offset); - std::vector bytes = reader_.readBytes(info.length); + auto cacheIt = decodedFileCache_.find(normalizedPath); + if (cacheIt == decodedFileCache_.end()) { + return std::nullopt; + } + + const std::vector& bytes = cacheIt->second; + if (bytes.empty()) { + return std::string(); + } return std::string(reinterpret_cast(bytes.data()), bytes.size()); } @@ -233,12 +379,16 @@ std::optional> PvfArchive::getFileBytes(const std::string& pa } PvfFileInfo& info = it->second; - if (!decodeFile(info)) { + if (!decodeFile(normalizedPath, info)) { return std::nullopt; } - reader_.seek(dataStartPos_ + info.offset); - return reader_.readBytes(info.length); + auto cacheIt = decodedFileCache_.find(normalizedPath); + if (cacheIt == decodedFileCache_.end()) { + return std::nullopt; + } + + return cacheIt->second; } std::optional PvfArchive::getFileRawData(const std::string& path) { @@ -249,17 +399,22 @@ std::optional PvfArchive::getFileRawData(const std::string& path) { } PvfFileInfo& info = it->second; - if (!decodeFile(info)) { + if (!decodeFile(normalizedPath, info)) { return std::nullopt; } - reader_.seek(dataStartPos_ + info.offset); + auto cacheIt = decodedFileCache_.find(normalizedPath); + if (cacheIt == decodedFileCache_.end()) { + return std::nullopt; + } + const std::vector& bytes = cacheIt->second; RawData result; - result.size = info.length; - result.data = std::make_unique(info.length); - reader_.read(result.data.get(), info.length); - + result.size = bytes.size(); + result.data = std::make_unique(bytes.size()); + if (!bytes.empty()) { + std::memcpy(result.data.get(), bytes.data(), bytes.size()); + } return result; } @@ -271,7 +426,8 @@ std::optional PvfArchive::getBinString(int key) const { return std::nullopt; } -std::optional PvfArchive::getLoadString(const std::string& type, const std::string& key) const { +std::optional PvfArchive::getLoadString(const std::string& type, + const std::string& key) const { std::string normalizedType = normalizePath(type); auto typeIt = loadStrings_.find(normalizedType); if (typeIt != loadStrings_.end()) { @@ -287,7 +443,8 @@ bool PvfArchive::hasBinString(int key) const { 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); auto typeIt = loadStrings_.find(normalizedType); if (typeIt != loadStrings_.end()) { @@ -334,7 +491,8 @@ std::string PvfArchive::normalizePath(const std::string& path) const { 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()) { return {}; } @@ -345,9 +503,11 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin [](unsigned char c) { return static_cast(std::tolower(c)); }); size_t slashPos = rawPath.find('/'); - std::string root = slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos); - static const char* kLogicalRoots[] = {"map", "sprite", "town", "sound", "audio", - "monster", "character", "ui"}; + std::string root = + slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos); + static const char* kLogicalRoots[] = {"map", "sprite", "town", + "sound", "audio", "monster", + "character", "ui"}; for (const char* logicalRoot : kLogicalRoots) { if (root == logicalRoot) { return normalizePath(rawPath); @@ -361,7 +521,8 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin return normalizePath(normalizedBaseDir + rawPath); } -std::vector PvfArchive::splitString(const std::string& str, const std::string& delimiter) const { +std::vector PvfArchive::splitString( + const std::string& str, const std::string& delimiter) const { std::vector tokens; size_t pos = 0; size_t found; @@ -378,16 +539,94 @@ std::vector PvfArchive::splitString(const std::string& str, const s return tokens; } -bool PvfArchive::decodeFile(PvfFileInfo& info) { - if (info.decoded) { +bool PvfArchive::decodeFile(const std::string& normalizedPath, + PvfFileInfo& info) { + auto cacheIt = decodedFileCache_.find(normalizedPath); + if (cacheIt != decodedFileCache_.end()) { + info.decoded = true; return true; } - reader_.seek(dataStartPos_ + info.offset); - reader_.crcDecode(info.length, info.crc32); - info.decoded = true; + auto bytesOpt = readDecodedFileBytes(info); + if (!bytesOpt.has_value()) { + return false; + } + decodedFileCache_[normalizedPath] = std::move(bytesOpt.value()); + info.decoded = true; return true; } +void PvfArchive::clearInitData() { + dataStartPos_ = 0; + fileInfo_.clear(); + binStringTable_.clear(); + loadStrings_.clear(); + decodedFileCache_.clear(); +} + +std::optional> PvfArchive::readArchiveBytes( + size_t absoluteOffset, size_t length) { + if (!isOpen()) { + return std::nullopt; + } + + if (length == 0) { + return std::vector(); + } + + 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 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> PvfArchive::readDecodedFileBytes( + const PvfFileInfo& info) { + auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length); + if (!bytesOpt.has_value()) { + return std::nullopt; + } + + std::vector bytes = std::move(bytesOpt.value()); + crcDecodeBuffer(bytes, info.crc32); + return bytes; +} + +void PvfArchive::crcDecodeBuffer(std::vector& 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(data[i]) | + (static_cast(data[i + 1]) << 8) | + (static_cast(data[i + 2]) << 16) | + (static_cast(data[i + 3]) << 24); + + uint32 decoded = (value ^ key ^ crc32); + decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu); + + data[i] = static_cast((decoded >> 0) & 0xFFu); + data[i + 1] = static_cast((decoded >> 8) & 0xFFu); + data[i + 2] = static_cast((decoded >> 16) & 0xFFu); + data[i + 3] = static_cast((decoded >> 24) & 0xFFu); + } +} + } // namespace frostbite2D diff --git a/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp b/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp index 64f8fc4..9853ac6 100644 --- a/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp +++ b/Frostbite2D/src/frostbite2D/resource/sound_pack_archive.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -75,7 +76,7 @@ void SoundPackArchive::scanNpkFiles() { bool SoundPackArchive::parseNpkFile(const std::string& npkPath) { Asset& asset = Asset::get(); - BinaryReader reader(npkPath); + BinaryFileStreamReader reader(npkPath); if (!reader.isOpen()) { 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()) { return ""; } diff --git a/Frostbite2D/src/frostbite2D/scene/scene.cpp b/Frostbite2D/src/frostbite2D/scene/scene.cpp index 66eea21..ae07dab 100644 --- a/Frostbite2D/src/frostbite2D/scene/scene.cpp +++ b/Frostbite2D/src/frostbite2D/scene/scene.cpp @@ -6,9 +6,7 @@ namespace frostbite2D { Scene* Scene::current_ = nullptr; -Scene::Scene() { - current_ = this; -} +Scene::Scene() = default; Scene::~Scene() { } diff --git a/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp b/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp index eb50683..1f88563 100644 --- a/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp +++ b/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp @@ -1,6 +1,7 @@ -#include +#include #include -#include +#include +#include namespace frostbite2D { @@ -9,8 +10,6 @@ SceneManager& SceneManager::get() { return instance; } -// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用 - void SceneManager::PushScene(Ptr scene) { if (!scene) { return; @@ -21,6 +20,7 @@ void SceneManager::PushScene(Ptr scene) { } sceneStack_.push_back(scene); + Scene::current_ = scene.Get(); scene->onEnter(); } @@ -32,6 +32,7 @@ void SceneManager::PopScene() { sceneStack_.back()->onExit(); sceneStack_.pop_back(); + Scene::current_ = sceneStack_.empty() ? nullptr : sceneStack_.back().Get(); if (!sceneStack_.empty()) { sceneStack_.back()->onEnter(); } @@ -42,15 +43,82 @@ void SceneManager::ReplaceScene(Ptr scene) { return; } - PopScene(); - PushScene(scene); + if (!sceneStack_.empty()) { + sceneStack_.back()->onExit(); + sceneStack_.pop_back(); + } + + sceneStack_.push_back(scene); + Scene::current_ = scene.Get(); + scene->onEnter(); +} + +void SceneManager::PushUIScene(Ptr 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 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() { + ClearUIScenes(); + while (!sceneStack_.empty()) { sceneStack_.back()->onExit(); sceneStack_.pop_back(); } + + Scene::current_ = nullptr; } 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() { + Renderer& renderer = Renderer::get(); + Camera* worldCamera = renderer.getCamera(); + if (!sceneStack_.empty()) { 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 { @@ -72,9 +183,19 @@ Scene* SceneManager::GetCurrentScene() const { return sceneStack_.back().Get(); } +UIScene* SceneManager::GetCurrentUIScene() const { + if (uiSceneStack_.empty()) { + return nullptr; + } + return uiSceneStack_.back().Get(); +} + bool SceneManager::HasActiveScene() const { return !sceneStack_.empty(); } +bool SceneManager::HasActiveUIScene() const { + return !uiSceneStack_.empty(); } +} // namespace frostbite2D diff --git a/Frostbite2D/src/frostbite2D/scene/ui_scene.cpp b/Frostbite2D/src/frostbite2D/scene/ui_scene.cpp new file mode 100644 index 0000000..1a3660a --- /dev/null +++ b/Frostbite2D/src/frostbite2D/scene/ui_scene.cpp @@ -0,0 +1,11 @@ +#include + +namespace frostbite2D { + +UIScene::UIScene() { + camera_.setFlipY(true); + camera_.setZoom(1.0f); + camera_.setPosition(Vec2::Zero()); +} + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index 8942889..dc682dd 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace frostbite2D { @@ -44,6 +45,7 @@ private: Animation* GetPrimaryAnimation(const std::string& actionName) const; Animation* GetCurrentPrimaryAnimation() const; void RefreshRuntimeCallbacks(); + bool shouldSkipMissingAnimation(const std::string& aniPath); void CreateAnimationBySlot(const std::string& actionName, const std::string& slotName, @@ -57,6 +59,8 @@ private: int direction_ = 1; ActionFrameFlagCallback actionFrameFlagCallback_; ActionEndCallback actionEndCallback_; + std::unordered_set missingAnimationPaths_; + size_t skippedMissingAnimationCount_ = 0; }; } // namespace frostbite2D diff --git a/Game/include/common/GameDebugActor.h b/Game/include/common/GameDebugActor.h new file mode 100644 index 0000000..8d65f5e --- /dev/null +++ b/Game/include/common/GameDebugActor.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +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 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 background_; + RefPtr coordText_; +}; + +} // namespace frostbite2D diff --git a/Game/include/map/GameDataLoader.h b/Game/include/map/GameDataLoader.h index fbfb54b..1cdc5e2 100644 --- a/Game/include/map/GameDataLoader.h +++ b/Game/include/map/GameDataLoader.h @@ -63,7 +63,7 @@ struct MapConfig { std::vector backgroundAnimations; std::vector mapAnimations; std::vector soundIds; - std::vector virtualMovableAreas; + std::vector virtualMovablePolygon; std::vector townMovableAreas; std::vector townMovableAreaTargets; }; diff --git a/Game/include/map/GameMap.h b/Game/include/map/GameMap.h index 89e7b34..6f3437a 100644 --- a/Game/include/map/GameMap.h +++ b/Game/include/map/GameMap.h @@ -49,6 +49,7 @@ public: Rect GetMovablePositionArea(size_t index) const; int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; } + bool IsDebugModeEnabled() const { return debugMode_; } private: /// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。 @@ -75,14 +76,14 @@ private: /// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。 RefPtr tileRoot_; /// 角色可行走矩形区域。 - std::vector movableArea_; + std::vector movablePolygon_; /// 进入后会触发切图/传送的矩形区域。 std::vector moveArea_; /// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。 int backgroundRepeatWidth_ = 0; /// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。 int mapOffsetY_ = 0; - bool debugMode_ = false; + bool debugMode_ = true; /// 当前地图正在播放的背景音乐。 Ptr currentMusic_; }; diff --git a/Game/include/map/GameMapLayer.h b/Game/include/map/GameMapLayer.h index eb40374..f76e7c0 100644 --- a/Game/include/map/GameMapLayer.h +++ b/Game/include/map/GameMapLayer.h @@ -9,11 +9,14 @@ class GameMapLayer : public Actor { public: void Render() override; - void AddDebugFeasibleAreaInfo(const Rect& rect, int type); + void SetDebugFeasibleAreaPolygon(const std::vector& polygon); + void AddDebugMoveAreaInfo(const Rect& rect); + void ClearDebugAreaInfo(); void AddObject(RefPtr obj); private: - std::vector feasibleAreaInfoList_; + std::vector feasibleAreaPolygon_; + std::vector feasibleAreaFillRects_; std::vector moveAreaInfoList_; }; diff --git a/Game/include/scene/GameDebugUIScene.h b/Game/include/scene/GameDebugUIScene.h new file mode 100644 index 0000000..0af034d --- /dev/null +++ b/Game/include/scene/GameDebugUIScene.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +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 diff --git a/Game/include/scene/GameMapTestScene.h b/Game/include/scene/GameMapTestScene.h index 23efda2..7873826 100644 --- a/Game/include/scene/GameMapTestScene.h +++ b/Game/include/scene/GameMapTestScene.h @@ -1,8 +1,9 @@ -#pragma once +#pragma once #include "camera/GameCameraController.h" #include "character/CharacterObject.h" #include "map/GameMap.h" +#include "scene/GameDebugUIScene.h" #include namespace frostbite2D { @@ -19,6 +20,7 @@ public: private: GameCameraController cameraController_; RefPtr character_; + RefPtr debugScene_; bool initialized_ = false; RefPtr map_; }; diff --git a/Game/include/ui/NineSliceActor.h b/Game/include/ui/NineSliceActor.h new file mode 100644 index 0000000..417ac80 --- /dev/null +++ b/Game/include/ui/NineSliceActor.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include + +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 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, const Vec2& position, + const Vec2& size); + Vec2 getNaturalSize() const; + + std::string imgPath_; + size_t startIndex_ = 0; + std::array, SliceCount> sliceSprites_; + std::array sliceNaturalSizes_ = {}; +}; + +} // namespace frostbite2D diff --git a/Game/src/bootstrap/main.cpp b/Game/src/bootstrap/main.cpp index 9c451c9..df100e0 100644 --- a/Game/src/bootstrap/main.cpp +++ b/Game/src/bootstrap/main.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "scene/GameMapTestScene.h" #include "world/GameWorld.h" @@ -28,6 +29,9 @@ int main(int argc, char **argv) { (void)argc; (void)argv; + StartupTrace::reset("main entry"); + StartupTrace::mark("main entry"); + AppConfig config = AppConfig::createDefault(); config.appName = "Frostbite2D Test App"; config.appVersion = "1.0.0"; @@ -36,79 +40,117 @@ int main(int argc, char **argv) { config.windowConfig.title = "Frostbite2D - Async Init Demo"; Application &app = Application::get(); - if (!app.init(config)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Failed to initialize application!"); - return -1; + { + ScopedStartupTrace startupTrace("Application::init"); + if (!app.init(config)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to initialize application!"); + return -1; + } } + StartupTrace::mark("Application::init complete"); app.run([]() { - auto &fontManager = FontManager::get(); - fontManager.init(); - fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf", - 12); + ScopedStartupTrace startupTrace("Application::run startup callback"); - auto &audioSystem = AudioSystem::get(); - audioSystem.init(); - audioSystem.setMasterVolume(1.0f); - audioSystem.setSoundVolume(0.8f); - audioSystem.setMusicVolume(0.6f); + { + ScopedStartupTrace stageTrace("FontManager startup"); + auto &fontManager = FontManager::get(); + fontManager.init(); + fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf", + 12); + } + + { + ScopedStartupTrace stageTrace("AudioSystem startup"); + auto &audioSystem = AudioSystem::get(); + audioSystem.init(); + audioSystem.setMasterVolume(1.0f); + audioSystem.setSoundVolume(0.8f); + audioSystem.setMusicVolume(0.6f); + } SDL_Log("游戏启动"); - auto LoadingScene = MakePtr(); - SceneManager::get().PushScene(LoadingScene); + { + ScopedStartupTrace stageTrace("Loading scene bootstrap"); + auto LoadingScene = MakePtr(); + SceneManager::get().PushScene(LoadingScene); - auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg"); - Background->SetSize(1280, 720); - LoadingScene->AddChild(Background); + auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg"); + Background->SetSize(1280, 720); + LoadingScene->AddChild(Background); - auto BackgroundBar = - Sprite::createFromFile("assets/ImagePacks2/Loading1.png"); - BackgroundBar->SetPosition(0, 686); - LoadingScene->AddChild(BackgroundBar); + auto BackgroundBar = + Sprite::createFromFile("assets/ImagePacks2/Loading1.png"); + BackgroundBar->SetPosition(0, 686); + LoadingScene->AddChild(BackgroundBar); - auto LoadCircleSp = - Sprite::createFromFile("assets/ImagePacks2/Loading2.png"); - LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f)); - LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60); - LoadCircleSp->SetBlendMode(BlendMode::Additive); - LoadCircleSp->AddUpdateListener([](Actor &self, float dt) { - auto rotation = self.GetRotation(); - self.SetRotation(rotation + 180.0f * dt); - }); - LoadingScene->AddChild(LoadCircleSp); + auto LoadCircleSp = + Sprite::createFromFile("assets/ImagePacks2/Loading2.png"); + LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f)); + LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60); + LoadCircleSp->SetBlendMode(BlendMode::Additive); + LoadCircleSp->AddUpdateListener([](Actor &self, float dt) { + auto rotation = self.GetRotation(); + self.SetRotation(rotation + 180.0f * dt); + }); + LoadingScene->AddChild(LoadCircleSp); + } + + StartupTrace::mark("loading scene ready"); TaskSystem::get().submitThen( []() -> std::string { + ScopedStartupTrace startupTrace("Async resource bootstrap"); SDL_Log("Async init task on main thread: %s", TaskSystem::get().isMainThread() ? "true" : "false"); - auto &pvf = PvfArchive::get(); - if (!pvf.open("assets/Script.pvf")) { - throw std::runtime_error("Failed to open assets/Script.pvf"); + { + ScopedStartupTrace stageTrace("PvfArchive open+init"); + auto &pvf = PvfArchive::get(); + if (!pvf.open("assets/Script.pvf")) { + throw std::runtime_error("Failed to open assets/Script.pvf"); + } + pvf.init(); } - pvf.init(); - auto &npk = NpkArchive::get(); - npk.setImagePackDirectory("assets/ImagePacks2"); - npk.setDefaultImg("sprite/interface/base.img", 0); - npk.init(); + { + ScopedStartupTrace stageTrace("NpkArchive init"); + auto &npk = NpkArchive::get(); + npk.setImagePackDirectory("assets/ImagePacks2"); + npk.setDefaultImg("sprite/interface/base.img", 0); + npk.init(); + } - auto &archive = SoundPackArchive::get(); - archive.setSoundPackDirectory("assets/SoundPacks"); - archive.init(); + { + ScopedStartupTrace stageTrace("SoundPackArchive init"); + auto &archive = SoundPackArchive::get(); + archive.setSoundPackDirectory("assets/SoundPacks"); + archive.init(); + } - auto &audioDatabase = AudioDatabase::get(); - audioDatabase.loadFromFile("assets/audio.xml"); + { + ScopedStartupTrace stageTrace("AudioDatabase load"); + auto &audioDatabase = AudioDatabase::get(); + audioDatabase.loadFromFile("assets/audio.xml"); + } return "后台资源加载成功"; }, [](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"); - auto testMapScene = MakePtr(); - SceneManager::get().ReplaceScene(testMapScene); + { + ScopedStartupTrace stageTrace( + "SceneManager::ReplaceScene(GameMapTestScene)"); + auto testMapScene = MakePtr(); + SceneManager::get().ReplaceScene(testMapScene); + } + + StartupTrace::mark("after SceneManager::ReplaceScene"); }, [](std::exception_ptr error) { try { diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp index 71858b2..79ecd50 100644 --- a/Game/src/character/CharacterAnimation.cpp +++ b/Game/src/character/CharacterAnimation.cpp @@ -1,5 +1,6 @@ #include "character/CharacterAnimation.h" #include "character/CharacterObject.h" +#include #include #include #include @@ -38,6 +39,8 @@ bool CharacterAnimation::Init(CharacterObject* parent, const CharacterEquipmentManager& equipmentManager) { parent_ = parent; actionAnimations_.clear(); + missingAnimationPaths_.clear(); + skippedMissingAnimationCount_ = 0; currentActionTag_.clear(); direction_ = 1; actionFrameFlagCallback_ = nullptr; @@ -56,6 +59,12 @@ bool CharacterAnimation::Init(CharacterObject* parent, 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; } @@ -77,6 +86,10 @@ void CharacterAnimation::CreateAnimationBySlot( const character::CharacterConfig& config, const CharacterEquipmentManager& equipmentManager) { if (slotName == std::string("skin_avatar")) { + if (shouldSkipMissingAnimation(actionPath)) { + return; + } + Animation::ReplaceData replaceData(0, 0); if (const auto* equip = equipmentManager.GetEquip(slotName)) { auto it = equip->jobAnimations.find(config.jobId); @@ -114,6 +127,10 @@ void CharacterAnimation::CreateAnimationBySlot( } std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail; + if (shouldSkipMissingAnimation(aniPath)) { + continue; + } + auto animation = MakePtr( aniPath, FormatImgPath, 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 { if (actionAnimations_.empty()) { return ""; diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index d0c9b5c..8445b4a 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -49,6 +50,7 @@ int32 RoundWorldCoordinate(float value) { } // namespace bool CharacterObject::Construction(int jobId) { + ScopedStartupTrace startupTrace("CharacterObject::Construction"); // Reset all runtime state before rebuilding the character from config. EnableEventReceive(); RemoveAllChildren(); @@ -67,7 +69,10 @@ bool CharacterObject::Construction(int jobId) { lastDeltaTime_ = 0.0f; inputEnabled_ = true; - auto config = character::loadCharacterConfig(jobId); + auto config = [&]() { + ScopedStartupTrace stageTrace("character::loadCharacterConfig"); + return character::loadCharacterConfig(jobId); + }(); if (!config) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "CharacterObject: failed to load job %d config", jobId); @@ -79,24 +84,36 @@ bool CharacterObject::Construction(int jobId) { direction_ = 1; config_ = *config; stateMachine_.Configure(*config_); - equipmentManager_.Init(config_->baseJobConfig); - if (auto actionLibrary = loadCharacterActionLibrary(*config_)) { + { + ScopedStartupTrace stageTrace("CharacterEquipmentManager::Init"); + equipmentManager_.Init(config_->baseJobConfig); + } + if (auto actionLibrary = [&]() { + ScopedStartupTrace stageTrace("loadCharacterActionLibrary"); + return loadCharacterActionLibrary(*config_); + }()) { actionLibrary_ = *actionLibrary; } animationManager_ = MakePtr(); - if (!animationManager_->Init(this, *config_, equipmentManager_)) { - ReportFatalCharacterError("CharacterObject::Construction", - "no usable animation tags were loaded"); - return false; + { + ScopedStartupTrace stageTrace("CharacterAnimation::Init"); + if (!animationManager_->Init(this, *config_, equipmentManager_)) { + ReportFatalCharacterError("CharacterObject::Construction", + "no usable animation tags were loaded"); + return false; + } } AddChild(animationManager_); - if (!RequireAction("idle", "CharacterObject::Construction")) { - return false; - } - if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) { - return false; + { + ScopedStartupTrace stageTrace("CharacterObject initial action setup"); + if (!RequireAction("idle", "CharacterObject::Construction")) { + return false; + } + if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) { + return false; + } } SetWorldPosition({}); diff --git a/Game/src/common/GameDebugActor.cpp b/Game/src/common/GameDebugActor.cpp new file mode 100644 index 0000000..bca9a77 --- /dev/null +++ b/Game/src/common/GameDebugActor.cpp @@ -0,0 +1,147 @@ +#include "common/GameDebugActor.h" +#include "character/CharacterObject.h" +#include "map/GameMap.h" +#include "ui/NineSliceActor.h" +#include +#include +#include +#include + +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::max()); + initOverlay(); +} + +GameDebugActor& GameDebugActor::get() { + return *getPtr(); +} + +Ptr GameDebugActor::getPtr() { + static Ptr 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 self = getPtr(); + if (GetParent() == parent) { + return true; + } + + if (GetParent()) { + GetParent()->RemoveChild(self); + } + + parent->AddChild(self); + return GetParent() == parent; +} + +void GameDebugActor::DetachFromParent() { + Ptr 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 diff --git a/Game/src/map/GameDataLoader.cpp b/Game/src/map/GameDataLoader.cpp index 00ebb81..b48eccf 100644 --- a/Game/src/map/GameDataLoader.cpp +++ b/Game/src/map/GameDataLoader.cpp @@ -268,16 +268,39 @@ bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) { outConfig.townMovableAreaTargets.push_back(target); } } else if (segment == "[virtual movable area]") { + std::vector polygonCoords; while (!stream.isEnd()) { std::string token = stream.get(); if (token == "[/virtual movable area]") { break; } - Rect rect(static_cast(toInt(token)), - static_cast(toInt(stream.get())), - static_cast(toInt(stream.get())), - static_cast(toInt(stream.get()))); - outConfig.virtualMovableAreas.push_back(rect); + polygonCoords.push_back(toInt(token)); + } + + if (polygonCoords.empty()) { + 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(polygonCoords[i]), + static_cast(polygonCoords[i + 1])); } } } diff --git a/Game/src/map/GameMap.cpp b/Game/src/map/GameMap.cpp index 37c38f7..c4b5e05 100644 --- a/Game/src/map/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,7 @@ static const int kLayerOrders[] = { constexpr int kTileRootZOrder = -1000000; constexpr float kExtendedTileStepY = 120.0f; +constexpr double kPolygonEpsilon = 0.001; int RoundWorldCoordinate(float value) { return static_cast(std::lround(value)); @@ -37,6 +39,57 @@ Vec3 MakeIntegerWorldPosition(int x, int y, int z) { static_cast(z)); } +bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) { + Vec2 segment = end - start; + Vec2 toPoint = point - start; + double cross = static_cast(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& 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(start.x) + + (static_cast(point.y) - static_cast(start.y)) * + (static_cast(end.x) - static_cast(start.x)) / + (static_cast(end.y) - static_cast(start.y)); + if (intersectX >= static_cast(point.x) - kPolygonEpsilon) { + inside = !inside; + } + } + + return inside; +} + +bool IsPointMovable(const std::vector& polygon, int x, int y) { + return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y)); +} + // 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite // 占位,避免整张地图中断。 Ptr createMapSprite(const std::string& path, int index) { @@ -70,33 +123,49 @@ void GameMap::clearLayerChildren() { for (auto& [_, layer] : layerMap_) { if (layer) { layer->RemoveAllChildren(); + layer->ClearDebugAreaInfo(); } } tileRoot_.Reset(); - movableArea_.clear(); + movablePolygon_.clear(); moveArea_.clear(); currentMusic_.Reset(); backgroundRepeatWidth_ = 0; } bool GameMap::LoadMap(const std::string &mapName) { - // 清空所有图层子节点。 + ScopedStartupTrace startupTrace("GameMap::LoadMap"); clearLayerChildren(); - // 读取PVF地图配置。 - if (!game::loadMapConfig(mapName, mapConfig_)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s", - mapName.c_str()); - return false; + { + ScopedStartupTrace stageTrace("game::loadMapConfig"); + if (!game::loadMapConfig(mapName, mapConfig_)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "GameMap: failed to load map %s", mapName.c_str()); + return false; + } } - // backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。 mapOffsetY_ = mapConfig_.backgroundPos; - // 加载顺序基本就是地图的组装顺序:先地板,再背景,再对象,再可行走数据。 - InitTile(); - InitBackgroundAnimation(); - InitMapAnimation(); - InitVirtualMovableArea(); - InitMoveArea(); + { + ScopedStartupTrace stageTrace("GameMap::InitTile"); + InitTile(); + } + { + ScopedStartupTrace stageTrace("GameMap::InitBackgroundAnimation"); + InitBackgroundAnimation(); + } + { + ScopedStartupTrace stageTrace("GameMap::InitMapAnimation"); + InitMapAnimation(); + } + { + ScopedStartupTrace stageTrace("GameMap::InitVirtualMovableArea"); + InitVirtualMovableArea(); + } + { + ScopedStartupTrace stageTrace("GameMap::InitMoveArea"); + InitMoveArea(); + } return true; } @@ -248,26 +317,48 @@ void GameMap::InitMapAnimation() { } void GameMap::InitVirtualMovableArea() { - movableArea_ = mapConfig_.virtualMovableAreas; - // debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。 + movablePolygon_ = mapConfig_.virtualMovablePolygon; + 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_) { return; } - for (const auto& rect : movableArea_) { - layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0); + auto layerIt = layerMap_.find("max"); + if (layerIt == layerMap_.end() || !layerIt->second) { + return; } + layerIt->second->SetDebugFeasibleAreaPolygon(movablePolygon_); } void GameMap::InitMoveArea() { moveArea_ = mapConfig_.townMovableAreas; - // move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。 if (!debugMode_) { return; } + auto layerIt = layerMap_.find("max"); + if (layerIt == layerMap_.end() || !layerIt->second) { + return; + } + for (const auto& rect : moveArea_) { - layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1); + layerIt->second->AddDebugMoveAreaInfo(rect); } } @@ -357,7 +448,7 @@ void GameMap::AddObject(RefPtr object) { if (!object) { return; } - // 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。 + // Keep dynamic objects on the normal layer and sort them by y. object->SetZOrder(static_cast(object->GetPosition().y)); layerMap_["normal"]->AddObject(object); } @@ -365,42 +456,41 @@ void GameMap::AddObject(RefPtr object) { Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { int currentX = RoundWorldCoordinate(curPos.x); 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 targetY = RoundWorldCoordinate(curPos.y + posOffset.y); - - // X/Y - // Check X and Y separately so edge sliding does not lock both axes. - bool isXValid = false; - for (const auto& area : movableArea_) { - if (area.containsPoint(MakeIntegerWorldPoint(targetX, currentY))) { - isXValid = true; - break; - } + int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z); + Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ); + if (movablePolygon_.size() < 3) { + return MakeIntegerWorldPosition(targetX, targetY, targetZ); + } + // 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(targetX); + result.y = static_cast(targetY); + return result; } - bool isYValid = false; - for (const auto& area : movableArea_) { - if (area.containsPoint(MakeIntegerWorldPoint(currentX, targetY))) { - isYValid = true; - break; + bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY); + bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY); + + if (isXValid && isYValid) { + int moveX = std::abs(targetX - currentX); + int moveY = std::abs(targetY - currentY); + if (moveX >= moveY) { + result.x = static_cast(targetX); + } else { + result.y = static_cast(targetY); } + return result; } if (isXValid) { result.x = static_cast(targetX); - } - if (isYValid) { + } else if (isYValid) { result.y = static_cast(targetY); } - result.z = static_cast(RoundWorldCoordinate(curPos.z + posOffset.z)); + return result; } diff --git a/Game/src/map/GameMapLayer.cpp b/Game/src/map/GameMapLayer.cpp index 9785406..116d433 100644 --- a/Game/src/map/GameMapLayer.cpp +++ b/Game/src/map/GameMapLayer.cpp @@ -1,32 +1,155 @@ #include "map/GameMapLayer.h" +#include +#include #include 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 BuildPolygonFillRects(const std::vector& polygon) { + std::vector 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(std::floor(minY)); + int scanlineMaxY = static_cast(std::ceil(maxY)) - 1; + if (scanlineMaxY < scanlineMinY) { + return fillRects; + } + + for (int y = scanlineMinY; y <= scanlineMaxY; ++y) { + float scanY = static_cast(y) + 0.5f; + std::vector 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(y), width, 1.0f); + } + } + } + + return fillRects; +} + +void DrawPolygonOutline(const std::vector& 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(std::ceil(length / kDebugEdgeStep))); + + for (int step = 0; step <= steps; ++step) { + float t = static_cast(step) / static_cast(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& 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() { Actor::Render(); 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.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_) { Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, 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) { - if (type == 0) { - feasibleAreaInfoList_.push_back(rect); - } else if (type == 1) { - moveAreaInfoList_.push_back(rect); - } +void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector& polygon) { + feasibleAreaPolygon_ = polygon; + feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_); +} + +void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect) { + moveAreaInfoList_.push_back(rect); +} + +void GameMapLayer::ClearDebugAreaInfo() { + feasibleAreaPolygon_.clear(); + feasibleAreaFillRects_.clear(); + moveAreaInfoList_.clear(); } void GameMapLayer::AddObject(RefPtr obj) { diff --git a/Game/src/scene/GameDebugUIScene.cpp b/Game/src/scene/GameDebugUIScene.cpp new file mode 100644 index 0000000..23e7191 --- /dev/null +++ b/Game/src/scene/GameDebugUIScene.cpp @@ -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 diff --git a/Game/src/scene/GameMapTestScene.cpp b/Game/src/scene/GameMapTestScene.cpp index a1da672..88c453b 100644 --- a/Game/src/scene/GameMapTestScene.cpp +++ b/Game/src/scene/GameMapTestScene.cpp @@ -1,5 +1,7 @@ #include "scene/GameMapTestScene.h" #include +#include +#include namespace frostbite2D { @@ -12,31 +14,48 @@ constexpr char kTestMapPath[] = "map/elvengard/elvengard.map"; GameMapTestScene::GameMapTestScene() = default; void GameMapTestScene::onEnter() { + ScopedStartupTrace startupTrace("GameMapTestScene::onEnter"); Scene::onEnter(); + + if (!debugScene_) { + debugScene_ = MakePtr(); + } + SceneManager::get().RemoveUIScene(debugScene_.Get()); + SceneManager::get().PushUIScene(debugScene_); + if (initialized_) { + debugScene_->SetDebugContext(map_.Get(), character_.Get()); return; } - - map_ = MakePtr(); - if (!map_->LoadMap(kTestMapPath)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "GameMapTestScene: failed to load map %s", kTestMapPath); - map_.Reset(); - return; + + { + ScopedStartupTrace stageTrace("GameMapTestScene map load"); + map_ = MakePtr(); + if (!map_->LoadMap(kTestMapPath)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "GameMapTestScene: failed to load map %s", kTestMapPath); + map_.Reset(); + debugScene_->ClearDebugContext(); + return; + } } AddChild(map_); - character_ = MakePtr(); - if (!character_->Construction(0)) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "GameMapTestScene: failed to construct default character"); - character_.Reset(); - } else { - Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f); - character_->SetCharacterPosition(spawnPos); - character_->EnableEventReceive(); - character_->SetEventPriority(-100); - map_->AddObject(character_); + { + ScopedStartupTrace stageTrace("GameMapTestScene character construction"); + character_ = MakePtr(); + if (!character_->Construction(0)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameMapTestScene: failed to construct default character"); + character_.Reset(); + } else { + Vec2 spawnPos = + map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f); + character_->SetCharacterPosition(spawnPos); + character_->EnableEventReceive(); + character_->SetEventPriority(-100); + map_->AddObject(character_); + } } cameraController_.SetMap(map_.Get()); @@ -49,11 +68,20 @@ void GameMapTestScene::onEnter() { cameraController_.ClearTarget(); cameraController_.SnapToDefaultFocus(); } - map_->Enter(); + { + ScopedStartupTrace stageTrace("GameMap::Enter"); + map_->Enter(); + } + debugScene_->SetDebugContext(map_.Get(), character_.Get()); initialized_ = true; } void GameMapTestScene::onExit() { + if (debugScene_) { + debugScene_->ClearDebugContext(); + SceneManager::get().RemoveUIScene(debugScene_.Get()); + } + Scene::onExit(); } diff --git a/Game/src/ui/NineSliceActor.cpp b/Game/src/ui/NineSliceActor.cpp new file mode 100644 index 0000000..79beac8 --- /dev/null +++ b/Game/src/ui/NineSliceActor.cpp @@ -0,0 +1,203 @@ +#include "ui/NineSliceActor.h" + +#include +#include +#include + +#include + +namespace frostbite2D { + +namespace { + +constexpr std::array 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::createFromNpk(const std::string& imgPath, + size_t startIndex) { + auto actor = MakePtr(); + 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, SliceCount> newSlices; + std::array 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(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(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, 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 diff --git a/platform/mingw.lua b/platform/mingw.lua index fce0613..0dce1de 100644 --- a/platform/mingw.lua +++ b/platform/mingw.lua @@ -40,7 +40,7 @@ target("Frostbite2D") -- 复制所有依赖的 DLL (Windows 平台) if is_plat("mingw") or is_plat("windows") then -- 复制所有包的 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 local pkg = target:pkg(pkg_name) if pkg then