feat(场景管理): 添加UIScene支持并重构场景管理器
refactor(渲染器): 优化相机切换时的渲染批处理 feat(调试工具): 新增游戏调试UI场景和九宫格面板组件 fix(动画系统): 跳过缺失的动画资源加载并记录日志 perf(资源加载): 使用BinaryFileStreamReader优化NPK文件解析 feat(地图系统): 支持多边形可行走区域调试显示 style(代码格式): 清理多余空格和统一文件编码 docs(注释): 补充关键类和方法的文档说明 test(启动跟踪): 添加启动过程性能跟踪工具 chore(依赖): 添加SDL2_ttf库支持
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class BinaryFileStreamReader {
|
||||
public:
|
||||
BinaryFileStreamReader() = default;
|
||||
explicit BinaryFileStreamReader(const std::string& filePath);
|
||||
~BinaryFileStreamReader();
|
||||
|
||||
bool open(const std::string& filePath);
|
||||
void close();
|
||||
bool isOpen() const;
|
||||
|
||||
size_t tell() const;
|
||||
void seek(size_t pos);
|
||||
void skip(size_t count);
|
||||
bool eof() const;
|
||||
|
||||
size_t size() const;
|
||||
size_t remaining() const;
|
||||
size_t lastReadCount() const;
|
||||
|
||||
size_t read(char* buffer, size_t size);
|
||||
std::vector<uint8> readBytes(size_t size);
|
||||
|
||||
template <typename T>
|
||||
T read() {
|
||||
T value{};
|
||||
read(reinterpret_cast<char*>(&value), sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
int8 readInt8();
|
||||
int16 readInt16();
|
||||
int32 readInt32();
|
||||
int64 readInt64();
|
||||
uint8 readUInt8();
|
||||
uint16 readUInt16();
|
||||
uint32 readUInt32();
|
||||
uint64 readUInt64();
|
||||
float readFloat();
|
||||
double readDouble();
|
||||
|
||||
std::string readString(size_t length);
|
||||
std::string readNullTerminatedString(size_t maxLength = 4096);
|
||||
|
||||
private:
|
||||
std::ifstream stream_;
|
||||
std::string filePath_;
|
||||
size_t size_ = 0;
|
||||
size_t position_ = 0;
|
||||
size_t lastReadCount_ = 0;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,18 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <frostbite2D/resource/binary_reader.h>
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <zlib.h>
|
||||
|
||||
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<SourceFileState> collectSourceFileStates() const;
|
||||
bool loadIndexCache(const std::vector<SourceFileState>& sourceFiles);
|
||||
bool saveIndexCache(const std::vector<SourceFileState>& 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<std::string, ImgRef> imgIndex_;
|
||||
std::map<std::string, CachedImageData> imageCache_;
|
||||
std::unordered_map<std::string, ImgRef> imgIndex_;
|
||||
std::unordered_map<std::string, CachedImageData> imageCache_;
|
||||
std::list<std::string> lruList_;
|
||||
std::unordered_map<std::string, std::list<std::string>::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
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <frostbite2D/resource/binary_reader.h>
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
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<char[]> data; ///< 原始数据指针(智能指针自动管理内存)
|
||||
size_t size; ///< 数据大小(字节)
|
||||
std::unique_ptr<char[]> 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<PvfFileInfo> getFileInfo(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* @brief 获取所有文件路径列表
|
||||
* @return 文件路径列表
|
||||
*/
|
||||
std::vector<std::string> listFiles() const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 文件内容读取
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 获取文件内容作为字符串
|
||||
* @param path 文件路径
|
||||
* @return 文件内容,失败返回 std::nullopt
|
||||
*/
|
||||
std::optional<std::string> getFileContent(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief 获取文件内容作为字节数组
|
||||
* @param path 文件路径
|
||||
* @return 字节数组,失败返回 std::nullopt
|
||||
*/
|
||||
std::optional<std::vector<uint8>> getFileBytes(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief 获取文件原始数据(使用智能指针管理内存,已包含 CRC 解密)
|
||||
*
|
||||
* 与原始 GetFileContentChar 功能相同,但使用智能指针避免内存泄漏。
|
||||
* 数据会在首次访问时自动进行 CRC 解密。
|
||||
*
|
||||
* @param path 文件路径
|
||||
* @return RawData 结构体,包含数据指针和大小;失败返回 std::nullopt
|
||||
*/
|
||||
std::optional<RawData> getFileRawData(const std::string& path);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 字符串资源访问
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 获取二进制字符串
|
||||
* @param key 字符串键
|
||||
* @return 字符串,不存在返回 std::nullopt
|
||||
*/
|
||||
std::optional<std::string> getBinString(int key) const;
|
||||
|
||||
/**
|
||||
* @brief 获取本地化字符串
|
||||
* @param type 字符串类型
|
||||
* @param key 字符串键
|
||||
* @return 字符串,不存在返回 std::nullopt
|
||||
*/
|
||||
std::optional<std::string> getLoadString(const std::string& type, const std::string& key) const;
|
||||
|
||||
/**
|
||||
* @brief 检查二进制字符串是否存在
|
||||
* @param key 字符串键
|
||||
* @return 存在返回 true
|
||||
*/
|
||||
std::optional<std::string> 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<std::string> splitString(const std::string& str, const std::string& delimiter) const;
|
||||
std::vector<std::string> splitString(const std::string& str,
|
||||
const std::string& delimiter) const;
|
||||
bool decodeFile(const std::string& normalizedPath, PvfFileInfo& info);
|
||||
void clearInitData();
|
||||
std::optional<std::vector<uint8>> readArchiveBytes(size_t absoluteOffset,
|
||||
size_t length);
|
||||
std::optional<std::vector<uint8>> readDecodedFileBytes(
|
||||
const PvfFileInfo& info);
|
||||
static void crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32);
|
||||
|
||||
/**
|
||||
* @brief 解码文件内容(内部使用)
|
||||
* @param info 文件信息(会修改 decoded 标志)
|
||||
* @return 成功返回 true
|
||||
*/
|
||||
bool decodeFile(PvfFileInfo& info);
|
||||
|
||||
BinaryReader reader_; ///< 二进制读取器
|
||||
size_t dataStartPos_ = 0; ///< 数据起始位置
|
||||
std::map<std::string, PvfFileInfo> fileInfo_; ///< 文件信息映射
|
||||
std::map<int, std::string> binStringTable_; ///< 二进制字符串表
|
||||
std::map<std::string, std::map<std::string, std::string>> loadStrings_; ///< 本地化字符串
|
||||
BinaryFileStreamReader reader_;
|
||||
size_t dataStartPos_ = 0;
|
||||
std::map<std::string, PvfFileInfo> fileInfo_;
|
||||
std::map<int, std::string> binStringTable_;
|
||||
std::map<std::string, std::map<std::string, std::string>> loadStrings_;
|
||||
std::map<std::string, std::vector<uint8>> decodedFileCache_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/event/event.h>
|
||||
#include <frostbite2D/types/type_alias.h>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class Scene;
|
||||
class UIScene;
|
||||
|
||||
class SceneManager {
|
||||
public:
|
||||
@@ -17,20 +19,31 @@ public:
|
||||
void PushScene(Ptr<Scene> scene);
|
||||
void PopScene();
|
||||
void ReplaceScene(Ptr<Scene> scene);
|
||||
|
||||
void PushUIScene(Ptr<UIScene> scene);
|
||||
void PopUIScene();
|
||||
void ReplaceUIScene(Ptr<UIScene> scene);
|
||||
bool RemoveUIScene(UIScene* scene);
|
||||
void ClearUIScenes();
|
||||
void ClearAll();
|
||||
|
||||
void 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<Ptr<Scene>> sceneStack_;
|
||||
std::vector<Ptr<UIScene>> uiSceneStack_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
20
Frostbite2D/include/frostbite2D/scene/ui_scene.h
Normal file
20
Frostbite2D/include/frostbite2D/scene/ui_scene.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/graphics/camera.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class UIScene : public Scene {
|
||||
public:
|
||||
UIScene();
|
||||
~UIScene() override = default;
|
||||
|
||||
Camera* GetCamera() { return &camera_; }
|
||||
const Camera* GetCamera() const { return &camera_; }
|
||||
|
||||
private:
|
||||
Camera camera_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
152
Frostbite2D/include/frostbite2D/utils/startup_trace.h
Normal file
152
Frostbite2D/include/frostbite2D/utils/startup_trace.h
Normal file
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class StartupTrace {
|
||||
public:
|
||||
static constexpr bool enabled() {
|
||||
#ifndef NDEBUG
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void reset(const char* label = nullptr) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
std::string line;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex());
|
||||
startCounter() = now;
|
||||
if (label && label[0] != '\0') {
|
||||
line = std::string("[Startup] reset: ") + label;
|
||||
clearLogFileUnlocked();
|
||||
writeLineUnlocked(line);
|
||||
} else {
|
||||
clearLogFileUnlocked();
|
||||
}
|
||||
}
|
||||
if (!line.empty()) {
|
||||
SDL_Log("%s", line.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static double totalElapsedMs() {
|
||||
if (!enabled()) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Uint64 start = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex());
|
||||
start = startCounter();
|
||||
}
|
||||
|
||||
if (start == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
Uint64 frequency = SDL_GetPerformanceFrequency();
|
||||
if (frequency == 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return static_cast<double>(now - start) * 1000.0 /
|
||||
static_cast<double>(frequency);
|
||||
}
|
||||
|
||||
static void mark(const char* label) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[256] = {};
|
||||
SDL_snprintf(buffer, sizeof(buffer),
|
||||
"[Startup] %s reached at %.2f ms (thread=%u)",
|
||||
label ? label : "<unnamed>", totalElapsedMs(), SDL_ThreadID());
|
||||
logLine(buffer);
|
||||
SDL_Log("%s", buffer);
|
||||
}
|
||||
|
||||
static void logLine(const std::string& line) {
|
||||
if (!enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex());
|
||||
writeLineUnlocked(line);
|
||||
}
|
||||
|
||||
private:
|
||||
static Uint64& startCounter() {
|
||||
static Uint64 counter = 0;
|
||||
return counter;
|
||||
}
|
||||
|
||||
static std::mutex& mutex() {
|
||||
static std::mutex value;
|
||||
return value;
|
||||
}
|
||||
|
||||
static const char* logFilePath() {
|
||||
return "startup_trace.log";
|
||||
}
|
||||
|
||||
static void clearLogFileUnlocked() {
|
||||
std::ofstream stream(logFilePath(), std::ios::trunc);
|
||||
}
|
||||
|
||||
static void writeLineUnlocked(const std::string& line) {
|
||||
std::ofstream stream(logFilePath(), std::ios::app);
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
stream << line << '\n';
|
||||
}
|
||||
};
|
||||
|
||||
class ScopedStartupTrace {
|
||||
public:
|
||||
explicit ScopedStartupTrace(const char* label)
|
||||
: label_(label ? label : "<unnamed>"),
|
||||
startCounter_(SDL_GetPerformanceCounter()) {}
|
||||
|
||||
~ScopedStartupTrace() {
|
||||
if (!StartupTrace::enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
Uint64 frequency = SDL_GetPerformanceFrequency();
|
||||
double elapsedMs = 0.0;
|
||||
if (frequency != 0) {
|
||||
elapsedMs = static_cast<double>(now - startCounter_) * 1000.0 /
|
||||
static_cast<double>(frequency);
|
||||
}
|
||||
|
||||
char buffer[256] = {};
|
||||
SDL_snprintf(buffer, sizeof(buffer),
|
||||
"[Startup] %s took %.2f ms (total %.2f ms, thread=%u)",
|
||||
label_, elapsedMs, StartupTrace::totalElapsedMs(),
|
||||
SDL_ThreadID());
|
||||
StartupTrace::logLine(buffer);
|
||||
SDL_Log("%s", buffer);
|
||||
}
|
||||
|
||||
private:
|
||||
const char* label_;
|
||||
Uint64 startCounter_ = 0;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -3,9 +3,36 @@
|
||||
#include <frostbite2D/graphics/texture.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
Uint32 readSurfacePixel(const SDL_Surface* surface, int x, int y) {
|
||||
const uint8* row =
|
||||
static_cast<const uint8*>(surface->pixels) + y * surface->pitch;
|
||||
const uint8* pixel = row + x * surface->format->BytesPerPixel;
|
||||
|
||||
switch (surface->format->BytesPerPixel) {
|
||||
case 1:
|
||||
return *pixel;
|
||||
case 2:
|
||||
return *reinterpret_cast<const uint16*>(pixel);
|
||||
case 3:
|
||||
if (SDL_BYTEORDER == SDL_BIG_ENDIAN) {
|
||||
return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
|
||||
}
|
||||
return pixel[0] | (pixel[1] << 8) | (pixel[2] << 16);
|
||||
case 4:
|
||||
return *reinterpret_cast<const uint32*>(pixel);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TextSprite::TextSprite()
|
||||
: 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<uint8> rgbaData(width * height * 4);
|
||||
uint8* pixels = static_cast<uint8*>(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<size_t>((y * width + x) * 4);
|
||||
rgbaData[offset + 0] = r;
|
||||
rgbaData[offset + 1] = g;
|
||||
rgbaData[offset + 2] = b;
|
||||
rgbaData[offset + 3] = a;
|
||||
}
|
||||
}
|
||||
|
||||
if (SDL_MUSTLOCK(surface)) {
|
||||
SDL_UnlockSurface(surface);
|
||||
}
|
||||
|
||||
SDL_FreeSurface(surface);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/types/type_math.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
|
||||
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<Event> 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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
#include <frostbite2D/resource/asset.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path toFsPath(const std::string& path) {
|
||||
#ifdef _WIN32
|
||||
return fs::u8path(path);
|
||||
#else
|
||||
return fs::path(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BinaryFileStreamReader::BinaryFileStreamReader(const std::string& filePath) {
|
||||
open(filePath);
|
||||
}
|
||||
|
||||
BinaryFileStreamReader::~BinaryFileStreamReader() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool BinaryFileStreamReader::open(const std::string& filePath) {
|
||||
close();
|
||||
|
||||
Asset& asset = Asset::get();
|
||||
filePath_ = asset.resolvePath(filePath);
|
||||
stream_.open(toFsPath(filePath_), std::ios::binary);
|
||||
if (!stream_.is_open()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"BinaryFileStreamReader: unable to open file: %s",
|
||||
filePath.c_str());
|
||||
filePath_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
stream_.seekg(0, std::ios::end);
|
||||
std::streamoff endPos = stream_.tellg();
|
||||
if (endPos < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"BinaryFileStreamReader: failed to determine file size: %s",
|
||||
filePath_.c_str());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
size_ = static_cast<size_t>(endPos);
|
||||
stream_.seekg(0, std::ios::beg);
|
||||
position_ = 0;
|
||||
lastReadCount_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BinaryFileStreamReader::close() {
|
||||
if (stream_.is_open()) {
|
||||
stream_.close();
|
||||
}
|
||||
filePath_.clear();
|
||||
size_ = 0;
|
||||
position_ = 0;
|
||||
lastReadCount_ = 0;
|
||||
}
|
||||
|
||||
bool BinaryFileStreamReader::isOpen() const {
|
||||
return stream_.is_open();
|
||||
}
|
||||
|
||||
size_t BinaryFileStreamReader::tell() const {
|
||||
return position_;
|
||||
}
|
||||
|
||||
void BinaryFileStreamReader::seek(size_t pos) {
|
||||
if (!isOpen()) {
|
||||
position_ = 0;
|
||||
lastReadCount_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
position_ = std::min(pos, size_);
|
||||
lastReadCount_ = 0;
|
||||
stream_.clear();
|
||||
stream_.seekg(static_cast<std::streamoff>(position_), std::ios::beg);
|
||||
}
|
||||
|
||||
void BinaryFileStreamReader::skip(size_t count) {
|
||||
seek(position_ + count);
|
||||
}
|
||||
|
||||
bool BinaryFileStreamReader::eof() const {
|
||||
return position_ >= size_;
|
||||
}
|
||||
|
||||
size_t BinaryFileStreamReader::size() const {
|
||||
return size_;
|
||||
}
|
||||
|
||||
size_t BinaryFileStreamReader::remaining() const {
|
||||
return position_ >= size_ ? 0 : size_ - position_;
|
||||
}
|
||||
|
||||
size_t BinaryFileStreamReader::lastReadCount() const {
|
||||
return lastReadCount_;
|
||||
}
|
||||
|
||||
size_t BinaryFileStreamReader::read(char* buffer, size_t size) {
|
||||
if (!buffer || size == 0 || eof() || !isOpen()) {
|
||||
lastReadCount_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytesToRead = std::min(size, remaining());
|
||||
stream_.clear();
|
||||
stream_.read(buffer, static_cast<std::streamsize>(bytesToRead));
|
||||
size_t bytesRead = static_cast<size_t>(stream_.gcount());
|
||||
position_ += bytesRead;
|
||||
lastReadCount_ = bytesRead;
|
||||
|
||||
if (bytesRead != bytesToRead) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"BinaryFileStreamReader: incomplete read, expected %zu bytes, got %zu bytes from %s",
|
||||
bytesToRead, bytesRead, filePath_.c_str());
|
||||
}
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
std::vector<uint8> BinaryFileStreamReader::readBytes(size_t size) {
|
||||
std::vector<uint8> result;
|
||||
size_t bytesToRead = std::min(size, remaining());
|
||||
if (bytesToRead == 0) {
|
||||
lastReadCount_ = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.resize(bytesToRead);
|
||||
size_t bytesRead = read(reinterpret_cast<char*>(result.data()), bytesToRead);
|
||||
result.resize(bytesRead);
|
||||
return result;
|
||||
}
|
||||
|
||||
int8 BinaryFileStreamReader::readInt8() {
|
||||
return read<int8>();
|
||||
}
|
||||
|
||||
int16 BinaryFileStreamReader::readInt16() {
|
||||
return read<int16>();
|
||||
}
|
||||
|
||||
int32 BinaryFileStreamReader::readInt32() {
|
||||
return read<int32>();
|
||||
}
|
||||
|
||||
int64 BinaryFileStreamReader::readInt64() {
|
||||
return read<int64>();
|
||||
}
|
||||
|
||||
uint8 BinaryFileStreamReader::readUInt8() {
|
||||
return read<uint8>();
|
||||
}
|
||||
|
||||
uint16 BinaryFileStreamReader::readUInt16() {
|
||||
return read<uint16>();
|
||||
}
|
||||
|
||||
uint32 BinaryFileStreamReader::readUInt32() {
|
||||
return read<uint32>();
|
||||
}
|
||||
|
||||
uint64 BinaryFileStreamReader::readUInt64() {
|
||||
return read<uint64>();
|
||||
}
|
||||
|
||||
float BinaryFileStreamReader::readFloat() {
|
||||
return read<float>();
|
||||
}
|
||||
|
||||
double BinaryFileStreamReader::readDouble() {
|
||||
return read<double>();
|
||||
}
|
||||
|
||||
std::string BinaryFileStreamReader::readString(size_t length) {
|
||||
if (length == 0 || eof() || !isOpen()) {
|
||||
lastReadCount_ = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t bytesToRead = std::min(length, remaining());
|
||||
std::string result(bytesToRead, '\0');
|
||||
size_t bytesRead = read(result.data(), bytesToRead);
|
||||
result.resize(bytesRead);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string BinaryFileStreamReader::readNullTerminatedString(size_t maxLength) {
|
||||
if (maxLength == 0 || eof() || !isOpen()) {
|
||||
lastReadCount_ = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result;
|
||||
result.reserve(std::min(maxLength, remaining()));
|
||||
size_t bytesRead = 0;
|
||||
char ch = '\0';
|
||||
|
||||
while (bytesRead < maxLength && position_ < size_) {
|
||||
stream_.clear();
|
||||
if (!stream_.get(ch)) {
|
||||
break;
|
||||
}
|
||||
|
||||
++position_;
|
||||
++bytesRead;
|
||||
if (ch == '\0') {
|
||||
lastReadCount_ = bytesRead;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.push_back(ch);
|
||||
}
|
||||
|
||||
lastReadCount_ = bytesRead;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,12 +1,108 @@
|
||||
#include <frostbite2D/resource/npk_archive.h>
|
||||
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
|
||||
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<std::chrono::nanoseconds>(
|
||||
value.time_since_epoch())
|
||||
.count();
|
||||
return static_cast<int64>(duration);
|
||||
}
|
||||
|
||||
bool appendBytes(std::vector<uint8>& buffer, const void* data, size_t size) {
|
||||
if (size == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (buffer.size() > buffer.max_size() - size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t oldSize = buffer.size();
|
||||
buffer.resize(oldSize + size);
|
||||
std::memcpy(buffer.data() + oldSize, data, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool appendPod(std::vector<uint8>& buffer, const T& value) {
|
||||
return appendBytes(buffer, &value, sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPod(std::ifstream& stream, T& value) {
|
||||
stream.read(reinterpret_cast<char*>(&value), sizeof(T));
|
||||
return stream.good();
|
||||
}
|
||||
|
||||
bool appendString(std::vector<uint8>& buffer, const std::string& value) {
|
||||
if (value.size() > kMaxCacheStringLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 length = static_cast<uint32>(value.size());
|
||||
if (!appendPod(buffer, length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return appendBytes(buffer, value.data(), length);
|
||||
}
|
||||
|
||||
bool readString(std::ifstream& stream, std::string& value) {
|
||||
uint32 length = 0;
|
||||
if (!readPod(stream, length)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length > kMaxCacheStringLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value.clear();
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
value.resize(length);
|
||||
stream.read(value.data(), length);
|
||||
return stream.good();
|
||||
}
|
||||
|
||||
const char* getZlibErrorName(int code) {
|
||||
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::SourceFileState> NpkArchive::collectSourceFileStates()
|
||||
const {
|
||||
Asset& asset = Asset::get();
|
||||
std::string npkDir = asset.resolvePath(imagePackDirectory_);
|
||||
std::vector<NpkArchive::SourceFileState> result;
|
||||
|
||||
if (!asset.isDirectory(npkDir)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
|
||||
result.reserve(files.size());
|
||||
|
||||
for (const auto& file : files) {
|
||||
std::error_code error;
|
||||
fs::path path = toFsPath(file);
|
||||
if (!fs::is_regular_file(path, error)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SourceFileState state;
|
||||
state.fileName = asset.getFileName(file);
|
||||
state.size = static_cast<uint64>(fs::file_size(path, error));
|
||||
if (error) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to read file size for %s",
|
||||
file.c_str());
|
||||
state.size = asset.getFileSize(file);
|
||||
}
|
||||
|
||||
error.clear();
|
||||
fs::file_time_type lastWriteTime = fs::last_write_time(path, error);
|
||||
if (error) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to read write time for %s",
|
||||
file.c_str());
|
||||
state.writeTime = 0;
|
||||
} else {
|
||||
state.writeTime = toCacheTimestamp(lastWriteTime);
|
||||
}
|
||||
result.push_back(std::move(state));
|
||||
}
|
||||
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const SourceFileState& left, const SourceFileState& right) {
|
||||
return left.fileName < right.fileName;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool NpkArchive::loadIndexCache(
|
||||
const std::vector<SourceFileState>& sourceFiles) {
|
||||
Asset& asset = Asset::get();
|
||||
std::string cachePath = getCacheFilePath();
|
||||
if (!asset.isRegularFile(cachePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream stream(toFsPath(cachePath), std::ios::binary);
|
||||
if (!stream.is_open()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to open cache file %s",
|
||||
cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
|
||||
uint32 version = 0;
|
||||
uint32 cachedValidationMode = 0;
|
||||
uint32 cachedSourceCount = 0;
|
||||
uint32 cachedImageCount = 0;
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache header");
|
||||
char magic[sizeof(kIndexCacheMagic)] = {};
|
||||
stream.read(magic, sizeof(magic));
|
||||
if (!stream.good() ||
|
||||
std::memcmp(magic, kIndexCacheMagic, sizeof(kIndexCacheMagic)) != 0) {
|
||||
SDL_Log("NpkArchive: cache magic mismatch, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readPod(stream, version) || !readPod(stream, cachedValidationMode) ||
|
||||
!readPod(stream, cachedSourceCount) || !readPod(stream, cachedImageCount)) {
|
||||
SDL_Log("NpkArchive: cache header incomplete, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version != kIndexCacheVersion) {
|
||||
SDL_Log("NpkArchive: cache version mismatch (%u != %u), will rescan",
|
||||
version, kIndexCacheVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cachedSourceCount > kMaxCacheSourceFileCount ||
|
||||
cachedImageCount > kMaxCacheImageCount) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: cache counts look invalid (sources=%u, images=%u)",
|
||||
cachedSourceCount, cachedImageCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cachedValidationMode != static_cast<uint32>(validationMode)) {
|
||||
SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan",
|
||||
cachedValidationMode, static_cast<uint32>(validationMode));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||
if (sourceFiles.empty()) {
|
||||
SDL_Log("NpkArchive: strict cache validation requires source metadata, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<SourceFileState> cachedSourceFiles;
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache sources");
|
||||
cachedSourceFiles.reserve(cachedSourceCount);
|
||||
for (uint32 i = 0; i < cachedSourceCount; ++i) {
|
||||
SourceFileState state;
|
||||
if (!readString(stream, state.fileName) || !readPod(stream, state.size) ||
|
||||
!readPod(stream, state.writeTime)) {
|
||||
SDL_Log("NpkArchive: cache source file section incomplete, will rescan");
|
||||
return false;
|
||||
}
|
||||
cachedSourceFiles.push_back(std::move(state));
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedSourceFiles != sourceFiles) {
|
||||
SDL_Log("NpkArchive: cache invalidated by directory changes, will rescan");
|
||||
return false;
|
||||
}
|
||||
} else if (cachedSourceCount != 0) {
|
||||
SDL_Log("NpkArchive: trusted cache should not contain source metadata, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, ImgRef> cachedIndex;
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache images");
|
||||
cachedIndex.reserve(cachedImageCount);
|
||||
for (uint32 i = 0; i < cachedImageCount; ++i) {
|
||||
std::string path;
|
||||
std::string npkFile;
|
||||
ImgRef img;
|
||||
if (!readString(stream, path) || !readString(stream, npkFile) ||
|
||||
!readPod(stream, img.frameCount) || !readPod(stream, img.offset) ||
|
||||
!readPod(stream, img.size)) {
|
||||
SDL_Log("NpkArchive: cache image section incomplete, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
img.path = path;
|
||||
img.npkFile = std::move(npkFile);
|
||||
img.loaded = false;
|
||||
cachedIndex.emplace(std::move(path), std::move(img));
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream.good() && !stream.eof()) {
|
||||
SDL_Log("NpkArchive: cache read failed, will rescan");
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache commit");
|
||||
imgIndex_.swap(cachedIndex);
|
||||
}
|
||||
SDL_Log("NpkArchive: loaded %zu image refs from cache %s", imgIndex_.size(),
|
||||
cachePath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NpkArchive::saveIndexCache(
|
||||
const std::vector<SourceFileState>& sourceFiles) const {
|
||||
std::string cachePath = getCacheFilePath();
|
||||
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
|
||||
uint32 sourceCount =
|
||||
validationMode == IndexCacheValidationMode::StrictSourceState
|
||||
? static_cast<uint32>(sourceFiles.size())
|
||||
: 0;
|
||||
uint32 imageCount = static_cast<uint32>(imgIndex_.size());
|
||||
std::vector<uint8> buffer;
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache serialize");
|
||||
size_t estimatedSize = sizeof(kIndexCacheMagic) + sizeof(uint32) * 4;
|
||||
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||
for (const auto& sourceFile : sourceFiles) {
|
||||
estimatedSize += sizeof(uint32) + sourceFile.fileName.size();
|
||||
estimatedSize += sizeof(sourceFile.size);
|
||||
estimatedSize += sizeof(sourceFile.writeTime);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [path, img] : imgIndex_) {
|
||||
estimatedSize += sizeof(uint32) + path.size();
|
||||
estimatedSize += sizeof(uint32) + img.npkFile.size();
|
||||
estimatedSize += sizeof(img.frameCount);
|
||||
estimatedSize += sizeof(img.offset);
|
||||
estimatedSize += sizeof(img.size);
|
||||
}
|
||||
|
||||
buffer.reserve(estimatedSize);
|
||||
|
||||
if (!appendBytes(buffer, kIndexCacheMagic, sizeof(kIndexCacheMagic)) ||
|
||||
!appendPod(buffer, kIndexCacheVersion) ||
|
||||
!appendPod(buffer, static_cast<uint32>(validationMode)) ||
|
||||
!appendPod(buffer, sourceCount) ||
|
||||
!appendPod(buffer, imageCount)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to serialize cache header %s",
|
||||
cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||
for (const auto& sourceFile : sourceFiles) {
|
||||
if (!appendString(buffer, sourceFile.fileName) ||
|
||||
!appendPod(buffer, sourceFile.size) ||
|
||||
!appendPod(buffer, sourceFile.writeTime)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to serialize cache source data %s",
|
||||
cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [path, img] : imgIndex_) {
|
||||
if (!appendString(buffer, path) || !appendString(buffer, img.npkFile) ||
|
||||
!appendPod(buffer, img.frameCount) || !appendPod(buffer, img.offset) ||
|
||||
!appendPod(buffer, img.size)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to serialize cache image data %s",
|
||||
cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache flush");
|
||||
std::ofstream stream(toFsPath(cachePath),
|
||||
std::ios::binary | std::ios::trunc);
|
||||
if (!stream.is_open()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: failed to create cache file %s",
|
||||
cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!buffer.empty()) {
|
||||
stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||
}
|
||||
|
||||
if (!stream.good()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NpkArchive: cache flush failed %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Log("NpkArchive: wrote index cache %s (mode=%u, %u source files, %u images)",
|
||||
cachePath.c_str(), static_cast<uint32>(validationMode), sourceCount,
|
||||
imageCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
void NpkArchive::scanNpkFiles() {
|
||||
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<SourceFileState> sourceFiles;
|
||||
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive collect source states");
|
||||
sourceFiles = collectSourceFileStates();
|
||||
}
|
||||
if (sourceFiles.empty()) {
|
||||
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache strict-load");
|
||||
if (loadIndexCache(sourceFiles)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{
|
||||
ScopedStartupTrace stageTrace("NpkArchive cache fast-load");
|
||||
if (loadIndexCache(sourceFiles)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
|
||||
std::sort(files.begin(), files.end());
|
||||
if (files.empty()) {
|
||||
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
|
||||
return;
|
||||
}
|
||||
SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
|
||||
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<std::string> 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<uint32>(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 "";
|
||||
}
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
bool canReadSpan(const std::vector<uint8>& bytes, size_t offset, size_t length) {
|
||||
return offset <= bytes.size() && length <= bytes.size() - offset;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool readPodAt(const std::vector<uint8>& bytes, size_t offset, T& value) {
|
||||
if (!canReadSpan(bytes, offset, sizeof(T))) {
|
||||
value = T{};
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(&value, bytes.data() + offset, sizeof(T));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string readStringAt(const std::vector<uint8>& bytes, size_t offset,
|
||||
size_t length) {
|
||||
if (!canReadSpan(bytes, offset, length)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string(reinterpret_cast<const char*>(bytes.data() + offset),
|
||||
length);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PvfArchive& PvfArchive::get() {
|
||||
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<size_t>(uuidLength) > reader_.remaining()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"PvfArchive: invalid uuid length in archive header");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string uuid = reader_.readString(static_cast<size_t>(uuidLength));
|
||||
(void)uuid;
|
||||
|
||||
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<size_t>(alignedIndexHeaderSize));
|
||||
if (!headerBytesOpt.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8> headerBytes = std::move(headerBytesOpt.value());
|
||||
crcDecodeBuffer(headerBytes, static_cast<uint32>(indexHeaderCrc));
|
||||
dataStartPos_ = indexHeaderPos + static_cast<size_t>(alignedIndexHeaderSize);
|
||||
|
||||
size_t cursor = 0;
|
||||
for (int32 i = 0; i < indexSize; ++i) {
|
||||
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<size_t>(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<uint32>(fileLength) + 3u) & 0xFFFFFFFCu;
|
||||
PvfFileInfo info;
|
||||
info.offset = relativeOffset;
|
||||
info.offset = static_cast<size_t>(relativeOffset);
|
||||
info.crc32 = static_cast<uint32>(crc32);
|
||||
info.length = realFileLength;
|
||||
info.length = static_cast<size_t>(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<uint8>& 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<size_t>(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<size_t>(startPos) + 4;
|
||||
size_t length = static_cast<size_t>(endPos - startPos);
|
||||
if (!canReadSpan(bytes, stringPos, length)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"PvfArchive: invalid string span at entry %d", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
binStringTable_[i] = readStringAt(bytes, stringPos, length);
|
||||
}
|
||||
}
|
||||
|
||||
void PvfArchive::initLoadStrings() {
|
||||
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<uint8>& 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<std::string> 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<std::string, std::string> 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<uint8>& contentBytes = contentBytesOpt.value();
|
||||
content.assign(reinterpret_cast<const char*>(contentBytes.data()),
|
||||
contentBytes.size());
|
||||
stringFileCache[key] = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!content.empty()) {
|
||||
auto& table = loadStrings_[type];
|
||||
std::vector<std::string> lines = splitString(content, "\n");
|
||||
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<std::string> 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<uint8> bytes = reader_.readBytes(info.length);
|
||||
auto cacheIt = decodedFileCache_.find(normalizedPath);
|
||||
if (cacheIt == decodedFileCache_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::vector<uint8>& bytes = cacheIt->second;
|
||||
if (bytes.empty()) {
|
||||
return std::string();
|
||||
}
|
||||
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
}
|
||||
|
||||
@@ -233,12 +379,16 @@ std::optional<std::vector<uint8>> 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<RawData> PvfArchive::getFileRawData(const std::string& path) {
|
||||
@@ -249,17 +399,22 @@ std::optional<RawData> 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<uint8>& bytes = cacheIt->second;
|
||||
RawData result;
|
||||
result.size = info.length;
|
||||
result.data = std::make_unique<char[]>(info.length);
|
||||
reader_.read(result.data.get(), info.length);
|
||||
|
||||
result.size = bytes.size();
|
||||
result.data = std::make_unique<char[]>(bytes.size());
|
||||
if (!bytes.empty()) {
|
||||
std::memcpy(result.data.get(), bytes.data(), bytes.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -271,7 +426,8 @@ std::optional<std::string> PvfArchive::getBinString(int key) const {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> PvfArchive::getLoadString(const std::string& type, const std::string& key) const {
|
||||
std::optional<std::string> PvfArchive::getLoadString(const std::string& type,
|
||||
const std::string& key) const {
|
||||
std::string normalizedType = normalizePath(type);
|
||||
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<char>(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<std::string> PvfArchive::splitString(const std::string& str, const std::string& delimiter) const {
|
||||
std::vector<std::string> PvfArchive::splitString(
|
||||
const std::string& str, const std::string& delimiter) const {
|
||||
std::vector<std::string> tokens;
|
||||
size_t pos = 0;
|
||||
size_t found;
|
||||
@@ -378,16 +539,94 @@ std::vector<std::string> 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<std::vector<uint8>> PvfArchive::readArchiveBytes(
|
||||
size_t absoluteOffset, size_t length) {
|
||||
if (!isOpen()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
return std::vector<uint8>();
|
||||
}
|
||||
|
||||
if (absoluteOffset > reader_.size() ||
|
||||
length > reader_.size() - absoluteOffset) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"PvfArchive: attempted to read beyond archive bounds (offset=%zu, size=%zu, archive=%zu)",
|
||||
absoluteOffset, length, reader_.size());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
reader_.seek(absoluteOffset);
|
||||
std::vector<uint8> bytes = reader_.readBytes(length);
|
||||
if (bytes.size() != length) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"PvfArchive: short read at offset %zu (expected %zu bytes, got %zu)",
|
||||
absoluteOffset, length, bytes.size());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint8>> PvfArchive::readDecodedFileBytes(
|
||||
const PvfFileInfo& info) {
|
||||
auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length);
|
||||
if (!bytesOpt.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<uint8> bytes = std::move(bytesOpt.value());
|
||||
crcDecodeBuffer(bytes, info.crc32);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void PvfArchive::crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32) {
|
||||
if (data.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32 key = 0x81A79011u;
|
||||
for (size_t i = 0; i + 3 < data.size(); i += 4) {
|
||||
uint32 value = static_cast<uint32>(data[i]) |
|
||||
(static_cast<uint32>(data[i + 1]) << 8) |
|
||||
(static_cast<uint32>(data[i + 2]) << 16) |
|
||||
(static_cast<uint32>(data[i + 3]) << 24);
|
||||
|
||||
uint32 decoded = (value ^ key ^ crc32);
|
||||
decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu);
|
||||
|
||||
data[i] = static_cast<uint8>((decoded >> 0) & 0xFFu);
|
||||
data[i + 1] = static_cast<uint8>((decoded >> 8) & 0xFFu);
|
||||
data[i + 2] = static_cast<uint8>((decoded >> 16) & 0xFFu);
|
||||
data[i + 3] = static_cast<uint8>((decoded >> 24) & 0xFFu);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <SDL.h>
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ namespace frostbite2D {
|
||||
|
||||
Scene* Scene::current_ = nullptr;
|
||||
|
||||
Scene::Scene() {
|
||||
current_ = this;
|
||||
}
|
||||
Scene::Scene() = default;
|
||||
|
||||
Scene::~Scene() {
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/scene/ui_scene.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
@@ -9,8 +10,6 @@ SceneManager& SceneManager::get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用
|
||||
|
||||
void SceneManager::PushScene(Ptr<Scene> scene) {
|
||||
if (!scene) {
|
||||
return;
|
||||
@@ -21,6 +20,7 @@ void SceneManager::PushScene(Ptr<Scene> 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> 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<UIScene> scene) {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiSceneStack_.push_back(scene);
|
||||
scene->onEnter();
|
||||
}
|
||||
|
||||
void SceneManager::PopUIScene() {
|
||||
if (uiSceneStack_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiSceneStack_.back()->onExit();
|
||||
uiSceneStack_.pop_back();
|
||||
}
|
||||
|
||||
void SceneManager::ReplaceUIScene(Ptr<UIScene> scene) {
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uiSceneStack_.empty()) {
|
||||
uiSceneStack_.back()->onExit();
|
||||
uiSceneStack_.pop_back();
|
||||
}
|
||||
|
||||
uiSceneStack_.push_back(scene);
|
||||
scene->onEnter();
|
||||
}
|
||||
|
||||
bool SceneManager::RemoveUIScene(UIScene* scene) {
|
||||
if (!scene) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto it = uiSceneStack_.begin(); it != uiSceneStack_.end(); ++it) {
|
||||
if (it->Get() != scene) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(*it)->onExit();
|
||||
uiSceneStack_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SceneManager::ClearUIScenes() {
|
||||
while (!uiSceneStack_.empty()) {
|
||||
uiSceneStack_.back()->onExit();
|
||||
uiSceneStack_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void SceneManager::ClearAll() {
|
||||
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
|
||||
|
||||
11
Frostbite2D/src/frostbite2D/scene/ui_scene.cpp
Normal file
11
Frostbite2D/src/frostbite2D/scene/ui_scene.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include <frostbite2D/scene/ui_scene.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
UIScene::UIScene() {
|
||||
camera_.setFlipY(true);
|
||||
camera_.setZoom(1.0f);
|
||||
camera_.setPosition(Vec2::Zero());
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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<std::string> missingAnimationPaths_;
|
||||
size_t skippedMissingAnimationCount_ = 0;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
47
Game/include/common/GameDebugActor.h
Normal file
47
Game/include/common/GameDebugActor.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class CharacterObject;
|
||||
class GameMap;
|
||||
class NineSliceActor;
|
||||
class TextSprite;
|
||||
|
||||
/**
|
||||
* @brief Reusable singleton debug actor for UI scenes.
|
||||
*/
|
||||
class GameDebugActor : public Actor {
|
||||
public:
|
||||
static GameDebugActor& get();
|
||||
static Ptr<GameDebugActor> getPtr();
|
||||
|
||||
GameDebugActor(const GameDebugActor&) = delete;
|
||||
GameDebugActor& operator=(const GameDebugActor&) = delete;
|
||||
|
||||
bool AttachToScene(Scene* scene);
|
||||
bool AttachToParent(Actor* parent);
|
||||
void DetachFromParent();
|
||||
|
||||
void SetDebugMap(GameMap* map);
|
||||
void SetTrackedCharacter(CharacterObject* character);
|
||||
void ClearDebugContext();
|
||||
|
||||
void OnUpdate(float deltaTime) override;
|
||||
|
||||
private:
|
||||
void initOverlay();
|
||||
void updateOverlay();
|
||||
void setOverlayVisible(bool visible);
|
||||
|
||||
GameDebugActor();
|
||||
~GameDebugActor() override = default;
|
||||
|
||||
GameMap* debugMap_ = nullptr;
|
||||
CharacterObject* trackedCharacter_ = nullptr;
|
||||
RefPtr<NineSliceActor> background_;
|
||||
RefPtr<TextSprite> coordText_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -63,7 +63,7 @@ struct MapConfig {
|
||||
std::vector<BackgroundAnimationConfig> backgroundAnimations;
|
||||
std::vector<MapAnimationConfig> mapAnimations;
|
||||
std::vector<std::string> soundIds;
|
||||
std::vector<Rect> virtualMovableAreas;
|
||||
std::vector<Vec2> virtualMovablePolygon;
|
||||
std::vector<Rect> townMovableAreas;
|
||||
std::vector<MoveAreaTarget> townMovableAreaTargets;
|
||||
};
|
||||
|
||||
@@ -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<Actor> tileRoot_;
|
||||
/// 角色可行走矩形区域。
|
||||
std::vector<Rect> movableArea_;
|
||||
std::vector<Vec2> movablePolygon_;
|
||||
/// 进入后会触发切图/传送的矩形区域。
|
||||
std::vector<Rect> moveArea_;
|
||||
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
|
||||
int backgroundRepeatWidth_ = 0;
|
||||
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
|
||||
int mapOffsetY_ = 0;
|
||||
bool debugMode_ = false;
|
||||
bool debugMode_ = true;
|
||||
/// 当前地图正在播放的背景音乐。
|
||||
Ptr<Music> currentMusic_;
|
||||
};
|
||||
|
||||
@@ -9,11 +9,14 @@ class GameMapLayer : public Actor {
|
||||
public:
|
||||
void Render() override;
|
||||
|
||||
void AddDebugFeasibleAreaInfo(const Rect& rect, int type);
|
||||
void SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon);
|
||||
void AddDebugMoveAreaInfo(const Rect& rect);
|
||||
void ClearDebugAreaInfo();
|
||||
void AddObject(RefPtr<Actor> obj);
|
||||
|
||||
private:
|
||||
std::vector<Rect> feasibleAreaInfoList_;
|
||||
std::vector<Vec2> feasibleAreaPolygon_;
|
||||
std::vector<Rect> feasibleAreaFillRects_;
|
||||
std::vector<Rect> moveAreaInfoList_;
|
||||
};
|
||||
|
||||
|
||||
22
Game/include/scene/GameDebugUIScene.h
Normal file
22
Game/include/scene/GameDebugUIScene.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/scene/ui_scene.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class CharacterObject;
|
||||
class GameMap;
|
||||
|
||||
class GameDebugUIScene : public UIScene {
|
||||
public:
|
||||
GameDebugUIScene() = default;
|
||||
~GameDebugUIScene() override = default;
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
void SetDebugContext(GameMap* map, CharacterObject* character);
|
||||
void ClearDebugContext();
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -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 <frostbite2D/scene/scene.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
@@ -19,6 +20,7 @@ public:
|
||||
private:
|
||||
GameCameraController cameraController_;
|
||||
RefPtr<CharacterObject> character_;
|
||||
RefPtr<GameDebugUIScene> debugScene_;
|
||||
bool initialized_ = false;
|
||||
RefPtr<GameMap> map_;
|
||||
};
|
||||
|
||||
54
Game/include/ui/NineSliceActor.h
Normal file
54
Game/include/ui/NineSliceActor.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/**
|
||||
* @brief Composes 9 consecutive NPK frames into a resizable nine-slice panel.
|
||||
*/
|
||||
class NineSliceActor : public Actor {
|
||||
public:
|
||||
NineSliceActor();
|
||||
~NineSliceActor() override = default;
|
||||
|
||||
static Ptr<NineSliceActor> createFromNpk(const std::string& imgPath,
|
||||
size_t startIndex = 0);
|
||||
|
||||
bool SetSlices(const std::string& imgPath, size_t startIndex);
|
||||
void SetSize(const Vec2& size);
|
||||
void SetSize(float width, float height);
|
||||
|
||||
const std::string& GetImgPath() const { return imgPath_; }
|
||||
size_t GetStartIndex() const { return startIndex_; }
|
||||
|
||||
private:
|
||||
enum SliceIndex : size_t {
|
||||
TopLeft = 0,
|
||||
Top = 1,
|
||||
TopRight = 2,
|
||||
Left = 3,
|
||||
Center = 4,
|
||||
Right = 5,
|
||||
BottomLeft = 6,
|
||||
Bottom = 7,
|
||||
BottomRight = 8,
|
||||
SliceCount = 9
|
||||
};
|
||||
|
||||
void updateLayout();
|
||||
void applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
|
||||
const Vec2& size);
|
||||
Vec2 getNaturalSize() const;
|
||||
|
||||
std::string imgPath_;
|
||||
size_t startIndex_ = 0;
|
||||
std::array<Ptr<Sprite>, SliceCount> sliceSprites_;
|
||||
std::array<Vec2, SliceCount> sliceNaturalSizes_ = {};
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
#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<Scene>();
|
||||
SceneManager::get().PushScene(LoadingScene);
|
||||
{
|
||||
ScopedStartupTrace stageTrace("Loading scene bootstrap");
|
||||
auto LoadingScene = MakePtr<Scene>();
|
||||
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<GameMapTestScene>();
|
||||
SceneManager::get().ReplaceScene(testMapScene);
|
||||
{
|
||||
ScopedStartupTrace stageTrace(
|
||||
"SceneManager::ReplaceScene(GameMapTestScene)");
|
||||
auto testMapScene = MakePtr<GameMapTestScene>();
|
||||
SceneManager::get().ReplaceScene(testMapScene);
|
||||
}
|
||||
|
||||
StartupTrace::mark("after SceneManager::ReplaceScene");
|
||||
},
|
||||
[](std::exception_ptr error) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "character/CharacterAnimation.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -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<Animation>(
|
||||
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 "<none>";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <SDL2/SDL.h>
|
||||
#include <cmath>
|
||||
#include <frostbite2D/core/application.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
@@ -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<CharacterAnimation>();
|
||||
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({});
|
||||
|
||||
147
Game/src/common/GameDebugActor.cpp
Normal file
147
Game/src/common/GameDebugActor.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "common/GameDebugActor.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include "map/GameMap.h"
|
||||
#include "ui/NineSliceActor.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/2d/text_sprite.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <limits>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kDebugHudMarginX = 12.0f;
|
||||
constexpr float kDebugHudMarginY = 12.0f;
|
||||
constexpr float kDebugHudPaddingX = 8.0f;
|
||||
constexpr float kDebugHudPaddingY = 6.0f;
|
||||
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
|
||||
|
||||
} // namespace
|
||||
|
||||
GameDebugActor::GameDebugActor() {
|
||||
SetName("GameDebugActor");
|
||||
SetZOrder(std::numeric_limits<int>::max());
|
||||
initOverlay();
|
||||
}
|
||||
|
||||
GameDebugActor& GameDebugActor::get() {
|
||||
return *getPtr();
|
||||
}
|
||||
|
||||
Ptr<GameDebugActor> GameDebugActor::getPtr() {
|
||||
static Ptr<GameDebugActor> instance(new GameDebugActor());
|
||||
return instance;
|
||||
}
|
||||
|
||||
bool GameDebugActor::AttachToScene(Scene* scene) {
|
||||
return AttachToParent(scene);
|
||||
}
|
||||
|
||||
bool GameDebugActor::AttachToParent(Actor* parent) {
|
||||
if (!parent || parent == this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Ptr<GameDebugActor> self = getPtr();
|
||||
if (GetParent() == parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GetParent()) {
|
||||
GetParent()->RemoveChild(self);
|
||||
}
|
||||
|
||||
parent->AddChild(self);
|
||||
return GetParent() == parent;
|
||||
}
|
||||
|
||||
void GameDebugActor::DetachFromParent() {
|
||||
Ptr<GameDebugActor> self = getPtr();
|
||||
if (GetParent()) {
|
||||
GetParent()->RemoveChild(self);
|
||||
}
|
||||
}
|
||||
|
||||
void GameDebugActor::SetDebugMap(GameMap* map) {
|
||||
debugMap_ = map;
|
||||
updateOverlay();
|
||||
}
|
||||
|
||||
void GameDebugActor::SetTrackedCharacter(CharacterObject* character) {
|
||||
trackedCharacter_ = character;
|
||||
updateOverlay();
|
||||
}
|
||||
|
||||
void GameDebugActor::ClearDebugContext() {
|
||||
debugMap_ = nullptr;
|
||||
trackedCharacter_ = nullptr;
|
||||
setOverlayVisible(false);
|
||||
}
|
||||
|
||||
void GameDebugActor::OnUpdate(float deltaTime) {
|
||||
(void)deltaTime;
|
||||
updateOverlay();
|
||||
}
|
||||
|
||||
void GameDebugActor::initOverlay() {
|
||||
if (background_ || coordText_) {
|
||||
return;
|
||||
}
|
||||
|
||||
background_ = NineSliceActor::createFromNpk(kDebugHudPopupImg, 0);
|
||||
if (background_) {
|
||||
background_->SetName("debugHudBackground");
|
||||
AddChild(background_);
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GameDebugActor: failed to load HUD background from %s",
|
||||
kDebugHudPopupImg);
|
||||
}
|
||||
|
||||
coordText_ = TextSprite::create();
|
||||
coordText_->SetName("debugCoordText");
|
||||
coordText_->SetFont("default");
|
||||
coordText_->SetTextColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
coordText_->SetZOrder(1);
|
||||
AddChild(coordText_);
|
||||
|
||||
setOverlayVisible(false);
|
||||
}
|
||||
|
||||
void GameDebugActor::updateOverlay() {
|
||||
if (!coordText_ || !debugMap_ || !trackedCharacter_ ||
|
||||
!debugMap_->IsDebugModeEnabled()) {
|
||||
setOverlayVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
|
||||
char textBuffer[64];
|
||||
SDL_snprintf(textBuffer, sizeof(textBuffer), "角色坐标: (%d, %d, %d)",
|
||||
worldPosition.x, worldPosition.y, worldPosition.z);
|
||||
coordText_->SetText(textBuffer);
|
||||
|
||||
Vec2 textSize = coordText_->GetTextSize();
|
||||
Vec2 panelSize(textSize.x + kDebugHudPaddingX * 2.0f,
|
||||
textSize.y + kDebugHudPaddingY * 2.0f);
|
||||
if (background_) {
|
||||
background_->SetSize(panelSize);
|
||||
}
|
||||
|
||||
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
|
||||
SetScale(1.0f);
|
||||
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
|
||||
setOverlayVisible(true);
|
||||
}
|
||||
|
||||
void GameDebugActor::setOverlayVisible(bool visible) {
|
||||
if (background_) {
|
||||
background_->SetVisible(visible);
|
||||
}
|
||||
if (coordText_) {
|
||||
coordText_->SetVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -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<int> polygonCoords;
|
||||
while (!stream.isEnd()) {
|
||||
std::string token = stream.get();
|
||||
if (token == "[/virtual movable area]") {
|
||||
break;
|
||||
}
|
||||
Rect rect(static_cast<float>(toInt(token)),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(toInt(stream.get())),
|
||||
static_cast<float>(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<float>(polygonCoords[i]),
|
||||
static_cast<float>(polygonCoords[i + 1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <frostbite2D/core/application.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/resource/audio_database.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
@@ -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<int>(std::lround(value));
|
||||
@@ -37,6 +39,57 @@ Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
|
||||
static_cast<float>(z));
|
||||
}
|
||||
|
||||
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
|
||||
Vec2 segment = end - start;
|
||||
Vec2 toPoint = point - start;
|
||||
double cross = static_cast<double>(segment.cross(toPoint));
|
||||
if (std::abs(cross) > kPolygonEpsilon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
|
||||
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
|
||||
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
|
||||
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
|
||||
return point.x >= minX && point.x <= maxX && point.y >= minY &&
|
||||
point.y <= maxY;
|
||||
}
|
||||
|
||||
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
|
||||
if (polygon.size() < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool inside = false;
|
||||
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
|
||||
const Vec2& start = polygon[j];
|
||||
const Vec2& end = polygon[i];
|
||||
if (IsPointOnSegment(point, start, end)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
|
||||
if (!crossesScanline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double intersectX =
|
||||
static_cast<double>(start.x) +
|
||||
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
|
||||
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
|
||||
(static_cast<double>(end.y) - static_cast<double>(start.y));
|
||||
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
|
||||
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
|
||||
}
|
||||
|
||||
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
|
||||
// 占位,避免整张地图中断。
|
||||
Ptr<Sprite> 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<Actor> object) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。
|
||||
// Keep dynamic objects on the normal layer and sort them by y.
|
||||
object->SetZOrder(static_cast<int>(object->GetPosition().y));
|
||||
layerMap_["normal"]->AddObject(object);
|
||||
}
|
||||
@@ -365,42 +456,41 @@ void GameMap::AddObject(RefPtr<Actor> 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<float>(targetX);
|
||||
result.y = static_cast<float>(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<float>(targetX);
|
||||
} else {
|
||||
result.y = static_cast<float>(targetY);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isXValid) {
|
||||
result.x = static_cast<float>(targetX);
|
||||
}
|
||||
if (isYValid) {
|
||||
} else if (isYValid) {
|
||||
result.y = static_cast<float>(targetY);
|
||||
}
|
||||
result.z = static_cast<float>(RoundWorldCoordinate(curPos.z + posOffset.z));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,155 @@
|
||||
#include "map/GameMapLayer.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kDebugAreaAlpha = 0.45f;
|
||||
constexpr float kDebugOutlineAlpha = 0.85f;
|
||||
constexpr float kDebugEdgePointSize = 5.0f;
|
||||
constexpr float kDebugVertexSize = 9.0f;
|
||||
constexpr float kDebugEdgeStep = 4.0f;
|
||||
|
||||
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) {
|
||||
std::vector<Rect> fillRects;
|
||||
if (polygon.size() < 3) {
|
||||
return fillRects;
|
||||
}
|
||||
|
||||
float minY = polygon.front().y;
|
||||
float maxY = polygon.front().y;
|
||||
for (const auto& point : polygon) {
|
||||
minY = std::min(minY, point.y);
|
||||
maxY = std::max(maxY, point.y);
|
||||
}
|
||||
|
||||
int scanlineMinY = static_cast<int>(std::floor(minY));
|
||||
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
|
||||
if (scanlineMaxY < scanlineMinY) {
|
||||
return fillRects;
|
||||
}
|
||||
|
||||
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
|
||||
float scanY = static_cast<float>(y) + 0.5f;
|
||||
std::vector<float> intersections;
|
||||
intersections.reserve(polygon.size());
|
||||
|
||||
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
|
||||
const Vec2& start = polygon[j];
|
||||
const Vec2& end = polygon[i];
|
||||
if (start.y == end.y) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float edgeMinY = std::min(start.y, end.y);
|
||||
float edgeMaxY = std::max(start.y, end.y);
|
||||
if (scanY < edgeMinY || scanY >= edgeMaxY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float t = (scanY - start.y) / (end.y - start.y);
|
||||
intersections.push_back(start.x + (end.x - start.x) * t);
|
||||
}
|
||||
|
||||
if (intersections.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::sort(intersections.begin(), intersections.end());
|
||||
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
|
||||
float left = std::floor(intersections[i]);
|
||||
float right = std::ceil(intersections[i + 1]);
|
||||
float width = right - left;
|
||||
if (width > 0.0f) {
|
||||
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fillRects;
|
||||
}
|
||||
|
||||
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
|
||||
const Color& color) {
|
||||
if (polygon.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& renderer = Renderer::get();
|
||||
for (size_t i = 0; i < polygon.size(); ++i) {
|
||||
Vec2 start = worldOrigin + polygon[i];
|
||||
Vec2 end = worldOrigin + polygon[(i + 1) % polygon.size()];
|
||||
Vec2 delta = end - start;
|
||||
float length = delta.length();
|
||||
int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep)));
|
||||
|
||||
for (int step = 0; step <= steps; ++step) {
|
||||
float t = static_cast<float>(step) / static_cast<float>(steps);
|
||||
Vec2 point = start + delta * t;
|
||||
renderer.drawQuad(
|
||||
Rect(point.x - kDebugEdgePointSize * 0.5f,
|
||||
point.y - kDebugEdgePointSize * 0.5f, kDebugEdgePointSize,
|
||||
kDebugEdgePointSize),
|
||||
color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPolygonVertices(const std::vector<Vec2>& polygon,
|
||||
const Vec2& worldOrigin, const Color& color) {
|
||||
auto& renderer = Renderer::get();
|
||||
for (const auto& vertex : polygon) {
|
||||
Vec2 point = worldOrigin + vertex;
|
||||
renderer.drawQuad(
|
||||
Rect(point.x - kDebugVertexSize * 0.5f,
|
||||
point.y - kDebugVertexSize * 0.5f, kDebugVertexSize,
|
||||
kDebugVertexSize),
|
||||
color);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GameMapLayer::Render() {
|
||||
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<Vec2>& 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<Actor> obj) {
|
||||
|
||||
28
Game/src/scene/GameDebugUIScene.cpp
Normal file
28
Game/src/scene/GameDebugUIScene.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "scene/GameDebugUIScene.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include "common/GameDebugActor.h"
|
||||
#include "map/GameMap.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
void GameDebugUIScene::onEnter() {
|
||||
UIScene::onEnter();
|
||||
GameDebugActor::get().AttachToScene(this);
|
||||
}
|
||||
|
||||
void GameDebugUIScene::onExit() {
|
||||
GameDebugActor::get().ClearDebugContext();
|
||||
GameDebugActor::get().DetachFromParent();
|
||||
UIScene::onExit();
|
||||
}
|
||||
|
||||
void GameDebugUIScene::SetDebugContext(GameMap* map, CharacterObject* character) {
|
||||
GameDebugActor::get().SetDebugMap(map);
|
||||
GameDebugActor::get().SetTrackedCharacter(character);
|
||||
}
|
||||
|
||||
void GameDebugUIScene::ClearDebugContext() {
|
||||
GameDebugActor::get().ClearDebugContext();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "scene/GameMapTestScene.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <frostbite2D/scene/scene_manager.h>
|
||||
#include <frostbite2D/utils/startup_trace.h>
|
||||
|
||||
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<GameDebugUIScene>();
|
||||
}
|
||||
SceneManager::get().RemoveUIScene(debugScene_.Get());
|
||||
SceneManager::get().PushUIScene(debugScene_);
|
||||
|
||||
if (initialized_) {
|
||||
debugScene_->SetDebugContext(map_.Get(), character_.Get());
|
||||
return;
|
||||
}
|
||||
|
||||
map_ = MakePtr<GameMap>();
|
||||
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<GameMap>();
|
||||
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<CharacterObject>();
|
||||
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<CharacterObject>();
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
203
Game/src/ui/NineSliceActor.cpp
Normal file
203
Game/src/ui/NineSliceActor.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
#include "ui/NineSliceActor.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include <frostbite2D/resource/npk_archive.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<const char*, 9> kSliceNames = {
|
||||
"topLeft", "top", "topRight", "left", "center",
|
||||
"right", "bottomLeft", "bottom", "bottomRight"};
|
||||
|
||||
struct AxisLayout {
|
||||
float start = 0.0f;
|
||||
float center = 0.0f;
|
||||
float end = 0.0f;
|
||||
};
|
||||
|
||||
AxisLayout resolveAxis(float totalSize, float startSize, float endSize) {
|
||||
totalSize = std::max(totalSize, 0.0f);
|
||||
startSize = std::max(startSize, 0.0f);
|
||||
endSize = std::max(endSize, 0.0f);
|
||||
|
||||
AxisLayout layout;
|
||||
float fixedSize = startSize + endSize;
|
||||
if (totalSize >= fixedSize) {
|
||||
layout.start = startSize;
|
||||
layout.center = totalSize - fixedSize;
|
||||
layout.end = endSize;
|
||||
return layout;
|
||||
}
|
||||
|
||||
if (fixedSize <= 0.0f) {
|
||||
layout.center = totalSize;
|
||||
return layout;
|
||||
}
|
||||
|
||||
float scale = totalSize / fixedSize;
|
||||
layout.start = startSize * scale;
|
||||
layout.end = endSize * scale;
|
||||
return layout;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NineSliceActor::NineSliceActor() {
|
||||
SetName("NineSliceActor");
|
||||
}
|
||||
|
||||
Ptr<NineSliceActor> NineSliceActor::createFromNpk(const std::string& imgPath,
|
||||
size_t startIndex) {
|
||||
auto actor = MakePtr<NineSliceActor>();
|
||||
if (!actor->SetSlices(imgPath, startIndex)) {
|
||||
return nullptr;
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
|
||||
bool NineSliceActor::SetSlices(const std::string& imgPath, size_t startIndex) {
|
||||
NpkArchive& npk = NpkArchive::get();
|
||||
auto imgOpt = npk.getImg(imgPath);
|
||||
if (!imgOpt) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NineSliceActor: img not found: %s", imgPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<Ptr<Sprite>, SliceCount> newSlices;
|
||||
std::array<Vec2, SliceCount> newNaturalSizes = {};
|
||||
|
||||
for (size_t i = 0; i < SliceCount; ++i) {
|
||||
size_t frameIndex = startIndex + i;
|
||||
auto frameOpt = npk.getImageFrame(*imgOpt, frameIndex);
|
||||
if (!frameOpt || frameOpt->data.empty()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NineSliceActor: frame %u missing for img %s",
|
||||
static_cast<unsigned>(frameIndex), imgPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto sprite = Sprite::createFromNpk(imgPath, frameIndex);
|
||||
if (!sprite || !sprite->GetTexture()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NineSliceActor: failed to create sprite for frame %u of %s",
|
||||
static_cast<unsigned>(frameIndex), imgPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
sprite->SetName(kSliceNames[i]);
|
||||
sprite->SetOffset(0.0f, 0.0f);
|
||||
sprite->SetPosition(0.0f, 0.0f);
|
||||
newNaturalSizes[i] = sprite->GetSize();
|
||||
newSlices[i] = sprite;
|
||||
}
|
||||
|
||||
for (auto& sprite : sliceSprites_) {
|
||||
if (sprite && sprite->GetParent() == this) {
|
||||
RemoveChild(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
sliceSprites_ = newSlices;
|
||||
sliceNaturalSizes_ = newNaturalSizes;
|
||||
imgPath_ = imgPath;
|
||||
startIndex_ = startIndex;
|
||||
|
||||
Vec2 targetSize = GetSize();
|
||||
if (targetSize.x <= 0.0f && targetSize.y <= 0.0f) {
|
||||
targetSize = getNaturalSize();
|
||||
} else {
|
||||
targetSize.x = std::max(targetSize.x, 0.0f);
|
||||
targetSize.y = std::max(targetSize.y, 0.0f);
|
||||
}
|
||||
|
||||
for (auto& sprite : sliceSprites_) {
|
||||
AddChild(sprite);
|
||||
}
|
||||
|
||||
Actor::SetSize(targetSize);
|
||||
updateLayout();
|
||||
return true;
|
||||
}
|
||||
|
||||
void NineSliceActor::SetSize(const Vec2& size) {
|
||||
SetSize(size.x, size.y);
|
||||
}
|
||||
|
||||
void NineSliceActor::SetSize(float width, float height) {
|
||||
Actor::SetSize(std::max(width, 0.0f), std::max(height, 0.0f));
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
Vec2 NineSliceActor::getNaturalSize() const {
|
||||
return Vec2(sliceNaturalSizes_[TopLeft].x + sliceNaturalSizes_[Center].x +
|
||||
sliceNaturalSizes_[TopRight].x,
|
||||
sliceNaturalSizes_[TopLeft].y + sliceNaturalSizes_[Center].y +
|
||||
sliceNaturalSizes_[BottomLeft].y);
|
||||
}
|
||||
|
||||
void NineSliceActor::updateLayout() {
|
||||
if (imgPath_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
AxisLayout horizontal = resolveAxis(GetSize().x, sliceNaturalSizes_[TopLeft].x,
|
||||
sliceNaturalSizes_[TopRight].x);
|
||||
AxisLayout vertical = resolveAxis(GetSize().y, sliceNaturalSizes_[TopLeft].y,
|
||||
sliceNaturalSizes_[BottomLeft].y);
|
||||
|
||||
float leftX = 0.0f;
|
||||
float centerX = horizontal.start;
|
||||
float rightX = horizontal.start + horizontal.center;
|
||||
|
||||
float topY = 0.0f;
|
||||
float middleY = vertical.start;
|
||||
float bottomY = vertical.start + vertical.center;
|
||||
|
||||
applySliceLayout(sliceSprites_[TopLeft], Vec2(leftX, topY),
|
||||
Vec2(horizontal.start, vertical.start));
|
||||
applySliceLayout(sliceSprites_[Top], Vec2(centerX, topY),
|
||||
Vec2(horizontal.center, vertical.start));
|
||||
applySliceLayout(sliceSprites_[TopRight], Vec2(rightX, topY),
|
||||
Vec2(horizontal.end, vertical.start));
|
||||
|
||||
applySliceLayout(sliceSprites_[Left], Vec2(leftX, middleY),
|
||||
Vec2(horizontal.start, vertical.center));
|
||||
applySliceLayout(sliceSprites_[Center], Vec2(centerX, middleY),
|
||||
Vec2(horizontal.center, vertical.center));
|
||||
applySliceLayout(sliceSprites_[Right], Vec2(rightX, middleY),
|
||||
Vec2(horizontal.end, vertical.center));
|
||||
|
||||
applySliceLayout(sliceSprites_[BottomLeft], Vec2(leftX, bottomY),
|
||||
Vec2(horizontal.start, vertical.end));
|
||||
applySliceLayout(sliceSprites_[Bottom], Vec2(centerX, bottomY),
|
||||
Vec2(horizontal.center, vertical.end));
|
||||
applySliceLayout(sliceSprites_[BottomRight], Vec2(rightX, bottomY),
|
||||
Vec2(horizontal.end, vertical.end));
|
||||
}
|
||||
|
||||
void NineSliceActor::applySliceLayout(Ptr<Sprite> sprite, const Vec2& position,
|
||||
const Vec2& size) {
|
||||
if (!sprite) {
|
||||
return;
|
||||
}
|
||||
|
||||
sprite->SetPosition(position);
|
||||
|
||||
Vec2 clampedSize(std::max(size.x, 0.0f), std::max(size.y, 0.0f));
|
||||
if (clampedSize.x <= 0.0f || clampedSize.y <= 0.0f) {
|
||||
sprite->SetSize(0.0f, 0.0f);
|
||||
sprite->SetVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
sprite->SetVisible(true);
|
||||
sprite->SetSize(clampedSize);
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user