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
|
#pragma once
|
||||||
|
|
||||||
#include <frostbite2D/types/type_alias.h>
|
#include <frostbite2D/types/type_alias.h>
|
||||||
#include <frostbite2D/resource/binary_reader.h>
|
|
||||||
#include <frostbite2D/resource/asset.h>
|
#include <frostbite2D/resource/asset.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
class BinaryFileStreamReader;
|
||||||
|
|
||||||
struct ImageFrame {
|
struct ImageFrame {
|
||||||
int32 type = 0;
|
int32 type = 0;
|
||||||
int32 compressionType = 0;
|
int32 compressionType = 0;
|
||||||
@@ -74,29 +75,57 @@ public:
|
|||||||
size_t getDefaultImgFrame() const;
|
size_t getDefaultImgFrame() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum class IndexCacheValidationMode : uint32 {
|
||||||
|
TrustCache = 1,
|
||||||
|
StrictSourceState = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SourceFileState {
|
||||||
|
std::string fileName;
|
||||||
|
uint64 size = 0;
|
||||||
|
int64 writeTime = 0;
|
||||||
|
|
||||||
|
bool operator==(const SourceFileState& other) const {
|
||||||
|
return fileName == other.fileName && size == other.size &&
|
||||||
|
writeTime == other.writeTime;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
NpkArchive() = default;
|
NpkArchive() = default;
|
||||||
~NpkArchive() = default;
|
~NpkArchive() = default;
|
||||||
|
|
||||||
std::string normalizePath(const std::string& path) const;
|
std::string normalizePath(const std::string& path) const;
|
||||||
|
std::string getCacheFilePath() const;
|
||||||
|
IndexCacheValidationMode getIndexCacheValidationMode() const;
|
||||||
|
std::vector<SourceFileState> collectSourceFileStates() const;
|
||||||
|
bool loadIndexCache(const std::vector<SourceFileState>& sourceFiles);
|
||||||
|
bool saveIndexCache(const std::vector<SourceFileState>& sourceFiles) const;
|
||||||
void scanNpkFiles();
|
void scanNpkFiles();
|
||||||
bool parseNpkFile(const std::string& npkPath);
|
bool parseNpkFile(const std::string& npkPath);
|
||||||
bool loadImgData(ImgRef& img);
|
bool loadImgData(ImgRef& img);
|
||||||
void parseColor(const uint8* tab, int type, uint8* saveByte, int offset);
|
void parseColor(const uint8* tab, int type, uint8* saveByte, int offset);
|
||||||
void evictCacheIfNeeded(size_t requiredSize);
|
void evictCacheIfNeeded(size_t requiredSize);
|
||||||
void updateCacheUsage(const std::string& imgPath);
|
void updateCacheUsage(const std::string& imgPath);
|
||||||
std::string readNpkInfoString(BinaryReader& reader);
|
std::string readNpkInfoString(BinaryFileStreamReader& reader);
|
||||||
|
|
||||||
static const uint8 NPK_KEY[256];
|
static const uint8 NPK_KEY[256];
|
||||||
|
|
||||||
std::string imagePackDirectory_ = "ImagePacks2";
|
std::string imagePackDirectory_ = "ImagePacks2";
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
std::map<std::string, ImgRef> imgIndex_;
|
std::unordered_map<std::string, ImgRef> imgIndex_;
|
||||||
std::map<std::string, CachedImageData> imageCache_;
|
std::unordered_map<std::string, CachedImageData> imageCache_;
|
||||||
std::list<std::string> lruList_;
|
std::list<std::string> lruList_;
|
||||||
|
std::unordered_map<std::string, std::list<std::string>::iterator> lruLookup_;
|
||||||
size_t maxCacheSize_ = 512 * 1024 * 1024;
|
size_t maxCacheSize_ = 512 * 1024 * 1024;
|
||||||
size_t currentCacheSize_ = 0;
|
size_t currentCacheSize_ = 0;
|
||||||
std::string defaultImgPath_;
|
std::string defaultImgPath_;
|
||||||
size_t defaultImgFrame_ = 0;
|
size_t defaultImgFrame_ = 0;
|
||||||
|
bool verboseFallbackLog_ =
|
||||||
|
#ifndef NDEBUG
|
||||||
|
true;
|
||||||
|
#else
|
||||||
|
false;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,41 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||||
#include <frostbite2D/types/type_alias.h>
|
#include <frostbite2D/types/type_alias.h>
|
||||||
#include <frostbite2D/resource/binary_reader.h>
|
|
||||||
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief PVF 文件信息结构体
|
* @brief PVF ???????
|
||||||
*/
|
*/
|
||||||
struct PvfFileInfo {
|
struct PvfFileInfo {
|
||||||
size_t offset = 0; ///< 相对于数据起始位置的偏移
|
size_t offset = 0; ///< ????????????
|
||||||
uint32 crc32 = 0; ///< CRC32 校验值
|
uint32 crc32 = 0; ///< CRC32 ???
|
||||||
size_t length = 0; ///< 文件长度(字节)
|
size_t length = 0; ///< ????????
|
||||||
bool decoded = false; ///< 是否已解码
|
bool decoded = false; ///< ?????
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 原始文件数据结构体
|
* @brief ?????????
|
||||||
*/
|
*/
|
||||||
struct RawData {
|
struct RawData {
|
||||||
std::unique_ptr<char[]> data; ///< 原始数据指针(智能指针自动管理内存)
|
std::unique_ptr<char[]> data; ///< ??????????????????
|
||||||
size_t size; ///< 数据大小(字节)
|
size_t size; ///< ????????
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief PVF 资源包归档类
|
* @brief PVF ??????
|
||||||
*
|
*
|
||||||
* 用于读取和解析 PVF 格式的游戏资源包文件。
|
* ??????? PVF ???????????
|
||||||
* 提供文件索引管理、字符串资源访问和文件内容读取功能。
|
* ??????????????????????????
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* auto& archive = PvfArchive::get();
|
|
||||||
* if (archive.open("Script.pvf")) {
|
|
||||||
* archive.init();
|
|
||||||
* auto content = archive.getFileContent("script/example.txt");
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
class PvfArchive {
|
class PvfArchive {
|
||||||
public:
|
public:
|
||||||
/**
|
|
||||||
* @brief 获取单例实例
|
|
||||||
* @return PVF 归档实例引用
|
|
||||||
*/
|
|
||||||
static PvfArchive& get();
|
static PvfArchive& get();
|
||||||
|
|
||||||
PvfArchive(const PvfArchive&) = delete;
|
PvfArchive(const PvfArchive&) = delete;
|
||||||
@@ -53,166 +43,53 @@ public:
|
|||||||
PvfArchive(PvfArchive&&) = delete;
|
PvfArchive(PvfArchive&&) = delete;
|
||||||
PvfArchive& operator=(PvfArchive&&) = delete;
|
PvfArchive& operator=(PvfArchive&&) = delete;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 文件操作
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 打开 PVF 文件
|
|
||||||
* @param filePath 文件路径
|
|
||||||
* @return 打开成功返回 true
|
|
||||||
*/
|
|
||||||
bool open(const std::string& filePath = "Script.pvf");
|
bool open(const std::string& filePath = "Script.pvf");
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 关闭并清空所有数据
|
|
||||||
*/
|
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查是否已打开
|
|
||||||
* @return 已打开返回 true
|
|
||||||
*/
|
|
||||||
bool isOpen() const;
|
bool isOpen() const;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 初始化
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 完整初始化(解析头部、加载字符串表)
|
|
||||||
*/
|
|
||||||
void init();
|
void init();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 解析 PVF 文件头部并建立文件索引
|
|
||||||
*/
|
|
||||||
void initHeader();
|
void initHeader();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 加载二进制字符串表 (stringtable.bin)
|
|
||||||
*/
|
|
||||||
void initBinStringTable();
|
void initBinStringTable();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 加载本地化字符串 (n_string.lst)
|
|
||||||
*/
|
|
||||||
void initLoadStrings();
|
void initLoadStrings();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 文件信息查询
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查文件是否存在
|
|
||||||
* @param path 文件路径
|
|
||||||
* @return 存在返回 true
|
|
||||||
*/
|
|
||||||
bool hasFile(const std::string& path) const;
|
bool hasFile(const std::string& path) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取文件信息
|
|
||||||
* @param path 文件路径
|
|
||||||
* @return 文件信息,不存在返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<PvfFileInfo> getFileInfo(const std::string& path) const;
|
std::optional<PvfFileInfo> getFileInfo(const std::string& path) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取所有文件路径列表
|
|
||||||
* @return 文件路径列表
|
|
||||||
*/
|
|
||||||
std::vector<std::string> listFiles() const;
|
std::vector<std::string> listFiles() const;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 文件内容读取
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取文件内容作为字符串
|
|
||||||
* @param path 文件路径
|
|
||||||
* @return 文件内容,失败返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<std::string> getFileContent(const std::string& path);
|
std::optional<std::string> getFileContent(const std::string& path);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取文件内容作为字节数组
|
|
||||||
* @param path 文件路径
|
|
||||||
* @return 字节数组,失败返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<std::vector<uint8>> getFileBytes(const std::string& path);
|
std::optional<std::vector<uint8>> getFileBytes(const std::string& path);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取文件原始数据(使用智能指针管理内存,已包含 CRC 解密)
|
|
||||||
*
|
|
||||||
* 与原始 GetFileContentChar 功能相同,但使用智能指针避免内存泄漏。
|
|
||||||
* 数据会在首次访问时自动进行 CRC 解密。
|
|
||||||
*
|
|
||||||
* @param path 文件路径
|
|
||||||
* @return RawData 结构体,包含数据指针和大小;失败返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<RawData> getFileRawData(const std::string& path);
|
std::optional<RawData> getFileRawData(const std::string& path);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 字符串资源访问
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取二进制字符串
|
|
||||||
* @param key 字符串键
|
|
||||||
* @return 字符串,不存在返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<std::string> getBinString(int key) const;
|
std::optional<std::string> getBinString(int key) const;
|
||||||
|
std::optional<std::string> getLoadString(const std::string& type,
|
||||||
/**
|
const std::string& key) const;
|
||||||
* @brief 获取本地化字符串
|
|
||||||
* @param type 字符串类型
|
|
||||||
* @param key 字符串键
|
|
||||||
* @return 字符串,不存在返回 std::nullopt
|
|
||||||
*/
|
|
||||||
std::optional<std::string> getLoadString(const std::string& type, const std::string& key) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查二进制字符串是否存在
|
|
||||||
* @param key 字符串键
|
|
||||||
* @return 存在返回 true
|
|
||||||
*/
|
|
||||||
bool hasBinString(int key) const;
|
bool hasBinString(int key) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查本地化字符串是否存在
|
|
||||||
* @param type 字符串类型
|
|
||||||
* @param key 字符串键
|
|
||||||
* @return 存在返回 true
|
|
||||||
*/
|
|
||||||
bool hasLoadString(const std::string& type, const std::string& key) const;
|
bool hasLoadString(const std::string& type, const std::string& key) const;
|
||||||
|
|
||||||
std::string normalizePath(const std::string& path) const;
|
std::string normalizePath(const std::string& path) const;
|
||||||
std::string resolvePath(const std::string& baseDir, const std::string& path) const;
|
std::string resolvePath(const std::string& baseDir,
|
||||||
|
const std::string& path) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PvfArchive() = default;
|
PvfArchive() = default;
|
||||||
~PvfArchive() = default;
|
~PvfArchive() = default;
|
||||||
|
|
||||||
/**
|
std::vector<std::string> splitString(const std::string& str,
|
||||||
* @brief 分割字符串
|
const std::string& delimiter) const;
|
||||||
* @param str 要分割的字符串
|
bool decodeFile(const std::string& normalizedPath, PvfFileInfo& info);
|
||||||
* @param delimiter 分隔符
|
void clearInitData();
|
||||||
* @return 分割后的字符串列表
|
std::optional<std::vector<uint8>> readArchiveBytes(size_t absoluteOffset,
|
||||||
*/
|
size_t length);
|
||||||
std::vector<std::string> splitString(const std::string& str, const std::string& delimiter) const;
|
std::optional<std::vector<uint8>> readDecodedFileBytes(
|
||||||
|
const PvfFileInfo& info);
|
||||||
|
static void crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32);
|
||||||
|
|
||||||
/**
|
BinaryFileStreamReader reader_;
|
||||||
* @brief 解码文件内容(内部使用)
|
size_t dataStartPos_ = 0;
|
||||||
* @param info 文件信息(会修改 decoded 标志)
|
std::map<std::string, PvfFileInfo> fileInfo_;
|
||||||
* @return 成功返回 true
|
std::map<int, std::string> binStringTable_;
|
||||||
*/
|
std::map<std::string, std::map<std::string, std::string>> loadStrings_;
|
||||||
bool decodeFile(PvfFileInfo& info);
|
std::map<std::string, std::vector<uint8>> decodedFileCache_;
|
||||||
|
|
||||||
BinaryReader reader_; ///< 二进制读取器
|
|
||||||
size_t dataStartPos_ = 0; ///< 数据起始位置
|
|
||||||
std::map<std::string, PvfFileInfo> fileInfo_; ///< 文件信息映射
|
|
||||||
std::map<int, std::string> binStringTable_; ///< 二进制字符串表
|
|
||||||
std::map<std::string, std::map<std::string, std::string>> loadStrings_; ///< 本地化字符串
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace frostbite2D
|
} // namespace frostbite2D
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
class BinaryFileStreamReader;
|
||||||
|
|
||||||
struct AudioRef {
|
struct AudioRef {
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string npkFile;
|
std::string npkFile;
|
||||||
@@ -65,7 +67,7 @@ private:
|
|||||||
bool loadAudioData(AudioRef& audio);
|
bool loadAudioData(AudioRef& audio);
|
||||||
void evictCacheIfNeeded(size_t requiredSize);
|
void evictCacheIfNeeded(size_t requiredSize);
|
||||||
void updateCacheUsage(const std::string& audioPath);
|
void updateCacheUsage(const std::string& audioPath);
|
||||||
std::string readNpkInfoString(BinaryReader& reader);
|
std::string readNpkInfoString(BinaryFileStreamReader& reader);
|
||||||
|
|
||||||
static const uint8 NPK_KEY[256];
|
static const uint8 NPK_KEY[256];
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/event/event.h>
|
||||||
#include <frostbite2D/types/type_alias.h>
|
#include <frostbite2D/types/type_alias.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
class Scene;
|
class Scene;
|
||||||
|
class UIScene;
|
||||||
|
|
||||||
class SceneManager {
|
class SceneManager {
|
||||||
public:
|
public:
|
||||||
@@ -17,20 +19,31 @@ public:
|
|||||||
void PushScene(Ptr<Scene> scene);
|
void PushScene(Ptr<Scene> scene);
|
||||||
void PopScene();
|
void PopScene();
|
||||||
void ReplaceScene(Ptr<Scene> scene);
|
void ReplaceScene(Ptr<Scene> scene);
|
||||||
|
|
||||||
|
void PushUIScene(Ptr<UIScene> scene);
|
||||||
|
void PopUIScene();
|
||||||
|
void ReplaceUIScene(Ptr<UIScene> scene);
|
||||||
|
bool RemoveUIScene(UIScene* scene);
|
||||||
|
void ClearUIScenes();
|
||||||
void ClearAll();
|
void ClearAll();
|
||||||
|
|
||||||
void Update(float deltaTime);
|
void Update(float deltaTime);
|
||||||
|
void UpdateUI(float deltaTime);
|
||||||
void Render();
|
void Render();
|
||||||
|
|
||||||
|
bool DispatchEvent(const Event& event);
|
||||||
|
bool DispatchUIEvent(const Event& event);
|
||||||
|
|
||||||
Scene* GetCurrentScene() const;
|
Scene* GetCurrentScene() const;
|
||||||
|
UIScene* GetCurrentUIScene() const;
|
||||||
bool HasActiveScene() const;
|
bool HasActiveScene() const;
|
||||||
|
bool HasActiveUIScene() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SceneManager() = default;
|
SceneManager() = default;
|
||||||
// ~SceneManager() 在 shutdown() 中手动调用销毁
|
|
||||||
|
|
||||||
std::vector<Ptr<Scene>> sceneStack_;
|
std::vector<Ptr<Scene>> sceneStack_;
|
||||||
|
std::vector<Ptr<UIScene>> uiSceneStack_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace frostbite2D
|
||||||
|
|
||||||
|
|||||||
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 <frostbite2D/graphics/texture.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <SDL2/SDL_ttf.h>
|
#include <SDL2/SDL_ttf.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Uint32 readSurfacePixel(const SDL_Surface* surface, int x, int y) {
|
||||||
|
const uint8* row =
|
||||||
|
static_cast<const uint8*>(surface->pixels) + y * surface->pitch;
|
||||||
|
const uint8* pixel = row + x * surface->format->BytesPerPixel;
|
||||||
|
|
||||||
|
switch (surface->format->BytesPerPixel) {
|
||||||
|
case 1:
|
||||||
|
return *pixel;
|
||||||
|
case 2:
|
||||||
|
return *reinterpret_cast<const uint16*>(pixel);
|
||||||
|
case 3:
|
||||||
|
if (SDL_BYTEORDER == SDL_BIG_ENDIAN) {
|
||||||
|
return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
|
||||||
|
}
|
||||||
|
return pixel[0] | (pixel[1] << 8) | (pixel[2] << 16);
|
||||||
|
case 4:
|
||||||
|
return *reinterpret_cast<const uint32*>(pixel);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
TextSprite::TextSprite()
|
TextSprite::TextSprite()
|
||||||
: Sprite() {
|
: Sprite() {
|
||||||
}
|
}
|
||||||
@@ -79,27 +106,36 @@ void TextSprite::RenderText() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_PixelFormat* rgbaFormat = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
|
|
||||||
SDL_Surface* converted = SDL_ConvertSurface(surface, rgbaFormat, 0);
|
|
||||||
SDL_FreeSurface(surface);
|
|
||||||
surface = converted;
|
|
||||||
SDL_FreeFormat(rgbaFormat);
|
|
||||||
|
|
||||||
if (!surface) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to convert text surface: %s", SDL_GetError());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = surface->w;
|
int width = surface->w;
|
||||||
int height = surface->h;
|
int height = surface->h;
|
||||||
|
|
||||||
std::vector<uint8> rgbaData(width * height * 4);
|
std::vector<uint8> rgbaData(width * height * 4);
|
||||||
uint8* pixels = static_cast<uint8*>(surface->pixels);
|
if (SDL_MUSTLOCK(surface) && SDL_LockSurface(surface) != 0) {
|
||||||
for (int i = 0; i < width * height; i++) {
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to lock text surface: %s",
|
||||||
rgbaData[i * 4 + 0] = pixels[i * 4 + 0];
|
SDL_GetError());
|
||||||
rgbaData[i * 4 + 1] = pixels[i * 4 + 1];
|
SDL_FreeSurface(surface);
|
||||||
rgbaData[i * 4 + 2] = pixels[i * 4 + 2];
|
return;
|
||||||
rgbaData[i * 4 + 3] = pixels[i * 4 + 3];
|
}
|
||||||
|
|
||||||
|
for (int y = 0; y < height; ++y) {
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
Uint8 r = 0;
|
||||||
|
Uint8 g = 0;
|
||||||
|
Uint8 b = 0;
|
||||||
|
Uint8 a = 0;
|
||||||
|
Uint32 pixel = readSurfacePixel(surface, x, y);
|
||||||
|
SDL_GetRGBA(pixel, surface->format, &r, &g, &b, &a);
|
||||||
|
|
||||||
|
size_t offset = static_cast<size_t>((y * width + x) * 4);
|
||||||
|
rgbaData[offset + 0] = r;
|
||||||
|
rgbaData[offset + 1] = g;
|
||||||
|
rgbaData[offset + 2] = b;
|
||||||
|
rgbaData[offset + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_MUSTLOCK(surface)) {
|
||||||
|
SDL_UnlockSurface(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_FreeSurface(surface);
|
SDL_FreeSurface(surface);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
#include <frostbite2D/types/type_math.h>
|
#include <frostbite2D/types/type_math.h>
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
@@ -38,6 +39,8 @@ bool Application::init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Application::init(const AppConfig& config) {
|
bool Application::init(const AppConfig& config) {
|
||||||
|
ScopedStartupTrace startupTrace("Application::init internals");
|
||||||
|
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -46,11 +49,17 @@ bool Application::init(const AppConfig& config) {
|
|||||||
|
|
||||||
// 平台相关初始化
|
// 平台相关初始化
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
switchInit();
|
{
|
||||||
|
ScopedStartupTrace stageTrace("switchInit");
|
||||||
|
switchInit();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
windowsInit();
|
{
|
||||||
|
ScopedStartupTrace stageTrace("windowsInit");
|
||||||
|
windowsInit();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 初始化核心模块
|
// 初始化核心模块
|
||||||
@@ -122,6 +131,7 @@ Application::~Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Application::initCoreModules() {
|
bool Application::initCoreModules() {
|
||||||
|
ScopedStartupTrace startupTrace("Application::initCoreModules");
|
||||||
|
|
||||||
// 初始化资产管理器
|
// 初始化资产管理器
|
||||||
auto &asset = Asset::get();
|
auto &asset = Asset::get();
|
||||||
@@ -129,32 +139,44 @@ bool Application::initCoreModules() {
|
|||||||
// 平台相关 switch平台不可以获取当前工作目录
|
// 平台相关 switch平台不可以获取当前工作目录
|
||||||
#ifndef __SWITCH__
|
#ifndef __SWITCH__
|
||||||
// 获取程序工作目录
|
// 获取程序工作目录
|
||||||
std::string workingDir = SDL_GetBasePath();
|
{
|
||||||
asset.setWorkingDirectory(workingDir);
|
ScopedStartupTrace stageTrace("Asset working directory setup");
|
||||||
SDL_Log("Asset working directory: %s", workingDir.c_str());
|
std::string workingDir = SDL_GetBasePath();
|
||||||
|
asset.setWorkingDirectory(workingDir);
|
||||||
|
SDL_Log("Asset working directory: %s", workingDir.c_str());
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
|
asset.setWorkingDirectory("/switch/Frostbite2D/" + config_.appName);
|
||||||
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
|
SDL_Log("Asset working directory: %s", asset.getWorkingDirectory().c_str());
|
||||||
#endif
|
#endif
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER) != 0) {
|
{
|
||||||
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
|
ScopedStartupTrace stageTrace("SDL_Init video/events/controller");
|
||||||
return false;
|
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));
|
ScopedStartupTrace stageTrace("SDL_GameControllerOpen(0)");
|
||||||
} else {
|
if (SDL_GameController *controller = SDL_GameControllerOpen(0)) {
|
||||||
SDL_Log("No GameController found");
|
SDL_Log("GameController opened: %s", SDL_GameControllerName(controller));
|
||||||
|
} else {
|
||||||
|
SDL_Log("No GameController found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用SDL2创建窗口
|
// 使用SDL2创建窗口
|
||||||
this->window_ = new Window();
|
this->window_ = new Window();
|
||||||
|
|
||||||
// 创建窗口会使用窗口配置
|
// 创建窗口会使用窗口配置
|
||||||
if (!window_->create(config_.windowConfig)) {
|
{
|
||||||
SDL_Log("Failed to create window");
|
ScopedStartupTrace stageTrace("Window::create");
|
||||||
return false;
|
if (!window_->create(config_.windowConfig)) {
|
||||||
|
SDL_Log("Failed to create window");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_->onResize([this](int width, int height) {
|
window_->onResize([this](int width, int height) {
|
||||||
@@ -165,9 +187,12 @@ bool Application::initCoreModules() {
|
|||||||
|
|
||||||
// 初始化渲染器
|
// 初始化渲染器
|
||||||
renderer_ = &Renderer::get();
|
renderer_ = &Renderer::get();
|
||||||
if (!renderer_->init()) {
|
{
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize renderer");
|
ScopedStartupTrace stageTrace("Renderer::init");
|
||||||
return false;
|
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);
|
renderer_->setViewport(0, 0, config_.windowConfig.width, config_.windowConfig.height);
|
||||||
|
|
||||||
// 创建并设置相机
|
// 创建并设置相机
|
||||||
camera_ = new Camera();
|
{
|
||||||
camera_->setViewport(config_.windowConfig.width, config_.windowConfig.height);
|
ScopedStartupTrace stageTrace("Camera setup");
|
||||||
camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角
|
camera_ = new Camera();
|
||||||
renderer_->setCamera(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");
|
ScopedStartupTrace stageTrace("TaskSystem::init");
|
||||||
return false;
|
if (!TaskSystem::get().init()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize task system");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -215,6 +246,7 @@ void Application::run(StartCallback callback) {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartupTrace::mark("before Application::mainLoop");
|
||||||
mainLoop();
|
mainLoop();
|
||||||
|
|
||||||
running_ = false;
|
running_ = false;
|
||||||
@@ -257,10 +289,7 @@ void Application::mainLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TaskSystem::get().drainMainThreadTasks();
|
TaskSystem::get().drainMainThreadTasks();
|
||||||
|
update();
|
||||||
if (!paused_) {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -383,10 +412,12 @@ std::unique_ptr<Event> Application::convertSDLEvent(const SDL_Event& sdlEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::dispatchEvent(const Event& event) {
|
void Application::dispatchEvent(const Event& event) {
|
||||||
Scene* currentScene = SceneManager::get().GetCurrentScene();
|
if (paused_) {
|
||||||
if (currentScene) {
|
SceneManager::get().DispatchUIEvent(event);
|
||||||
currentScene->OnEvent(event);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SceneManager::get().DispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::update() {
|
void Application::update() {
|
||||||
@@ -395,7 +426,10 @@ void Application::update() {
|
|||||||
lastFrameTime_ = currentTime;
|
lastFrameTime_ = currentTime;
|
||||||
totalTime_ += deltaTime_;
|
totalTime_ += deltaTime_;
|
||||||
|
|
||||||
SceneManager::get().Update(deltaTime_);
|
if (!paused_) {
|
||||||
|
SceneManager::get().Update(deltaTime_);
|
||||||
|
}
|
||||||
|
SceneManager::get().UpdateUI(deltaTime_);
|
||||||
|
|
||||||
frameCount_++;
|
frameCount_++;
|
||||||
fpsTimer_ += deltaTime_;
|
fpsTimer_ += deltaTime_;
|
||||||
@@ -407,6 +441,7 @@ void Application::update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::render() {
|
void Application::render() {
|
||||||
|
static bool firstFramePresented = false;
|
||||||
Renderer& renderer = Renderer::get();
|
Renderer& renderer = Renderer::get();
|
||||||
|
|
||||||
renderer.beginFrame();
|
renderer.beginFrame();
|
||||||
@@ -416,6 +451,11 @@ void Application::render() {
|
|||||||
if (window_) {
|
if (window_) {
|
||||||
window_->swap();
|
window_->swap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstFramePresented) {
|
||||||
|
firstFramePresented = true;
|
||||||
|
StartupTrace::mark("first frame presented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppConfig& Application::getConfig() const {
|
const AppConfig& Application::getConfig() const {
|
||||||
|
|||||||
@@ -93,10 +93,18 @@ void Renderer::clear(uint32_t flags) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::setCamera(Camera* camera) {
|
void Renderer::setCamera(Camera* camera) {
|
||||||
|
if (initialized_ && camera_ != camera) {
|
||||||
|
batch_.flush();
|
||||||
|
}
|
||||||
|
|
||||||
camera_ = camera;
|
camera_ = camera;
|
||||||
if (camera) {
|
if (camera) {
|
||||||
camera_->setViewport(viewportWidth_, viewportHeight_);
|
camera_->setViewport(viewportWidth_, viewportHeight_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialized_) {
|
||||||
|
updateUniforms();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Renderer::setupBlendMode(BlendMode mode) {
|
void Renderer::setupBlendMode(BlendMode mode) {
|
||||||
@@ -159,6 +167,13 @@ void Renderer::updateUniforms() {
|
|||||||
coloredShader->setMat4("u_view", camera_->getViewMatrix());
|
coloredShader->setMat4("u_view", camera_->getViewMatrix());
|
||||||
coloredShader->setMat4("u_projection", camera_->getProjectionMatrix());
|
coloredShader->setMat4("u_projection", camera_->getProjectionMatrix());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto* textShader = shaderManager_.getShader("text");
|
||||||
|
if (textShader) {
|
||||||
|
textShader->use();
|
||||||
|
textShader->setMat4("u_view", camera_->getViewMatrix());
|
||||||
|
textShader->setMat4("u_projection", camera_->getProjectionMatrix());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/npk_archive.h>
|
||||||
|
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <system_error>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
constexpr char kIndexCacheFileName[] = ".frostbite_npk_index_cache.bin";
|
||||||
|
constexpr char kIndexCacheMagic[8] = {'F', 'B', 'N', 'P', 'K', 'I', 'D', 'X'};
|
||||||
|
constexpr uint32 kIndexCacheVersion = 2;
|
||||||
|
constexpr uint32 kMaxCacheStringLength = 4096;
|
||||||
|
constexpr uint32 kMaxCacheSourceFileCount = 20000;
|
||||||
|
constexpr uint32 kMaxCacheImageCount = 20000000;
|
||||||
|
|
||||||
|
fs::path toFsPath(const std::string& path) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return fs::u8path(path);
|
||||||
|
#else
|
||||||
|
return fs::path(path);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int64 toCacheTimestamp(const fs::file_time_type& value) {
|
||||||
|
auto duration =
|
||||||
|
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
value.time_since_epoch())
|
||||||
|
.count();
|
||||||
|
return static_cast<int64>(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool appendBytes(std::vector<uint8>& buffer, const void* data, size_t size) {
|
||||||
|
if (size == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.size() > buffer.max_size() - size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t oldSize = buffer.size();
|
||||||
|
buffer.resize(oldSize + size);
|
||||||
|
std::memcpy(buffer.data() + oldSize, data, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool appendPod(std::vector<uint8>& buffer, const T& value) {
|
||||||
|
return appendBytes(buffer, &value, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool readPod(std::ifstream& stream, T& value) {
|
||||||
|
stream.read(reinterpret_cast<char*>(&value), sizeof(T));
|
||||||
|
return stream.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool appendString(std::vector<uint8>& buffer, const std::string& value) {
|
||||||
|
if (value.size() > kMaxCacheStringLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 length = static_cast<uint32>(value.size());
|
||||||
|
if (!appendPod(buffer, length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return appendBytes(buffer, value.data(), length);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool readString(std::ifstream& stream, std::string& value) {
|
||||||
|
uint32 length = 0;
|
||||||
|
if (!readPod(stream, length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length > kMaxCacheStringLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.clear();
|
||||||
|
if (length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.resize(length);
|
||||||
|
stream.read(value.data(), length);
|
||||||
|
return stream.good();
|
||||||
|
}
|
||||||
|
|
||||||
const char* getZlibErrorName(int code) {
|
const char* getZlibErrorName(int code) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case Z_OK:
|
case Z_OK:
|
||||||
@@ -74,6 +170,7 @@ void NpkArchive::close() {
|
|||||||
imgIndex_.clear();
|
imgIndex_.clear();
|
||||||
imageCache_.clear();
|
imageCache_.clear();
|
||||||
lruList_.clear();
|
lruList_.clear();
|
||||||
|
lruLookup_.clear();
|
||||||
currentCacheSize_ = 0;
|
currentCacheSize_ = 0;
|
||||||
initialized_ = false;
|
initialized_ = false;
|
||||||
}
|
}
|
||||||
@@ -89,6 +186,291 @@ std::string NpkArchive::normalizePath(const std::string& path) const {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string NpkArchive::getCacheFilePath() const {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::string relativePath =
|
||||||
|
asset.combinePath(imagePackDirectory_, kIndexCacheFileName);
|
||||||
|
return asset.resolvePath(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
NpkArchive::IndexCacheValidationMode NpkArchive::getIndexCacheValidationMode()
|
||||||
|
const {
|
||||||
|
#ifdef __SWITCH__
|
||||||
|
return IndexCacheValidationMode::TrustCache;
|
||||||
|
#else
|
||||||
|
return IndexCacheValidationMode::StrictSourceState;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<NpkArchive::SourceFileState> NpkArchive::collectSourceFileStates()
|
||||||
|
const {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::string npkDir = asset.resolvePath(imagePackDirectory_);
|
||||||
|
std::vector<NpkArchive::SourceFileState> result;
|
||||||
|
|
||||||
|
if (!asset.isDirectory(npkDir)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
|
||||||
|
result.reserve(files.size());
|
||||||
|
|
||||||
|
for (const auto& file : files) {
|
||||||
|
std::error_code error;
|
||||||
|
fs::path path = toFsPath(file);
|
||||||
|
if (!fs::is_regular_file(path, error)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceFileState state;
|
||||||
|
state.fileName = asset.getFileName(file);
|
||||||
|
state.size = static_cast<uint64>(fs::file_size(path, error));
|
||||||
|
if (error) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to read file size for %s",
|
||||||
|
file.c_str());
|
||||||
|
state.size = asset.getFileSize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.clear();
|
||||||
|
fs::file_time_type lastWriteTime = fs::last_write_time(path, error);
|
||||||
|
if (error) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to read write time for %s",
|
||||||
|
file.c_str());
|
||||||
|
state.writeTime = 0;
|
||||||
|
} else {
|
||||||
|
state.writeTime = toCacheTimestamp(lastWriteTime);
|
||||||
|
}
|
||||||
|
result.push_back(std::move(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(result.begin(), result.end(),
|
||||||
|
[](const SourceFileState& left, const SourceFileState& right) {
|
||||||
|
return left.fileName < right.fileName;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NpkArchive::loadIndexCache(
|
||||||
|
const std::vector<SourceFileState>& sourceFiles) {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::string cachePath = getCacheFilePath();
|
||||||
|
if (!asset.isRegularFile(cachePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream stream(toFsPath(cachePath), std::ios::binary);
|
||||||
|
if (!stream.is_open()) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to open cache file %s",
|
||||||
|
cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
|
||||||
|
uint32 version = 0;
|
||||||
|
uint32 cachedValidationMode = 0;
|
||||||
|
uint32 cachedSourceCount = 0;
|
||||||
|
uint32 cachedImageCount = 0;
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache header");
|
||||||
|
char magic[sizeof(kIndexCacheMagic)] = {};
|
||||||
|
stream.read(magic, sizeof(magic));
|
||||||
|
if (!stream.good() ||
|
||||||
|
std::memcmp(magic, kIndexCacheMagic, sizeof(kIndexCacheMagic)) != 0) {
|
||||||
|
SDL_Log("NpkArchive: cache magic mismatch, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readPod(stream, version) || !readPod(stream, cachedValidationMode) ||
|
||||||
|
!readPod(stream, cachedSourceCount) || !readPod(stream, cachedImageCount)) {
|
||||||
|
SDL_Log("NpkArchive: cache header incomplete, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version != kIndexCacheVersion) {
|
||||||
|
SDL_Log("NpkArchive: cache version mismatch (%u != %u), will rescan",
|
||||||
|
version, kIndexCacheVersion);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedSourceCount > kMaxCacheSourceFileCount ||
|
||||||
|
cachedImageCount > kMaxCacheImageCount) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: cache counts look invalid (sources=%u, images=%u)",
|
||||||
|
cachedSourceCount, cachedImageCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedValidationMode != static_cast<uint32>(validationMode)) {
|
||||||
|
SDL_Log("NpkArchive: cache validation mode mismatch (%u != %u), will rescan",
|
||||||
|
cachedValidationMode, static_cast<uint32>(validationMode));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||||
|
if (sourceFiles.empty()) {
|
||||||
|
SDL_Log("NpkArchive: strict cache validation requires source metadata, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SourceFileState> cachedSourceFiles;
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache sources");
|
||||||
|
cachedSourceFiles.reserve(cachedSourceCount);
|
||||||
|
for (uint32 i = 0; i < cachedSourceCount; ++i) {
|
||||||
|
SourceFileState state;
|
||||||
|
if (!readString(stream, state.fileName) || !readPod(stream, state.size) ||
|
||||||
|
!readPod(stream, state.writeTime)) {
|
||||||
|
SDL_Log("NpkArchive: cache source file section incomplete, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cachedSourceFiles.push_back(std::move(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedSourceFiles != sourceFiles) {
|
||||||
|
SDL_Log("NpkArchive: cache invalidated by directory changes, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (cachedSourceCount != 0) {
|
||||||
|
SDL_Log("NpkArchive: trusted cache should not contain source metadata, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::string, ImgRef> cachedIndex;
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache images");
|
||||||
|
cachedIndex.reserve(cachedImageCount);
|
||||||
|
for (uint32 i = 0; i < cachedImageCount; ++i) {
|
||||||
|
std::string path;
|
||||||
|
std::string npkFile;
|
||||||
|
ImgRef img;
|
||||||
|
if (!readString(stream, path) || !readString(stream, npkFile) ||
|
||||||
|
!readPod(stream, img.frameCount) || !readPod(stream, img.offset) ||
|
||||||
|
!readPod(stream, img.size)) {
|
||||||
|
SDL_Log("NpkArchive: cache image section incomplete, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.path = path;
|
||||||
|
img.npkFile = std::move(npkFile);
|
||||||
|
img.loaded = false;
|
||||||
|
cachedIndex.emplace(std::move(path), std::move(img));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stream.good() && !stream.eof()) {
|
||||||
|
SDL_Log("NpkArchive: cache read failed, will rescan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache commit");
|
||||||
|
imgIndex_.swap(cachedIndex);
|
||||||
|
}
|
||||||
|
SDL_Log("NpkArchive: loaded %zu image refs from cache %s", imgIndex_.size(),
|
||||||
|
cachePath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NpkArchive::saveIndexCache(
|
||||||
|
const std::vector<SourceFileState>& sourceFiles) const {
|
||||||
|
std::string cachePath = getCacheFilePath();
|
||||||
|
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
|
||||||
|
uint32 sourceCount =
|
||||||
|
validationMode == IndexCacheValidationMode::StrictSourceState
|
||||||
|
? static_cast<uint32>(sourceFiles.size())
|
||||||
|
: 0;
|
||||||
|
uint32 imageCount = static_cast<uint32>(imgIndex_.size());
|
||||||
|
std::vector<uint8> buffer;
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache serialize");
|
||||||
|
size_t estimatedSize = sizeof(kIndexCacheMagic) + sizeof(uint32) * 4;
|
||||||
|
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||||
|
for (const auto& sourceFile : sourceFiles) {
|
||||||
|
estimatedSize += sizeof(uint32) + sourceFile.fileName.size();
|
||||||
|
estimatedSize += sizeof(sourceFile.size);
|
||||||
|
estimatedSize += sizeof(sourceFile.writeTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [path, img] : imgIndex_) {
|
||||||
|
estimatedSize += sizeof(uint32) + path.size();
|
||||||
|
estimatedSize += sizeof(uint32) + img.npkFile.size();
|
||||||
|
estimatedSize += sizeof(img.frameCount);
|
||||||
|
estimatedSize += sizeof(img.offset);
|
||||||
|
estimatedSize += sizeof(img.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.reserve(estimatedSize);
|
||||||
|
|
||||||
|
if (!appendBytes(buffer, kIndexCacheMagic, sizeof(kIndexCacheMagic)) ||
|
||||||
|
!appendPod(buffer, kIndexCacheVersion) ||
|
||||||
|
!appendPod(buffer, static_cast<uint32>(validationMode)) ||
|
||||||
|
!appendPod(buffer, sourceCount) ||
|
||||||
|
!appendPod(buffer, imageCount)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to serialize cache header %s",
|
||||||
|
cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||||
|
for (const auto& sourceFile : sourceFiles) {
|
||||||
|
if (!appendString(buffer, sourceFile.fileName) ||
|
||||||
|
!appendPod(buffer, sourceFile.size) ||
|
||||||
|
!appendPod(buffer, sourceFile.writeTime)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to serialize cache source data %s",
|
||||||
|
cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [path, img] : imgIndex_) {
|
||||||
|
if (!appendString(buffer, path) || !appendString(buffer, img.npkFile) ||
|
||||||
|
!appendPod(buffer, img.frameCount) || !appendPod(buffer, img.offset) ||
|
||||||
|
!appendPod(buffer, img.size)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to serialize cache image data %s",
|
||||||
|
cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache flush");
|
||||||
|
std::ofstream stream(toFsPath(cachePath),
|
||||||
|
std::ios::binary | std::ios::trunc);
|
||||||
|
if (!stream.is_open()) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: failed to create cache file %s",
|
||||||
|
cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buffer.empty()) {
|
||||||
|
stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stream.good()) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NpkArchive: cache flush failed %s", cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("NpkArchive: wrote index cache %s (mode=%u, %u source files, %u images)",
|
||||||
|
cachePath.c_str(), static_cast<uint32>(validationMode), sourceCount,
|
||||||
|
imageCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void NpkArchive::scanNpkFiles() {
|
void NpkArchive::scanNpkFiles() {
|
||||||
Asset& asset = Asset::get();
|
Asset& asset = Asset::get();
|
||||||
std::string npkDir = asset.resolvePath(imagePackDirectory_);
|
std::string npkDir = asset.resolvePath(imagePackDirectory_);
|
||||||
@@ -97,16 +479,58 @@ void NpkArchive::scanNpkFiles() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imgIndex_.clear();
|
||||||
|
IndexCacheValidationMode validationMode = getIndexCacheValidationMode();
|
||||||
|
|
||||||
|
std::vector<SourceFileState> sourceFiles;
|
||||||
|
if (validationMode == IndexCacheValidationMode::StrictSourceState) {
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive collect source states");
|
||||||
|
sourceFiles = collectSourceFileStates();
|
||||||
|
}
|
||||||
|
if (sourceFiles.empty()) {
|
||||||
|
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache strict-load");
|
||||||
|
if (loadIndexCache(sourceFiles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("NpkArchive cache fast-load");
|
||||||
|
if (loadIndexCache(sourceFiles)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
|
std::vector<std::string> files = asset.listFilesWithExtension(npkDir, ".npk");
|
||||||
|
std::sort(files.begin(), files.end());
|
||||||
|
if (files.empty()) {
|
||||||
|
SDL_Log("NpkArchive: no NPK files found in %s", npkDir.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
|
SDL_Log("Scanning %d NPK files...", static_cast<int>(files.size()));
|
||||||
for (const auto &file : files) {
|
{
|
||||||
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) {
|
bool NpkArchive::parseNpkFile(const std::string& npkPath) {
|
||||||
Asset& asset = Asset::get();
|
Asset& asset = Asset::get();
|
||||||
BinaryReader reader(npkPath);
|
BinaryFileStreamReader reader(npkPath);
|
||||||
|
|
||||||
if (!reader.isOpen()) {
|
if (!reader.isOpen()) {
|
||||||
SDL_Log("Failed to open NPK file: %s", npkPath.c_str());
|
SDL_Log("Failed to open NPK file: %s", npkPath.c_str());
|
||||||
@@ -158,6 +582,7 @@ std::vector<std::string> NpkArchive::listImgs() const {
|
|||||||
for (const auto& pair : imgIndex_) {
|
for (const auto& pair : imgIndex_) {
|
||||||
result.push_back(pair.first);
|
result.push_back(pair.first);
|
||||||
}
|
}
|
||||||
|
std::sort(result.begin(), result.end());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +628,7 @@ bool NpkArchive::loadImgData(ImgRef& img) {
|
|||||||
std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile);
|
std::string npkPath = asset.combinePath(imagePackDirectory_, img.npkFile);
|
||||||
npkPath = asset.resolvePath(npkPath);
|
npkPath = asset.resolvePath(npkPath);
|
||||||
|
|
||||||
BinaryReader reader(npkPath);
|
BinaryFileStreamReader reader(npkPath);
|
||||||
if (!reader.isOpen()) {
|
if (!reader.isOpen()) {
|
||||||
SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str());
|
SDL_Log("Failed to open NPK for IMG: %s", npkPath.c_str());
|
||||||
return false;
|
return false;
|
||||||
@@ -290,19 +715,26 @@ bool NpkArchive::loadImgData(ImgRef& img) {
|
|||||||
frame.size == static_cast<uint32>(deSize)) {
|
frame.size == static_cast<uint32>(deSize)) {
|
||||||
frame.data = std::move(compressedData);
|
frame.data = std::move(compressedData);
|
||||||
cachedData.memoryUsage += frame.data.size();
|
cachedData.memoryUsage += frame.data.size();
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
if (verboseFallbackLog_) {
|
||||||
"NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"type=%d, compression=%d, size=%dx%d",
|
"NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, "
|
||||||
img.path.c_str(), i, frame.type, frame.compressionType,
|
"type=%d, compression=%d, size=%dx%d",
|
||||||
frame.width, frame.height);
|
img.path.c_str(), i, frame.type, frame.compressionType,
|
||||||
|
frame.width, frame.height);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
if (verboseFallbackLog_) {
|
||||||
"Failed to uncompress image data: %d (%s), img=%s, npk=%s, frame=%d, "
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"type=%d, compression=%d, size=%dx%d, offset=%u, packed=%u, expected=%d",
|
"Failed to uncompress image data: %d (%s), img=%s, "
|
||||||
uncompressResult, getZlibErrorName(uncompressResult), img.path.c_str(),
|
"npk=%s, frame=%d, "
|
||||||
img.npkFile.c_str(), i, frame.type, frame.compressionType, frame.width,
|
"type=%d, compression=%d, size=%dx%d, offset=%u, "
|
||||||
frame.height, frame.offset, frame.size, deSize);
|
"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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,9 +757,16 @@ bool NpkArchive::loadImgData(ImgRef& img) {
|
|||||||
cachedData.memoryUsage += frame.data.size();
|
cachedData.memoryUsage += frame.data.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
imageCache_[img.path] = std::move(cachedData);
|
auto existingCacheIt = imageCache_.find(img.path);
|
||||||
currentCacheSize_ += imageCache_[img.path].memoryUsage;
|
if (existingCacheIt != imageCache_.end()) {
|
||||||
lruList_.push_front(img.path);
|
currentCacheSize_ -= existingCacheIt->second.memoryUsage;
|
||||||
|
imageCache_.erase(existingCacheIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
evictCacheIfNeeded(cachedData.memoryUsage);
|
||||||
|
auto cacheIt = imageCache_.emplace(img.path, std::move(cachedData)).first;
|
||||||
|
currentCacheSize_ += cacheIt->second.memoryUsage;
|
||||||
|
updateCacheUsage(img.path);
|
||||||
|
|
||||||
img.loaded = true;
|
img.loaded = true;
|
||||||
imgIndex_[img.path] = img;
|
imgIndex_[img.path] = img;
|
||||||
@@ -372,6 +811,7 @@ void NpkArchive::setCacheSize(size_t maxBytes) {
|
|||||||
void NpkArchive::clearCache() {
|
void NpkArchive::clearCache() {
|
||||||
imageCache_.clear();
|
imageCache_.clear();
|
||||||
lruList_.clear();
|
lruList_.clear();
|
||||||
|
lruLookup_.clear();
|
||||||
currentCacheSize_ = 0;
|
currentCacheSize_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +823,7 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
|
|||||||
while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
|
while (currentCacheSize_ + requiredSize > maxCacheSize_ && !lruList_.empty()) {
|
||||||
std::string oldest = lruList_.back();
|
std::string oldest = lruList_.back();
|
||||||
lruList_.pop_back();
|
lruList_.pop_back();
|
||||||
|
lruLookup_.erase(oldest);
|
||||||
|
|
||||||
auto it = imageCache_.find(oldest);
|
auto it = imageCache_.find(oldest);
|
||||||
if (it != imageCache_.end()) {
|
if (it != imageCache_.end()) {
|
||||||
@@ -393,11 +834,12 @@ void NpkArchive::evictCacheIfNeeded(size_t requiredSize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NpkArchive::updateCacheUsage(const std::string& imgPath) {
|
void NpkArchive::updateCacheUsage(const std::string& imgPath) {
|
||||||
auto it = std::find(lruList_.begin(), lruList_.end(), imgPath);
|
auto lruIt = lruLookup_.find(imgPath);
|
||||||
if (it != lruList_.end()) {
|
if (lruIt != lruLookup_.end()) {
|
||||||
lruList_.erase(it);
|
lruList_.erase(lruIt->second);
|
||||||
}
|
}
|
||||||
lruList_.push_front(imgPath);
|
lruList_.push_front(imgPath);
|
||||||
|
lruLookup_[imgPath] = lruList_.begin();
|
||||||
|
|
||||||
auto cacheIt = imageCache_.find(imgPath);
|
auto cacheIt = imageCache_.find(imgPath);
|
||||||
if (cacheIt != imageCache_.end()) {
|
if (cacheIt != imageCache_.end()) {
|
||||||
@@ -405,7 +847,7 @@ void NpkArchive::updateCacheUsage(const std::string& imgPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string NpkArchive::readNpkInfoString(BinaryReader& reader) {
|
std::string NpkArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
|
||||||
if (reader.eof()) {
|
if (reader.eof()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,47 @@
|
|||||||
#include <frostbite2D/resource/pvf_archive.h>
|
#include <frostbite2D/resource/pvf_archive.h>
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <sstream>
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool canReadSpan(const std::vector<uint8>& bytes, size_t offset, size_t length) {
|
||||||
|
return offset <= bytes.size() && length <= bytes.size() - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool readPodAt(const std::vector<uint8>& bytes, size_t offset, T& value) {
|
||||||
|
if (!canReadSpan(bytes, offset, sizeof(T))) {
|
||||||
|
value = T{};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(&value, bytes.data() + offset, sizeof(T));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readStringAt(const std::vector<uint8>& bytes, size_t offset,
|
||||||
|
size_t length) {
|
||||||
|
if (!canReadSpan(bytes, offset, length)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string(reinterpret_cast<const char*>(bytes.data() + offset),
|
||||||
|
length);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
PvfArchive& PvfArchive::get() {
|
PvfArchive& PvfArchive::get() {
|
||||||
static PvfArchive instance;
|
static PvfArchive instance;
|
||||||
return instance;
|
return instance;
|
||||||
@@ -14,8 +50,10 @@ PvfArchive& PvfArchive::get() {
|
|||||||
bool PvfArchive::open(const std::string& filePath) {
|
bool PvfArchive::open(const std::string& filePath) {
|
||||||
close();
|
close();
|
||||||
|
|
||||||
|
ScopedStartupTrace stageTrace("PvfArchive::open stream");
|
||||||
if (!reader_.open(filePath)) {
|
if (!reader_.open(filePath)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 无法打开文件: %s", filePath.c_str());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: unable to open file: %s", filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,10 +62,7 @@ bool PvfArchive::open(const std::string& filePath) {
|
|||||||
|
|
||||||
void PvfArchive::close() {
|
void PvfArchive::close() {
|
||||||
reader_.close();
|
reader_.close();
|
||||||
dataStartPos_ = 0;
|
clearInitData();
|
||||||
fileInfo_.clear();
|
|
||||||
binStringTable_.clear();
|
|
||||||
loadStrings_.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PvfArchive::isOpen() const {
|
bool PvfArchive::isOpen() const {
|
||||||
@@ -36,16 +71,39 @@ bool PvfArchive::isOpen() const {
|
|||||||
|
|
||||||
void PvfArchive::init() {
|
void PvfArchive::init() {
|
||||||
if (!isOpen()) {
|
if (!isOpen()) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 请先打开文件再初始化");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: open the archive before calling init");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
initHeader();
|
clearInitData();
|
||||||
initBinStringTable();
|
|
||||||
initLoadStrings();
|
{
|
||||||
|
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() {
|
void PvfArchive::initHeader() {
|
||||||
|
fileInfo_.clear();
|
||||||
|
dataStartPos_ = 0;
|
||||||
|
decodedFileCache_.clear();
|
||||||
|
|
||||||
if (!isOpen()) {
|
if (!isOpen()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,7 +111,13 @@ void PvfArchive::initHeader() {
|
|||||||
reader_.seek(0);
|
reader_.seek(0);
|
||||||
|
|
||||||
int32 uuidLength = reader_.readInt32();
|
int32 uuidLength = reader_.readInt32();
|
||||||
std::string uuid = reader_.readString(uuidLength);
|
if (uuidLength < 0 || static_cast<size_t>(uuidLength) > reader_.remaining()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: invalid uuid length in archive header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string uuid = reader_.readString(static_cast<size_t>(uuidLength));
|
||||||
(void)uuid;
|
(void)uuid;
|
||||||
|
|
||||||
int32 version = reader_.readInt32();
|
int32 version = reader_.readInt32();
|
||||||
@@ -63,126 +127,201 @@ void PvfArchive::initHeader() {
|
|||||||
int32 indexHeaderCrc = reader_.readInt32();
|
int32 indexHeaderCrc = reader_.readInt32();
|
||||||
int32 indexSize = reader_.readInt32();
|
int32 indexSize = reader_.readInt32();
|
||||||
|
|
||||||
size_t firstPos = reader_.tell();
|
if (alignedIndexHeaderSize <= 0 || indexSize < 0) {
|
||||||
reader_.crcDecode(alignedIndexHeaderSize, indexHeaderCrc);
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: invalid index header metadata (size=%d, count=%d)",
|
||||||
|
alignedIndexHeaderSize, indexSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dataStartPos_ = alignedIndexHeaderSize + 56;
|
size_t indexHeaderPos = reader_.tell();
|
||||||
size_t currPos = 0;
|
auto headerBytesOpt =
|
||||||
|
readArchiveBytes(indexHeaderPos, static_cast<size_t>(alignedIndexHeaderSize));
|
||||||
|
if (!headerBytesOpt.has_value()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8> headerBytes = std::move(headerBytesOpt.value());
|
||||||
|
crcDecodeBuffer(headerBytes, static_cast<uint32>(indexHeaderCrc));
|
||||||
|
dataStartPos_ = indexHeaderPos + static_cast<size_t>(alignedIndexHeaderSize);
|
||||||
|
|
||||||
|
size_t cursor = 0;
|
||||||
for (int32 i = 0; i < indexSize; ++i) {
|
for (int32 i = 0; i < indexSize; ++i) {
|
||||||
reader_.seek(firstPos + currPos);
|
int32 fileNumber = 0;
|
||||||
|
int32 filePathLength = 0;
|
||||||
|
int32 fileLength = 0;
|
||||||
|
int32 crc32 = 0;
|
||||||
|
int32 relativeOffset = 0;
|
||||||
|
|
||||||
int32 fileNumber = reader_.readInt32();
|
if (!readPodAt(headerBytes, cursor, fileNumber) ||
|
||||||
|
!readPodAt(headerBytes, cursor + 4, filePathLength)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: truncated index header at entry %d", i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
(void)fileNumber;
|
(void)fileNumber;
|
||||||
|
cursor += 8;
|
||||||
|
|
||||||
int32 filePathLength = reader_.readInt32();
|
if (filePathLength < 0) {
|
||||||
std::string fileName = normalizePath(reader_.readString(filePathLength));
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: negative path length in index entry %d", i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
int32 fileLength = reader_.readInt32();
|
size_t pathLength = static_cast<size_t>(filePathLength);
|
||||||
int32 crc32 = reader_.readInt32();
|
if (!canReadSpan(headerBytes, cursor, pathLength + 12)) {
|
||||||
int32 relativeOffset = reader_.readInt32();
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: truncated file entry payload at index %d", i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (fileLength > 0) {
|
std::string fileName =
|
||||||
int32 realFileLength = (fileLength + 3) & 0xFFFFFFFC;
|
normalizePath(readStringAt(headerBytes, cursor, pathLength));
|
||||||
|
cursor += pathLength;
|
||||||
|
|
||||||
|
readPodAt(headerBytes, cursor, fileLength);
|
||||||
|
readPodAt(headerBytes, cursor + 4, crc32);
|
||||||
|
readPodAt(headerBytes, cursor + 8, relativeOffset);
|
||||||
|
cursor += 12;
|
||||||
|
|
||||||
|
if (fileLength > 0 && relativeOffset >= 0) {
|
||||||
|
uint32 alignedLength = (static_cast<uint32>(fileLength) + 3u) & 0xFFFFFFFCu;
|
||||||
PvfFileInfo info;
|
PvfFileInfo info;
|
||||||
info.offset = relativeOffset;
|
info.offset = static_cast<size_t>(relativeOffset);
|
||||||
info.crc32 = static_cast<uint32>(crc32);
|
info.crc32 = static_cast<uint32>(crc32);
|
||||||
info.length = realFileLength;
|
info.length = static_cast<size_t>(alignedLength);
|
||||||
info.decoded = false;
|
info.decoded = false;
|
||||||
fileInfo_[fileName] = info;
|
fileInfo_[fileName] = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
currPos += 20;
|
|
||||||
currPos += filePathLength;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PvfArchive::initBinStringTable() {
|
void PvfArchive::initBinStringTable() {
|
||||||
std::string tablePath = "stringtable.bin";
|
binStringTable_.clear();
|
||||||
auto infoOpt = getFileInfo(tablePath);
|
|
||||||
|
|
||||||
|
auto infoOpt = getFileInfo("stringtable.bin");
|
||||||
if (!infoOpt.has_value()) {
|
if (!infoOpt.has_value()) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: stringtable.bin 文件不存在");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: stringtable.bin was not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PvfFileInfo info = infoOpt.value();
|
auto bytesOpt = readDecodedFileBytes(infoOpt.value());
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
if (!bytesOpt.has_value()) {
|
||||||
reader_.crcDecode(info.length, info.crc32);
|
return;
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
}
|
||||||
|
|
||||||
size_t fileHeaderPos = reader_.tell();
|
const std::vector<uint8>& bytes = bytesOpt.value();
|
||||||
int32 count = reader_.readInt32();
|
int32 count = 0;
|
||||||
|
if (!readPodAt(bytes, 0, count) || count < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: invalid stringtable.bin header");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (int32 i = 0; i < count; ++i) {
|
for (int32 i = 0; i < count; ++i) {
|
||||||
reader_.seek(fileHeaderPos + i * 4 + 4);
|
size_t offsetsPos = static_cast<size_t>(i) * 4 + 4;
|
||||||
int32 startPos = reader_.readInt32();
|
int32 startPos = 0;
|
||||||
int32 endPos = reader_.readInt32();
|
int32 endPos = 0;
|
||||||
int32 len = endPos - startPos;
|
if (!readPodAt(bytes, offsetsPos, startPos) ||
|
||||||
|
!readPodAt(bytes, offsetsPos + 4, endPos)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: truncated string table at entry %d", i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
reader_.seek(fileHeaderPos + startPos + 4);
|
if (startPos < 0 || endPos < startPos) {
|
||||||
std::string str = reader_.readString(len);
|
continue;
|
||||||
binStringTable_[i] = str;
|
}
|
||||||
|
|
||||||
|
size_t stringPos = static_cast<size_t>(startPos) + 4;
|
||||||
|
size_t length = static_cast<size_t>(endPos - startPos);
|
||||||
|
if (!canReadSpan(bytes, stringPos, length)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: invalid string span at entry %d", i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
binStringTable_[i] = readStringAt(bytes, stringPos, length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PvfArchive::initLoadStrings() {
|
void PvfArchive::initLoadStrings() {
|
||||||
std::string lstPath = "n_string.lst";
|
loadStrings_.clear();
|
||||||
auto infoOpt = getFileInfo(lstPath);
|
|
||||||
|
|
||||||
|
auto infoOpt = getFileInfo("n_string.lst");
|
||||||
if (!infoOpt.has_value()) {
|
if (!infoOpt.has_value()) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: n_string.lst 文件不存在");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: n_string.lst was not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PvfFileInfo info = infoOpt.value();
|
auto listBytesOpt = readDecodedFileBytes(infoOpt.value());
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
if (!listBytesOpt.has_value()) {
|
||||||
reader_.crcDecode(info.length, info.crc32);
|
return;
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
}
|
||||||
|
|
||||||
size_t fileHeaderPos = reader_.tell();
|
const std::vector<uint8>& listBytes = listBytesOpt.value();
|
||||||
int16 flag = reader_.readInt16();
|
if (listBytes.size() < sizeof(int16)) {
|
||||||
(void)flag;
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: n_string.lst is too small");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
size_t i = 2;
|
std::map<std::string, std::string> stringFileCache;
|
||||||
while (i < info.length) {
|
size_t cursor = sizeof(int16);
|
||||||
if ((info.length - i) >= 10) {
|
while (cursor + 10 <= listBytes.size()) {
|
||||||
reader_.seek(fileHeaderPos + i + 6);
|
int32 findKey = 0;
|
||||||
int32 findKey = reader_.readInt32();
|
if (!readPodAt(listBytes, cursor + 6, findKey)) {
|
||||||
|
|
||||||
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 {
|
|
||||||
break;
|
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;
|
PvfFileInfo& info = it->second;
|
||||||
if (!decodeFile(info)) {
|
if (!decodeFile(normalizedPath, info)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
auto cacheIt = decodedFileCache_.find(normalizedPath);
|
||||||
std::vector<uint8> bytes = reader_.readBytes(info.length);
|
if (cacheIt == decodedFileCache_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint8>& bytes = cacheIt->second;
|
||||||
|
if (bytes.empty()) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +379,16 @@ std::optional<std::vector<uint8>> PvfArchive::getFileBytes(const std::string& pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
PvfFileInfo& info = it->second;
|
PvfFileInfo& info = it->second;
|
||||||
if (!decodeFile(info)) {
|
if (!decodeFile(normalizedPath, info)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
auto cacheIt = decodedFileCache_.find(normalizedPath);
|
||||||
return reader_.readBytes(info.length);
|
if (cacheIt == decodedFileCache_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheIt->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
|
std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
|
||||||
@@ -249,17 +399,22 @@ std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PvfFileInfo& info = it->second;
|
PvfFileInfo& info = it->second;
|
||||||
if (!decodeFile(info)) {
|
if (!decodeFile(normalizedPath, info)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
auto cacheIt = decodedFileCache_.find(normalizedPath);
|
||||||
|
if (cacheIt == decodedFileCache_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<uint8>& bytes = cacheIt->second;
|
||||||
RawData result;
|
RawData result;
|
||||||
result.size = info.length;
|
result.size = bytes.size();
|
||||||
result.data = std::make_unique<char[]>(info.length);
|
result.data = std::make_unique<char[]>(bytes.size());
|
||||||
reader_.read(result.data.get(), info.length);
|
if (!bytes.empty()) {
|
||||||
|
std::memcpy(result.data.get(), bytes.data(), bytes.size());
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +426,8 @@ std::optional<std::string> PvfArchive::getBinString(int key) const {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> PvfArchive::getLoadString(const std::string& type, const std::string& key) const {
|
std::optional<std::string> PvfArchive::getLoadString(const std::string& type,
|
||||||
|
const std::string& key) const {
|
||||||
std::string normalizedType = normalizePath(type);
|
std::string normalizedType = normalizePath(type);
|
||||||
auto typeIt = loadStrings_.find(normalizedType);
|
auto typeIt = loadStrings_.find(normalizedType);
|
||||||
if (typeIt != loadStrings_.end()) {
|
if (typeIt != loadStrings_.end()) {
|
||||||
@@ -287,7 +443,8 @@ bool PvfArchive::hasBinString(int key) const {
|
|||||||
return binStringTable_.find(key) != binStringTable_.end();
|
return binStringTable_.find(key) != binStringTable_.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PvfArchive::hasLoadString(const std::string& type, const std::string& key) const {
|
bool PvfArchive::hasLoadString(const std::string& type,
|
||||||
|
const std::string& key) const {
|
||||||
std::string normalizedType = normalizePath(type);
|
std::string normalizedType = normalizePath(type);
|
||||||
auto typeIt = loadStrings_.find(normalizedType);
|
auto typeIt = loadStrings_.find(normalizedType);
|
||||||
if (typeIt != loadStrings_.end()) {
|
if (typeIt != loadStrings_.end()) {
|
||||||
@@ -334,7 +491,8 @@ std::string PvfArchive::normalizePath(const std::string& path) const {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string PvfArchive::resolvePath(const std::string& baseDir, const std::string& path) const {
|
std::string PvfArchive::resolvePath(const std::string& baseDir,
|
||||||
|
const std::string& path) const {
|
||||||
if (path.empty()) {
|
if (path.empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -345,9 +503,11 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin
|
|||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
|
||||||
size_t slashPos = rawPath.find('/');
|
size_t slashPos = rawPath.find('/');
|
||||||
std::string root = slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
|
std::string root =
|
||||||
static const char* kLogicalRoots[] = {"map", "sprite", "town", "sound", "audio",
|
slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos);
|
||||||
"monster", "character", "ui"};
|
static const char* kLogicalRoots[] = {"map", "sprite", "town",
|
||||||
|
"sound", "audio", "monster",
|
||||||
|
"character", "ui"};
|
||||||
for (const char* logicalRoot : kLogicalRoots) {
|
for (const char* logicalRoot : kLogicalRoots) {
|
||||||
if (root == logicalRoot) {
|
if (root == logicalRoot) {
|
||||||
return normalizePath(rawPath);
|
return normalizePath(rawPath);
|
||||||
@@ -361,7 +521,8 @@ std::string PvfArchive::resolvePath(const std::string& baseDir, const std::strin
|
|||||||
return normalizePath(normalizedBaseDir + rawPath);
|
return normalizePath(normalizedBaseDir + rawPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> PvfArchive::splitString(const std::string& str, const std::string& delimiter) const {
|
std::vector<std::string> PvfArchive::splitString(
|
||||||
|
const std::string& str, const std::string& delimiter) const {
|
||||||
std::vector<std::string> tokens;
|
std::vector<std::string> tokens;
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
size_t found;
|
size_t found;
|
||||||
@@ -378,16 +539,94 @@ std::vector<std::string> PvfArchive::splitString(const std::string& str, const s
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PvfArchive::decodeFile(PvfFileInfo& info) {
|
bool PvfArchive::decodeFile(const std::string& normalizedPath,
|
||||||
if (info.decoded) {
|
PvfFileInfo& info) {
|
||||||
|
auto cacheIt = decodedFileCache_.find(normalizedPath);
|
||||||
|
if (cacheIt != decodedFileCache_.end()) {
|
||||||
|
info.decoded = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
reader_.seek(dataStartPos_ + info.offset);
|
auto bytesOpt = readDecodedFileBytes(info);
|
||||||
reader_.crcDecode(info.length, info.crc32);
|
if (!bytesOpt.has_value()) {
|
||||||
info.decoded = true;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedFileCache_[normalizedPath] = std::move(bytesOpt.value());
|
||||||
|
info.decoded = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PvfArchive::clearInitData() {
|
||||||
|
dataStartPos_ = 0;
|
||||||
|
fileInfo_.clear();
|
||||||
|
binStringTable_.clear();
|
||||||
|
loadStrings_.clear();
|
||||||
|
decodedFileCache_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<uint8>> PvfArchive::readArchiveBytes(
|
||||||
|
size_t absoluteOffset, size_t length) {
|
||||||
|
if (!isOpen()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
return std::vector<uint8>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absoluteOffset > reader_.size() ||
|
||||||
|
length > reader_.size() - absoluteOffset) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: attempted to read beyond archive bounds (offset=%zu, size=%zu, archive=%zu)",
|
||||||
|
absoluteOffset, length, reader_.size());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(absoluteOffset);
|
||||||
|
std::vector<uint8> bytes = reader_.readBytes(length);
|
||||||
|
if (bytes.size() != length) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PvfArchive: short read at offset %zu (expected %zu bytes, got %zu)",
|
||||||
|
absoluteOffset, length, bytes.size());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<uint8>> PvfArchive::readDecodedFileBytes(
|
||||||
|
const PvfFileInfo& info) {
|
||||||
|
auto bytesOpt = readArchiveBytes(dataStartPos_ + info.offset, info.length);
|
||||||
|
if (!bytesOpt.has_value()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8> bytes = std::move(bytesOpt.value());
|
||||||
|
crcDecodeBuffer(bytes, info.crc32);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::crcDecodeBuffer(std::vector<uint8>& data, uint32 crc32) {
|
||||||
|
if (data.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32 key = 0x81A79011u;
|
||||||
|
for (size_t i = 0; i + 3 < data.size(); i += 4) {
|
||||||
|
uint32 value = static_cast<uint32>(data[i]) |
|
||||||
|
(static_cast<uint32>(data[i + 1]) << 8) |
|
||||||
|
(static_cast<uint32>(data[i + 2]) << 16) |
|
||||||
|
(static_cast<uint32>(data[i + 3]) << 24);
|
||||||
|
|
||||||
|
uint32 decoded = (value ^ key ^ crc32);
|
||||||
|
decoded = (decoded >> 6) | ((decoded << (32 - 6)) & 0xFFFFFFFFu);
|
||||||
|
|
||||||
|
data[i] = static_cast<uint8>((decoded >> 0) & 0xFFu);
|
||||||
|
data[i + 1] = static_cast<uint8>((decoded >> 8) & 0xFFu);
|
||||||
|
data[i + 2] = static_cast<uint8>((decoded >> 16) & 0xFFu);
|
||||||
|
data[i + 3] = static_cast<uint8>((decoded >> 24) & 0xFFu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace frostbite2D
|
} // namespace frostbite2D
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <frostbite2D/resource/sound_pack_archive.h>
|
#include <frostbite2D/resource/sound_pack_archive.h>
|
||||||
|
#include <frostbite2D/resource/binary_file_stream_reader.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
@@ -75,7 +76,7 @@ void SoundPackArchive::scanNpkFiles() {
|
|||||||
|
|
||||||
bool SoundPackArchive::parseNpkFile(const std::string& npkPath) {
|
bool SoundPackArchive::parseNpkFile(const std::string& npkPath) {
|
||||||
Asset& asset = Asset::get();
|
Asset& asset = Asset::get();
|
||||||
BinaryReader reader(npkPath);
|
BinaryFileStreamReader reader(npkPath);
|
||||||
|
|
||||||
if (!reader.isOpen()) {
|
if (!reader.isOpen()) {
|
||||||
SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str());
|
SDL_Log("Failed to open sound NPK file: %s", npkPath.c_str());
|
||||||
@@ -230,7 +231,7 @@ void SoundPackArchive::updateCacheUsage(const std::string& audioPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SoundPackArchive::readNpkInfoString(BinaryReader& reader) {
|
std::string SoundPackArchive::readNpkInfoString(BinaryFileStreamReader& reader) {
|
||||||
if (reader.eof()) {
|
if (reader.eof()) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ namespace frostbite2D {
|
|||||||
|
|
||||||
Scene* Scene::current_ = nullptr;
|
Scene* Scene::current_ = nullptr;
|
||||||
|
|
||||||
Scene::Scene() {
|
Scene::Scene() = default;
|
||||||
current_ = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Scene::~Scene() {
|
Scene::~Scene() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
#include <frostbite2D/scene/ui_scene.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
@@ -9,8 +10,6 @@ SceneManager& SceneManager::get() {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除析构函数中的自动销毁,改为在 Application::shutdown() 中手动调用
|
|
||||||
|
|
||||||
void SceneManager::PushScene(Ptr<Scene> scene) {
|
void SceneManager::PushScene(Ptr<Scene> scene) {
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
return;
|
return;
|
||||||
@@ -21,6 +20,7 @@ void SceneManager::PushScene(Ptr<Scene> scene) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sceneStack_.push_back(scene);
|
sceneStack_.push_back(scene);
|
||||||
|
Scene::current_ = scene.Get();
|
||||||
scene->onEnter();
|
scene->onEnter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ void SceneManager::PopScene() {
|
|||||||
sceneStack_.back()->onExit();
|
sceneStack_.back()->onExit();
|
||||||
sceneStack_.pop_back();
|
sceneStack_.pop_back();
|
||||||
|
|
||||||
|
Scene::current_ = sceneStack_.empty() ? nullptr : sceneStack_.back().Get();
|
||||||
if (!sceneStack_.empty()) {
|
if (!sceneStack_.empty()) {
|
||||||
sceneStack_.back()->onEnter();
|
sceneStack_.back()->onEnter();
|
||||||
}
|
}
|
||||||
@@ -42,15 +43,82 @@ void SceneManager::ReplaceScene(Ptr<Scene> scene) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PopScene();
|
if (!sceneStack_.empty()) {
|
||||||
PushScene(scene);
|
sceneStack_.back()->onExit();
|
||||||
|
sceneStack_.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneStack_.push_back(scene);
|
||||||
|
Scene::current_ = scene.Get();
|
||||||
|
scene->onEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::PushUIScene(Ptr<UIScene> scene) {
|
||||||
|
if (!scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSceneStack_.push_back(scene);
|
||||||
|
scene->onEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::PopUIScene() {
|
||||||
|
if (uiSceneStack_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSceneStack_.back()->onExit();
|
||||||
|
uiSceneStack_.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::ReplaceUIScene(Ptr<UIScene> scene) {
|
||||||
|
if (!scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uiSceneStack_.empty()) {
|
||||||
|
uiSceneStack_.back()->onExit();
|
||||||
|
uiSceneStack_.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
uiSceneStack_.push_back(scene);
|
||||||
|
scene->onEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SceneManager::RemoveUIScene(UIScene* scene) {
|
||||||
|
if (!scene) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = uiSceneStack_.begin(); it != uiSceneStack_.end(); ++it) {
|
||||||
|
if (it->Get() != scene) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*it)->onExit();
|
||||||
|
uiSceneStack_.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::ClearUIScenes() {
|
||||||
|
while (!uiSceneStack_.empty()) {
|
||||||
|
uiSceneStack_.back()->onExit();
|
||||||
|
uiSceneStack_.pop_back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneManager::ClearAll() {
|
void SceneManager::ClearAll() {
|
||||||
|
ClearUIScenes();
|
||||||
|
|
||||||
while (!sceneStack_.empty()) {
|
while (!sceneStack_.empty()) {
|
||||||
sceneStack_.back()->onExit();
|
sceneStack_.back()->onExit();
|
||||||
sceneStack_.pop_back();
|
sceneStack_.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scene::current_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneManager::Update(float deltaTime) {
|
void SceneManager::Update(float deltaTime) {
|
||||||
@@ -59,10 +127,53 @@ void SceneManager::Update(float deltaTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SceneManager::UpdateUI(float deltaTime) {
|
||||||
|
for (auto& scene : uiSceneStack_) {
|
||||||
|
if (scene) {
|
||||||
|
scene->Update(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SceneManager::Render() {
|
void SceneManager::Render() {
|
||||||
|
Renderer& renderer = Renderer::get();
|
||||||
|
Camera* worldCamera = renderer.getCamera();
|
||||||
|
|
||||||
if (!sceneStack_.empty()) {
|
if (!sceneStack_.empty()) {
|
||||||
sceneStack_.back()->Render();
|
sceneStack_.back()->Render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto& scene : uiSceneStack_) {
|
||||||
|
if (!scene) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.setCamera(scene->GetCamera());
|
||||||
|
scene->Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer.getCamera() != worldCamera) {
|
||||||
|
renderer.setCamera(worldCamera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SceneManager::DispatchEvent(const Event& event) {
|
||||||
|
if (DispatchUIEvent(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scene* currentScene = GetCurrentScene();
|
||||||
|
return currentScene ? currentScene->OnEvent(event) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SceneManager::DispatchUIEvent(const Event& event) {
|
||||||
|
for (auto it = uiSceneStack_.rbegin(); it != uiSceneStack_.rend(); ++it) {
|
||||||
|
if (*it && (*it)->OnEvent(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scene* SceneManager::GetCurrentScene() const {
|
Scene* SceneManager::GetCurrentScene() const {
|
||||||
@@ -72,9 +183,19 @@ Scene* SceneManager::GetCurrentScene() const {
|
|||||||
return sceneStack_.back().Get();
|
return sceneStack_.back().Get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UIScene* SceneManager::GetCurrentUIScene() const {
|
||||||
|
if (uiSceneStack_.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return uiSceneStack_.back().Get();
|
||||||
|
}
|
||||||
|
|
||||||
bool SceneManager::HasActiveScene() const {
|
bool SceneManager::HasActiveScene() const {
|
||||||
return !sceneStack_.empty();
|
return !sceneStack_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SceneManager::HasActiveUIScene() const {
|
||||||
|
return !uiSceneStack_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
|
|||||||
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 <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
@@ -44,6 +45,7 @@ private:
|
|||||||
Animation* GetPrimaryAnimation(const std::string& actionName) const;
|
Animation* GetPrimaryAnimation(const std::string& actionName) const;
|
||||||
Animation* GetCurrentPrimaryAnimation() const;
|
Animation* GetCurrentPrimaryAnimation() const;
|
||||||
void RefreshRuntimeCallbacks();
|
void RefreshRuntimeCallbacks();
|
||||||
|
bool shouldSkipMissingAnimation(const std::string& aniPath);
|
||||||
|
|
||||||
void CreateAnimationBySlot(const std::string& actionName,
|
void CreateAnimationBySlot(const std::string& actionName,
|
||||||
const std::string& slotName,
|
const std::string& slotName,
|
||||||
@@ -57,6 +59,8 @@ private:
|
|||||||
int direction_ = 1;
|
int direction_ = 1;
|
||||||
ActionFrameFlagCallback actionFrameFlagCallback_;
|
ActionFrameFlagCallback actionFrameFlagCallback_;
|
||||||
ActionEndCallback actionEndCallback_;
|
ActionEndCallback actionEndCallback_;
|
||||||
|
std::unordered_set<std::string> missingAnimationPaths_;
|
||||||
|
size_t skippedMissingAnimationCount_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace frostbite2D
|
} // namespace frostbite2D
|
||||||
|
|||||||
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<BackgroundAnimationConfig> backgroundAnimations;
|
||||||
std::vector<MapAnimationConfig> mapAnimations;
|
std::vector<MapAnimationConfig> mapAnimations;
|
||||||
std::vector<std::string> soundIds;
|
std::vector<std::string> soundIds;
|
||||||
std::vector<Rect> virtualMovableAreas;
|
std::vector<Vec2> virtualMovablePolygon;
|
||||||
std::vector<Rect> townMovableAreas;
|
std::vector<Rect> townMovableAreas;
|
||||||
std::vector<MoveAreaTarget> townMovableAreaTargets;
|
std::vector<MoveAreaTarget> townMovableAreaTargets;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public:
|
|||||||
Rect GetMovablePositionArea(size_t index) const;
|
Rect GetMovablePositionArea(size_t index) const;
|
||||||
|
|
||||||
int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
|
int GetBackgroundRepeatWidth() const { return backgroundRepeatWidth_; }
|
||||||
|
bool IsDebugModeEnabled() const { return debugMode_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
|
/// 初始化固定图层。图层名字与 DNF 地图层概念保持一致。
|
||||||
@@ -75,14 +76,14 @@ private:
|
|||||||
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
|
/// bottom 层里的专用地板容器,固定放在该层最底部,避免地图动画被地板压住。
|
||||||
RefPtr<Actor> tileRoot_;
|
RefPtr<Actor> tileRoot_;
|
||||||
/// 角色可行走矩形区域。
|
/// 角色可行走矩形区域。
|
||||||
std::vector<Rect> movableArea_;
|
std::vector<Vec2> movablePolygon_;
|
||||||
/// 进入后会触发切图/传送的矩形区域。
|
/// 进入后会触发切图/传送的矩形区域。
|
||||||
std::vector<Rect> moveArea_;
|
std::vector<Rect> moveArea_;
|
||||||
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
|
/// 地板铺设后推导出的横向覆盖宽度,用于背景动画横向平铺。
|
||||||
int backgroundRepeatWidth_ = 0;
|
int backgroundRepeatWidth_ = 0;
|
||||||
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
|
/// 地图配置里的整体 Y 偏移;既影响层位置,也影响地板校准。
|
||||||
int mapOffsetY_ = 0;
|
int mapOffsetY_ = 0;
|
||||||
bool debugMode_ = false;
|
bool debugMode_ = true;
|
||||||
/// 当前地图正在播放的背景音乐。
|
/// 当前地图正在播放的背景音乐。
|
||||||
Ptr<Music> currentMusic_;
|
Ptr<Music> currentMusic_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ class GameMapLayer : public Actor {
|
|||||||
public:
|
public:
|
||||||
void Render() override;
|
void Render() override;
|
||||||
|
|
||||||
void AddDebugFeasibleAreaInfo(const Rect& rect, int type);
|
void SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon);
|
||||||
|
void AddDebugMoveAreaInfo(const Rect& rect);
|
||||||
|
void ClearDebugAreaInfo();
|
||||||
void AddObject(RefPtr<Actor> obj);
|
void AddObject(RefPtr<Actor> obj);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Rect> feasibleAreaInfoList_;
|
std::vector<Vec2> feasibleAreaPolygon_;
|
||||||
|
std::vector<Rect> feasibleAreaFillRects_;
|
||||||
std::vector<Rect> moveAreaInfoList_;
|
std::vector<Rect> moveAreaInfoList_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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 "camera/GameCameraController.h"
|
||||||
#include "character/CharacterObject.h"
|
#include "character/CharacterObject.h"
|
||||||
#include "map/GameMap.h"
|
#include "map/GameMap.h"
|
||||||
|
#include "scene/GameDebugUIScene.h"
|
||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
@@ -19,6 +20,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
GameCameraController cameraController_;
|
GameCameraController cameraController_;
|
||||||
RefPtr<CharacterObject> character_;
|
RefPtr<CharacterObject> character_;
|
||||||
|
RefPtr<GameDebugUIScene> debugScene_;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
RefPtr<GameMap> map_;
|
RefPtr<GameMap> map_;
|
||||||
};
|
};
|
||||||
|
|||||||
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/resource/sound_pack_archive.h>
|
||||||
#include <frostbite2D/scene/scene.h>
|
#include <frostbite2D/scene/scene.h>
|
||||||
#include <frostbite2D/scene/scene_manager.h>
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include "scene/GameMapTestScene.h"
|
#include "scene/GameMapTestScene.h"
|
||||||
#include "world/GameWorld.h"
|
#include "world/GameWorld.h"
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ int main(int argc, char **argv) {
|
|||||||
(void)argc;
|
(void)argc;
|
||||||
(void)argv;
|
(void)argv;
|
||||||
|
|
||||||
|
StartupTrace::reset("main entry");
|
||||||
|
StartupTrace::mark("main entry");
|
||||||
|
|
||||||
AppConfig config = AppConfig::createDefault();
|
AppConfig config = AppConfig::createDefault();
|
||||||
config.appName = "Frostbite2D Test App";
|
config.appName = "Frostbite2D Test App";
|
||||||
config.appVersion = "1.0.0";
|
config.appVersion = "1.0.0";
|
||||||
@@ -36,79 +40,117 @@ int main(int argc, char **argv) {
|
|||||||
config.windowConfig.title = "Frostbite2D - Async Init Demo";
|
config.windowConfig.title = "Frostbite2D - Async Init Demo";
|
||||||
|
|
||||||
Application &app = Application::get();
|
Application &app = Application::get();
|
||||||
if (!app.init(config)) {
|
{
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
ScopedStartupTrace startupTrace("Application::init");
|
||||||
"Failed to initialize application!");
|
if (!app.init(config)) {
|
||||||
return -1;
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to initialize application!");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
StartupTrace::mark("Application::init complete");
|
||||||
|
|
||||||
app.run([]() {
|
app.run([]() {
|
||||||
auto &fontManager = FontManager::get();
|
ScopedStartupTrace startupTrace("Application::run startup callback");
|
||||||
fontManager.init();
|
|
||||||
fontManager.registerFont("default", "assets/Fonts/VonwaonBitmap-12px.ttf",
|
|
||||||
12);
|
|
||||||
|
|
||||||
auto &audioSystem = AudioSystem::get();
|
{
|
||||||
audioSystem.init();
|
ScopedStartupTrace stageTrace("FontManager startup");
|
||||||
audioSystem.setMasterVolume(1.0f);
|
auto &fontManager = FontManager::get();
|
||||||
audioSystem.setSoundVolume(0.8f);
|
fontManager.init();
|
||||||
audioSystem.setMusicVolume(0.6f);
|
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("游戏启动");
|
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");
|
auto Background = Sprite::createFromFile("assets/ImagePacks2/Loading0.jpg");
|
||||||
Background->SetSize(1280, 720);
|
Background->SetSize(1280, 720);
|
||||||
LoadingScene->AddChild(Background);
|
LoadingScene->AddChild(Background);
|
||||||
|
|
||||||
auto BackgroundBar =
|
auto BackgroundBar =
|
||||||
Sprite::createFromFile("assets/ImagePacks2/Loading1.png");
|
Sprite::createFromFile("assets/ImagePacks2/Loading1.png");
|
||||||
BackgroundBar->SetPosition(0, 686);
|
BackgroundBar->SetPosition(0, 686);
|
||||||
LoadingScene->AddChild(BackgroundBar);
|
LoadingScene->AddChild(BackgroundBar);
|
||||||
|
|
||||||
auto LoadCircleSp =
|
auto LoadCircleSp =
|
||||||
Sprite::createFromFile("assets/ImagePacks2/Loading2.png");
|
Sprite::createFromFile("assets/ImagePacks2/Loading2.png");
|
||||||
LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f));
|
LoadCircleSp->SetAnchor(Vec2(0.5f, 0.5f));
|
||||||
LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60);
|
LoadCircleSp->SetPosition(1280 / 2.0f, 686 - 60);
|
||||||
LoadCircleSp->SetBlendMode(BlendMode::Additive);
|
LoadCircleSp->SetBlendMode(BlendMode::Additive);
|
||||||
LoadCircleSp->AddUpdateListener([](Actor &self, float dt) {
|
LoadCircleSp->AddUpdateListener([](Actor &self, float dt) {
|
||||||
auto rotation = self.GetRotation();
|
auto rotation = self.GetRotation();
|
||||||
self.SetRotation(rotation + 180.0f * dt);
|
self.SetRotation(rotation + 180.0f * dt);
|
||||||
});
|
});
|
||||||
LoadingScene->AddChild(LoadCircleSp);
|
LoadingScene->AddChild(LoadCircleSp);
|
||||||
|
}
|
||||||
|
|
||||||
|
StartupTrace::mark("loading scene ready");
|
||||||
|
|
||||||
TaskSystem::get().submitThen(
|
TaskSystem::get().submitThen(
|
||||||
[]() -> std::string {
|
[]() -> std::string {
|
||||||
|
ScopedStartupTrace startupTrace("Async resource bootstrap");
|
||||||
SDL_Log("Async init task on main thread: %s",
|
SDL_Log("Async init task on main thread: %s",
|
||||||
TaskSystem::get().isMainThread() ? "true" : "false");
|
TaskSystem::get().isMainThread() ? "true" : "false");
|
||||||
|
|
||||||
auto &pvf = PvfArchive::get();
|
{
|
||||||
if (!pvf.open("assets/Script.pvf")) {
|
ScopedStartupTrace stageTrace("PvfArchive open+init");
|
||||||
throw std::runtime_error("Failed to open assets/Script.pvf");
|
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");
|
ScopedStartupTrace stageTrace("NpkArchive init");
|
||||||
npk.setDefaultImg("sprite/interface/base.img", 0);
|
auto &npk = NpkArchive::get();
|
||||||
npk.init();
|
npk.setImagePackDirectory("assets/ImagePacks2");
|
||||||
|
npk.setDefaultImg("sprite/interface/base.img", 0);
|
||||||
|
npk.init();
|
||||||
|
}
|
||||||
|
|
||||||
auto &archive = SoundPackArchive::get();
|
{
|
||||||
archive.setSoundPackDirectory("assets/SoundPacks");
|
ScopedStartupTrace stageTrace("SoundPackArchive init");
|
||||||
archive.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 "后台资源加载成功";
|
return "后台资源加载成功";
|
||||||
},
|
},
|
||||||
[](std::string message) mutable {
|
[](std::string message) mutable {
|
||||||
SDL_Log("后台资源加载成功");
|
ScopedStartupTrace startupTrace("Async completion main-thread scene switch");
|
||||||
|
SDL_Log("%s", message.c_str());
|
||||||
|
StartupTrace::mark("before SceneManager::ReplaceScene");
|
||||||
|
|
||||||
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) {
|
[](std::exception_ptr error) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "character/CharacterAnimation.h"
|
#include "character/CharacterAnimation.h"
|
||||||
#include "character/CharacterObject.h"
|
#include "character/CharacterObject.h"
|
||||||
|
#include <frostbite2D/resource/pvf_archive.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -38,6 +39,8 @@ bool CharacterAnimation::Init(CharacterObject* parent,
|
|||||||
const CharacterEquipmentManager& equipmentManager) {
|
const CharacterEquipmentManager& equipmentManager) {
|
||||||
parent_ = parent;
|
parent_ = parent;
|
||||||
actionAnimations_.clear();
|
actionAnimations_.clear();
|
||||||
|
missingAnimationPaths_.clear();
|
||||||
|
skippedMissingAnimationCount_ = 0;
|
||||||
currentActionTag_.clear();
|
currentActionTag_.clear();
|
||||||
direction_ = 1;
|
direction_ = 1;
|
||||||
actionFrameFlagCallback_ = nullptr;
|
actionFrameFlagCallback_ = nullptr;
|
||||||
@@ -56,6 +59,12 @@ bool CharacterAnimation::Init(CharacterObject* parent,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skippedMissingAnimationCount_ > 0) {
|
||||||
|
SDL_Log("CharacterAnimation: skipped %zu missing animation loads across %zu unique paths for job %d",
|
||||||
|
skippedMissingAnimationCount_, missingAnimationPaths_.size(),
|
||||||
|
config.jobId);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +86,10 @@ void CharacterAnimation::CreateAnimationBySlot(
|
|||||||
const character::CharacterConfig& config,
|
const character::CharacterConfig& config,
|
||||||
const CharacterEquipmentManager& equipmentManager) {
|
const CharacterEquipmentManager& equipmentManager) {
|
||||||
if (slotName == std::string("skin_avatar")) {
|
if (slotName == std::string("skin_avatar")) {
|
||||||
|
if (shouldSkipMissingAnimation(actionPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Animation::ReplaceData replaceData(0, 0);
|
Animation::ReplaceData replaceData(0, 0);
|
||||||
if (const auto* equip = equipmentManager.GetEquip(slotName)) {
|
if (const auto* equip = equipmentManager.GetEquip(slotName)) {
|
||||||
auto it = equip->jobAnimations.find(config.jobId);
|
auto it = equip->jobAnimations.find(config.jobId);
|
||||||
@@ -114,6 +127,10 @@ void CharacterAnimation::CreateAnimationBySlot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail;
|
std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail;
|
||||||
|
if (shouldSkipMissingAnimation(aniPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
auto animation = MakePtr<Animation>(
|
auto animation = MakePtr<Animation>(
|
||||||
aniPath, FormatImgPath,
|
aniPath, FormatImgPath,
|
||||||
Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1]));
|
Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1]));
|
||||||
@@ -128,6 +145,23 @@ void CharacterAnimation::CreateAnimationBySlot(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CharacterAnimation::shouldSkipMissingAnimation(const std::string& aniPath) {
|
||||||
|
PvfArchive& pvf = PvfArchive::get();
|
||||||
|
std::string normalizedPath = pvf.normalizePath(aniPath);
|
||||||
|
if (missingAnimationPaths_.find(normalizedPath) != missingAnimationPaths_.end()) {
|
||||||
|
++skippedMissingAnimationCount_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pvf.hasFile(normalizedPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
missingAnimationPaths_.insert(std::move(normalizedPath));
|
||||||
|
++skippedMissingAnimationCount_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
std::string CharacterAnimation::DescribeAvailableActions() const {
|
std::string CharacterAnimation::DescribeAvailableActions() const {
|
||||||
if (actionAnimations_.empty()) {
|
if (actionAnimations_.empty()) {
|
||||||
return "<none>";
|
return "<none>";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <frostbite2D/core/application.h>
|
#include <frostbite2D/core/application.h>
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ int32 RoundWorldCoordinate(float value) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool CharacterObject::Construction(int jobId) {
|
bool CharacterObject::Construction(int jobId) {
|
||||||
|
ScopedStartupTrace startupTrace("CharacterObject::Construction");
|
||||||
// Reset all runtime state before rebuilding the character from config.
|
// Reset all runtime state before rebuilding the character from config.
|
||||||
EnableEventReceive();
|
EnableEventReceive();
|
||||||
RemoveAllChildren();
|
RemoveAllChildren();
|
||||||
@@ -67,7 +69,10 @@ bool CharacterObject::Construction(int jobId) {
|
|||||||
lastDeltaTime_ = 0.0f;
|
lastDeltaTime_ = 0.0f;
|
||||||
inputEnabled_ = true;
|
inputEnabled_ = true;
|
||||||
|
|
||||||
auto config = character::loadCharacterConfig(jobId);
|
auto config = [&]() {
|
||||||
|
ScopedStartupTrace stageTrace("character::loadCharacterConfig");
|
||||||
|
return character::loadCharacterConfig(jobId);
|
||||||
|
}();
|
||||||
if (!config) {
|
if (!config) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"CharacterObject: failed to load job %d config", jobId);
|
"CharacterObject: failed to load job %d config", jobId);
|
||||||
@@ -79,24 +84,36 @@ bool CharacterObject::Construction(int jobId) {
|
|||||||
direction_ = 1;
|
direction_ = 1;
|
||||||
config_ = *config;
|
config_ = *config;
|
||||||
stateMachine_.Configure(*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;
|
actionLibrary_ = *actionLibrary;
|
||||||
}
|
}
|
||||||
|
|
||||||
animationManager_ = MakePtr<CharacterAnimation>();
|
animationManager_ = MakePtr<CharacterAnimation>();
|
||||||
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
|
{
|
||||||
ReportFatalCharacterError("CharacterObject::Construction",
|
ScopedStartupTrace stageTrace("CharacterAnimation::Init");
|
||||||
"no usable animation tags were loaded");
|
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
|
||||||
return false;
|
ReportFatalCharacterError("CharacterObject::Construction",
|
||||||
|
"no usable animation tags were loaded");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AddChild(animationManager_);
|
AddChild(animationManager_);
|
||||||
|
|
||||||
if (!RequireAction("idle", "CharacterObject::Construction")) {
|
{
|
||||||
return false;
|
ScopedStartupTrace stageTrace("CharacterObject initial action setup");
|
||||||
}
|
if (!RequireAction("idle", "CharacterObject::Construction")) {
|
||||||
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetWorldPosition({});
|
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);
|
outConfig.townMovableAreaTargets.push_back(target);
|
||||||
}
|
}
|
||||||
} else if (segment == "[virtual movable area]") {
|
} else if (segment == "[virtual movable area]") {
|
||||||
|
std::vector<int> polygonCoords;
|
||||||
while (!stream.isEnd()) {
|
while (!stream.isEnd()) {
|
||||||
std::string token = stream.get();
|
std::string token = stream.get();
|
||||||
if (token == "[/virtual movable area]") {
|
if (token == "[/virtual movable area]") {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Rect rect(static_cast<float>(toInt(token)),
|
polygonCoords.push_back(toInt(token));
|
||||||
static_cast<float>(toInt(stream.get())),
|
}
|
||||||
static_cast<float>(toInt(stream.get())),
|
|
||||||
static_cast<float>(toInt(stream.get())));
|
if (polygonCoords.empty()) {
|
||||||
outConfig.virtualMovableAreas.push_back(rect);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygonCoords.size() % 2 != 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GameDataLoader: map %s has odd [virtual movable area] coordinate count (%zu)",
|
||||||
|
outConfig.mapPath.c_str(), polygonCoords.size());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygonCoords.size() < 6) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GameDataLoader: map %s [virtual movable area] needs at least 3 vertices, got %zu",
|
||||||
|
outConfig.mapPath.c_str(), polygonCoords.size() / 2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
outConfig.virtualMovablePolygon.clear();
|
||||||
|
outConfig.virtualMovablePolygon.reserve(polygonCoords.size() / 2);
|
||||||
|
for (size_t i = 0; i + 1 < polygonCoords.size(); i += 2) {
|
||||||
|
outConfig.virtualMovablePolygon.emplace_back(
|
||||||
|
static_cast<float>(polygonCoords[i]),
|
||||||
|
static_cast<float>(polygonCoords[i + 1]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <frostbite2D/core/application.h>
|
#include <frostbite2D/core/application.h>
|
||||||
#include <frostbite2D/graphics/renderer.h>
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
#include <frostbite2D/resource/audio_database.h>
|
#include <frostbite2D/resource/audio_database.h>
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ static const int kLayerOrders[] = {
|
|||||||
|
|
||||||
constexpr int kTileRootZOrder = -1000000;
|
constexpr int kTileRootZOrder = -1000000;
|
||||||
constexpr float kExtendedTileStepY = 120.0f;
|
constexpr float kExtendedTileStepY = 120.0f;
|
||||||
|
constexpr double kPolygonEpsilon = 0.001;
|
||||||
|
|
||||||
int RoundWorldCoordinate(float value) {
|
int RoundWorldCoordinate(float value) {
|
||||||
return static_cast<int>(std::lround(value));
|
return static_cast<int>(std::lround(value));
|
||||||
@@ -37,6 +39,57 @@ Vec3 MakeIntegerWorldPosition(int x, int y, int z) {
|
|||||||
static_cast<float>(z));
|
static_cast<float>(z));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) {
|
||||||
|
Vec2 segment = end - start;
|
||||||
|
Vec2 toPoint = point - start;
|
||||||
|
double cross = static_cast<double>(segment.cross(toPoint));
|
||||||
|
if (std::abs(cross) > kPolygonEpsilon) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double minX = std::min(start.x, end.x) - kPolygonEpsilon;
|
||||||
|
double maxX = std::max(start.x, end.x) + kPolygonEpsilon;
|
||||||
|
double minY = std::min(start.y, end.y) - kPolygonEpsilon;
|
||||||
|
double maxY = std::max(start.y, end.y) + kPolygonEpsilon;
|
||||||
|
return point.x >= minX && point.x <= maxX && point.y >= minY &&
|
||||||
|
point.y <= maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPointInPolygon(const std::vector<Vec2>& polygon, const Vec2& point) {
|
||||||
|
if (polygon.size() < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inside = false;
|
||||||
|
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
|
||||||
|
const Vec2& start = polygon[j];
|
||||||
|
const Vec2& end = polygon[i];
|
||||||
|
if (IsPointOnSegment(point, start, end)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool crossesScanline = (start.y > point.y) != (end.y > point.y);
|
||||||
|
if (!crossesScanline) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
double intersectX =
|
||||||
|
static_cast<double>(start.x) +
|
||||||
|
(static_cast<double>(point.y) - static_cast<double>(start.y)) *
|
||||||
|
(static_cast<double>(end.x) - static_cast<double>(start.x)) /
|
||||||
|
(static_cast<double>(end.y) - static_cast<double>(start.y));
|
||||||
|
if (intersectX >= static_cast<double>(point.x) - kPolygonEpsilon) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPointMovable(const std::vector<Vec2>& polygon, int x, int y) {
|
||||||
|
return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
|
// 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite
|
||||||
// 占位,避免整张地图中断。
|
// 占位,避免整张地图中断。
|
||||||
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
||||||
@@ -70,33 +123,49 @@ void GameMap::clearLayerChildren() {
|
|||||||
for (auto& [_, layer] : layerMap_) {
|
for (auto& [_, layer] : layerMap_) {
|
||||||
if (layer) {
|
if (layer) {
|
||||||
layer->RemoveAllChildren();
|
layer->RemoveAllChildren();
|
||||||
|
layer->ClearDebugAreaInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tileRoot_.Reset();
|
tileRoot_.Reset();
|
||||||
movableArea_.clear();
|
movablePolygon_.clear();
|
||||||
moveArea_.clear();
|
moveArea_.clear();
|
||||||
currentMusic_.Reset();
|
currentMusic_.Reset();
|
||||||
backgroundRepeatWidth_ = 0;
|
backgroundRepeatWidth_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameMap::LoadMap(const std::string &mapName) {
|
bool GameMap::LoadMap(const std::string &mapName) {
|
||||||
// 清空所有图层子节点。
|
ScopedStartupTrace startupTrace("GameMap::LoadMap");
|
||||||
clearLayerChildren();
|
clearLayerChildren();
|
||||||
// 读取PVF地图配置。
|
{
|
||||||
if (!game::loadMapConfig(mapName, mapConfig_)) {
|
ScopedStartupTrace stageTrace("game::loadMapConfig");
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s",
|
if (!game::loadMapConfig(mapName, mapConfig_)) {
|
||||||
mapName.c_str());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
return false;
|
"GameMap: failed to load map %s", mapName.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// backgroundPos 会影响地图整体的视觉基线,所以地板和各显示层都会参考它。
|
|
||||||
mapOffsetY_ = mapConfig_.backgroundPos;
|
mapOffsetY_ = mapConfig_.backgroundPos;
|
||||||
// 加载顺序基本就是地图的组装顺序:先地板,再背景,再对象,再可行走数据。
|
{
|
||||||
InitTile();
|
ScopedStartupTrace stageTrace("GameMap::InitTile");
|
||||||
InitBackgroundAnimation();
|
InitTile();
|
||||||
InitMapAnimation();
|
}
|
||||||
InitVirtualMovableArea();
|
{
|
||||||
InitMoveArea();
|
ScopedStartupTrace stageTrace("GameMap::InitBackgroundAnimation");
|
||||||
|
InitBackgroundAnimation();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("GameMap::InitMapAnimation");
|
||||||
|
InitMapAnimation();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("GameMap::InitVirtualMovableArea");
|
||||||
|
InitVirtualMovableArea();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ScopedStartupTrace stageTrace("GameMap::InitMoveArea");
|
||||||
|
InitMoveArea();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -248,26 +317,48 @@ void GameMap::InitMapAnimation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameMap::InitVirtualMovableArea() {
|
void GameMap::InitVirtualMovableArea() {
|
||||||
movableArea_ = mapConfig_.virtualMovableAreas;
|
movablePolygon_ = mapConfig_.virtualMovablePolygon;
|
||||||
// debugMode 打开时,把可行走区域可视化到最高层,方便校验地图配置。
|
if (movablePolygon_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float minX = movablePolygon_.front().x;
|
||||||
|
float minY = movablePolygon_.front().y;
|
||||||
|
float maxX = movablePolygon_.front().x;
|
||||||
|
float maxY = movablePolygon_.front().y;
|
||||||
|
for (const auto& point : movablePolygon_) {
|
||||||
|
minX = std::min(minX, point.x);
|
||||||
|
minY = std::min(minY, point.y);
|
||||||
|
maxX = std::max(maxX, point.x);
|
||||||
|
maxY = std::max(maxY, point.y);
|
||||||
|
}
|
||||||
|
SDL_Log("GameMap: movable polygon vertices=%zu bounds=(%.0f, %.0f)-(%.0f, %.0f)",
|
||||||
|
movablePolygon_.size(), minX, minY, maxX, maxY);
|
||||||
|
|
||||||
if (!debugMode_) {
|
if (!debugMode_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& rect : movableArea_) {
|
auto layerIt = layerMap_.find("max");
|
||||||
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0);
|
if (layerIt == layerMap_.end() || !layerIt->second) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
layerIt->second->SetDebugFeasibleAreaPolygon(movablePolygon_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMap::InitMoveArea() {
|
void GameMap::InitMoveArea() {
|
||||||
moveArea_ = mapConfig_.townMovableAreas;
|
moveArea_ = mapConfig_.townMovableAreas;
|
||||||
// move area 和普通 movable area 分开展示,方便区分“能走”与“会触发切图”。
|
|
||||||
if (!debugMode_) {
|
if (!debugMode_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto layerIt = layerMap_.find("max");
|
||||||
|
if (layerIt == layerMap_.end() || !layerIt->second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& rect : moveArea_) {
|
for (const auto& rect : moveArea_) {
|
||||||
layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1);
|
layerIt->second->AddDebugMoveAreaInfo(rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +448,7 @@ void GameMap::AddObject(RefPtr<Actor> object) {
|
|||||||
if (!object) {
|
if (!object) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 动态对象默认进 normal 层,并沿用 2D 地图里常见的“按 y 值排序”规则。
|
// Keep dynamic objects on the normal layer and sort them by y.
|
||||||
object->SetZOrder(static_cast<int>(object->GetPosition().y));
|
object->SetZOrder(static_cast<int>(object->GetPosition().y));
|
||||||
layerMap_["normal"]->AddObject(object);
|
layerMap_["normal"]->AddObject(object);
|
||||||
}
|
}
|
||||||
@@ -365,42 +456,41 @@ void GameMap::AddObject(RefPtr<Actor> object) {
|
|||||||
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
|
Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const {
|
||||||
int currentX = RoundWorldCoordinate(curPos.x);
|
int currentX = RoundWorldCoordinate(curPos.x);
|
||||||
int currentY = RoundWorldCoordinate(curPos.y);
|
int currentY = RoundWorldCoordinate(curPos.y);
|
||||||
int currentZ = RoundWorldCoordinate(curPos.z);
|
|
||||||
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, currentZ);
|
|
||||||
if (movableArea_.empty()) {
|
|
||||||
return MakeIntegerWorldPosition(RoundWorldCoordinate(curPos.x + posOffset.x),
|
|
||||||
RoundWorldCoordinate(curPos.y + posOffset.y),
|
|
||||||
RoundWorldCoordinate(curPos.z + posOffset.z));
|
|
||||||
}
|
|
||||||
|
|
||||||
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
|
int targetX = RoundWorldCoordinate(curPos.x + posOffset.x);
|
||||||
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
|
int targetY = RoundWorldCoordinate(curPos.y + posOffset.y);
|
||||||
|
int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z);
|
||||||
// X/Y
|
Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ);
|
||||||
// Check X and Y separately so edge sliding does not lock both axes.
|
if (movablePolygon_.size() < 3) {
|
||||||
bool isXValid = false;
|
return MakeIntegerWorldPosition(targetX, targetY, targetZ);
|
||||||
for (const auto& area : movableArea_) {
|
}
|
||||||
if (area.containsPoint(MakeIntegerWorldPoint(targetX, currentY))) {
|
// Prefer the full destination first; only fall back to single-axis sliding
|
||||||
isXValid = true;
|
// when the combined move would leave the movable polygon.
|
||||||
break;
|
if (IsPointMovable(movablePolygon_, targetX, targetY)) {
|
||||||
}
|
result.x = static_cast<float>(targetX);
|
||||||
|
result.y = static_cast<float>(targetY);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isYValid = false;
|
bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY);
|
||||||
for (const auto& area : movableArea_) {
|
bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY);
|
||||||
if (area.containsPoint(MakeIntegerWorldPoint(currentX, targetY))) {
|
|
||||||
isYValid = true;
|
if (isXValid && isYValid) {
|
||||||
break;
|
int moveX = std::abs(targetX - currentX);
|
||||||
|
int moveY = std::abs(targetY - currentY);
|
||||||
|
if (moveX >= moveY) {
|
||||||
|
result.x = static_cast<float>(targetX);
|
||||||
|
} else {
|
||||||
|
result.y = static_cast<float>(targetY);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isXValid) {
|
if (isXValid) {
|
||||||
result.x = static_cast<float>(targetX);
|
result.x = static_cast<float>(targetX);
|
||||||
}
|
} else if (isYValid) {
|
||||||
if (isYValid) {
|
|
||||||
result.y = static_cast<float>(targetY);
|
result.y = static_cast<float>(targetY);
|
||||||
}
|
}
|
||||||
result.z = static_cast<float>(RoundWorldCoordinate(curPos.z + posOffset.z));
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,155 @@
|
|||||||
#include "map/GameMapLayer.h"
|
#include "map/GameMapLayer.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <frostbite2D/graphics/renderer.h>
|
#include <frostbite2D/graphics/renderer.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kDebugAreaAlpha = 0.45f;
|
||||||
|
constexpr float kDebugOutlineAlpha = 0.85f;
|
||||||
|
constexpr float kDebugEdgePointSize = 5.0f;
|
||||||
|
constexpr float kDebugVertexSize = 9.0f;
|
||||||
|
constexpr float kDebugEdgeStep = 4.0f;
|
||||||
|
|
||||||
|
std::vector<Rect> BuildPolygonFillRects(const std::vector<Vec2>& polygon) {
|
||||||
|
std::vector<Rect> fillRects;
|
||||||
|
if (polygon.size() < 3) {
|
||||||
|
return fillRects;
|
||||||
|
}
|
||||||
|
|
||||||
|
float minY = polygon.front().y;
|
||||||
|
float maxY = polygon.front().y;
|
||||||
|
for (const auto& point : polygon) {
|
||||||
|
minY = std::min(minY, point.y);
|
||||||
|
maxY = std::max(maxY, point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
int scanlineMinY = static_cast<int>(std::floor(minY));
|
||||||
|
int scanlineMaxY = static_cast<int>(std::ceil(maxY)) - 1;
|
||||||
|
if (scanlineMaxY < scanlineMinY) {
|
||||||
|
return fillRects;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = scanlineMinY; y <= scanlineMaxY; ++y) {
|
||||||
|
float scanY = static_cast<float>(y) + 0.5f;
|
||||||
|
std::vector<float> intersections;
|
||||||
|
intersections.reserve(polygon.size());
|
||||||
|
|
||||||
|
for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) {
|
||||||
|
const Vec2& start = polygon[j];
|
||||||
|
const Vec2& end = polygon[i];
|
||||||
|
if (start.y == end.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float edgeMinY = std::min(start.y, end.y);
|
||||||
|
float edgeMaxY = std::max(start.y, end.y);
|
||||||
|
if (scanY < edgeMinY || scanY >= edgeMaxY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float t = (scanY - start.y) / (end.y - start.y);
|
||||||
|
intersections.push_back(start.x + (end.x - start.x) * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersections.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(intersections.begin(), intersections.end());
|
||||||
|
for (size_t i = 0; i + 1 < intersections.size(); i += 2) {
|
||||||
|
float left = std::floor(intersections[i]);
|
||||||
|
float right = std::ceil(intersections[i + 1]);
|
||||||
|
float width = right - left;
|
||||||
|
if (width > 0.0f) {
|
||||||
|
fillRects.emplace_back(left, static_cast<float>(y), width, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fillRects;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawPolygonOutline(const std::vector<Vec2>& polygon, const Vec2& worldOrigin,
|
||||||
|
const Color& color) {
|
||||||
|
if (polygon.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& renderer = Renderer::get();
|
||||||
|
for (size_t i = 0; i < polygon.size(); ++i) {
|
||||||
|
Vec2 start = worldOrigin + polygon[i];
|
||||||
|
Vec2 end = worldOrigin + polygon[(i + 1) % polygon.size()];
|
||||||
|
Vec2 delta = end - start;
|
||||||
|
float length = delta.length();
|
||||||
|
int steps = std::max(1, static_cast<int>(std::ceil(length / kDebugEdgeStep)));
|
||||||
|
|
||||||
|
for (int step = 0; step <= steps; ++step) {
|
||||||
|
float t = static_cast<float>(step) / static_cast<float>(steps);
|
||||||
|
Vec2 point = start + delta * t;
|
||||||
|
renderer.drawQuad(
|
||||||
|
Rect(point.x - kDebugEdgePointSize * 0.5f,
|
||||||
|
point.y - kDebugEdgePointSize * 0.5f, kDebugEdgePointSize,
|
||||||
|
kDebugEdgePointSize),
|
||||||
|
color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawPolygonVertices(const std::vector<Vec2>& polygon,
|
||||||
|
const Vec2& worldOrigin, const Color& color) {
|
||||||
|
auto& renderer = Renderer::get();
|
||||||
|
for (const auto& vertex : polygon) {
|
||||||
|
Vec2 point = worldOrigin + vertex;
|
||||||
|
renderer.drawQuad(
|
||||||
|
Rect(point.x - kDebugVertexSize * 0.5f,
|
||||||
|
point.y - kDebugVertexSize * 0.5f, kDebugVertexSize,
|
||||||
|
kDebugVertexSize),
|
||||||
|
color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void GameMapLayer::Render() {
|
void GameMapLayer::Render() {
|
||||||
Actor::Render();
|
Actor::Render();
|
||||||
|
|
||||||
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero());
|
Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero());
|
||||||
|
|
||||||
for (const auto& rect : feasibleAreaInfoList_) {
|
for (const auto& rect : feasibleAreaFillRects_) {
|
||||||
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
||||||
rect.width(), rect.height());
|
rect.width(), rect.height());
|
||||||
Renderer::get().drawQuad(drawRect, Color(1.0f, 0.0f, 0.0f, 0.35f));
|
Renderer::get().drawQuad(drawRect, Color(0.0f, 1.0f, 0.0f, kDebugAreaAlpha));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& rect : moveAreaInfoList_) {
|
for (const auto& rect : moveAreaInfoList_) {
|
||||||
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y,
|
||||||
rect.width(), rect.height());
|
rect.width(), rect.height());
|
||||||
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, 0.35f));
|
Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, kDebugAreaAlpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!feasibleAreaPolygon_.empty()) {
|
||||||
|
Color outlineColor(0.0f, 1.0f, 0.0f, kDebugOutlineAlpha);
|
||||||
|
DrawPolygonOutline(feasibleAreaPolygon_, worldOrigin, outlineColor);
|
||||||
|
DrawPolygonVertices(feasibleAreaPolygon_, worldOrigin, outlineColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMapLayer::AddDebugFeasibleAreaInfo(const Rect& rect, int type) {
|
void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector<Vec2>& polygon) {
|
||||||
if (type == 0) {
|
feasibleAreaPolygon_ = polygon;
|
||||||
feasibleAreaInfoList_.push_back(rect);
|
feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_);
|
||||||
} else if (type == 1) {
|
}
|
||||||
moveAreaInfoList_.push_back(rect);
|
|
||||||
}
|
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) {
|
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 "scene/GameMapTestScene.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
#include <frostbite2D/scene/scene_manager.h>
|
||||||
|
#include <frostbite2D/utils/startup_trace.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
@@ -12,31 +14,48 @@ constexpr char kTestMapPath[] = "map/elvengard/elvengard.map";
|
|||||||
GameMapTestScene::GameMapTestScene() = default;
|
GameMapTestScene::GameMapTestScene() = default;
|
||||||
|
|
||||||
void GameMapTestScene::onEnter() {
|
void GameMapTestScene::onEnter() {
|
||||||
|
ScopedStartupTrace startupTrace("GameMapTestScene::onEnter");
|
||||||
Scene::onEnter();
|
Scene::onEnter();
|
||||||
|
|
||||||
|
if (!debugScene_) {
|
||||||
|
debugScene_ = MakePtr<GameDebugUIScene>();
|
||||||
|
}
|
||||||
|
SceneManager::get().RemoveUIScene(debugScene_.Get());
|
||||||
|
SceneManager::get().PushUIScene(debugScene_);
|
||||||
|
|
||||||
if (initialized_) {
|
if (initialized_) {
|
||||||
|
debugScene_->SetDebugContext(map_.Get(), character_.Get());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
map_ = MakePtr<GameMap>();
|
{
|
||||||
if (!map_->LoadMap(kTestMapPath)) {
|
ScopedStartupTrace stageTrace("GameMapTestScene map load");
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
map_ = MakePtr<GameMap>();
|
||||||
"GameMapTestScene: failed to load map %s", kTestMapPath);
|
if (!map_->LoadMap(kTestMapPath)) {
|
||||||
map_.Reset();
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
return;
|
"GameMapTestScene: failed to load map %s", kTestMapPath);
|
||||||
|
map_.Reset();
|
||||||
|
debugScene_->ClearDebugContext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AddChild(map_);
|
AddChild(map_);
|
||||||
|
|
||||||
character_ = MakePtr<CharacterObject>();
|
{
|
||||||
if (!character_->Construction(0)) {
|
ScopedStartupTrace stageTrace("GameMapTestScene character construction");
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
character_ = MakePtr<CharacterObject>();
|
||||||
"GameMapTestScene: failed to construct default character");
|
if (!character_->Construction(0)) {
|
||||||
character_.Reset();
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
} else {
|
"GameMapTestScene: failed to construct default character");
|
||||||
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
|
character_.Reset();
|
||||||
character_->SetCharacterPosition(spawnPos);
|
} else {
|
||||||
character_->EnableEventReceive();
|
Vec2 spawnPos =
|
||||||
character_->SetEventPriority(-100);
|
map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
|
||||||
map_->AddObject(character_);
|
character_->SetCharacterPosition(spawnPos);
|
||||||
|
character_->EnableEventReceive();
|
||||||
|
character_->SetEventPriority(-100);
|
||||||
|
map_->AddObject(character_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cameraController_.SetMap(map_.Get());
|
cameraController_.SetMap(map_.Get());
|
||||||
@@ -49,11 +68,20 @@ void GameMapTestScene::onEnter() {
|
|||||||
cameraController_.ClearTarget();
|
cameraController_.ClearTarget();
|
||||||
cameraController_.SnapToDefaultFocus();
|
cameraController_.SnapToDefaultFocus();
|
||||||
}
|
}
|
||||||
map_->Enter();
|
{
|
||||||
|
ScopedStartupTrace stageTrace("GameMap::Enter");
|
||||||
|
map_->Enter();
|
||||||
|
}
|
||||||
|
debugScene_->SetDebugContext(map_.Get(), character_.Get());
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameMapTestScene::onExit() {
|
void GameMapTestScene::onExit() {
|
||||||
|
if (debugScene_) {
|
||||||
|
debugScene_->ClearDebugContext();
|
||||||
|
SceneManager::get().RemoveUIScene(debugScene_.Get());
|
||||||
|
}
|
||||||
|
|
||||||
Scene::onExit();
|
Scene::onExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 平台)
|
-- 复制所有依赖的 DLL (Windows 平台)
|
||||||
if is_plat("mingw") or is_plat("windows") then
|
if is_plat("mingw") or is_plat("windows") then
|
||||||
-- 复制所有包的 DLL 文件
|
-- 复制所有包的 DLL 文件
|
||||||
local all_pkgs = {"libsdl2", "libsdl2_image", "libsdl2_mixer"}
|
local all_pkgs = {"libsdl2", "libsdl2_image", "libsdl2_mixer", "libsdl2_ttf"}
|
||||||
for _, pkg_name in ipairs(all_pkgs) do
|
for _, pkg_name in ipairs(all_pkgs) do
|
||||||
local pkg = target:pkg(pkg_name)
|
local pkg = target:pkg(pkg_name)
|
||||||
if pkg then
|
if pkg then
|
||||||
|
|||||||
Reference in New Issue
Block a user