feat(资源加载): 替换stb_image为SDL_image并添加脚本解析功能
- 将stb_image替换为SDL_image以解决Switch平台兼容性问题 - 添加PVF资源包解析器和脚本解析器功能 - 修改各平台配置文件添加SDL_image依赖 - 更新纹理加载逻辑使用SDL_image API - 新增脚本解析相关类用于处理游戏脚本数据
This commit is contained in:
46
.opencode/plans/clean_xmake_cache.md
Normal file
46
.opencode/plans/clean_xmake_cache.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 清理 Xmake 缓存并重新构建计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
- 已在 `sprite.cpp` 中添加 `printf("LoadA")` 调试代码
|
||||||
|
- 但运行时该调试语句未执行
|
||||||
|
- 说明 xmake 仍在使用旧的编译缓存
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 步骤 1: 清理 Xmake 缓存
|
||||||
|
运行以下命令清理所有编译缓存:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xmake clean -a
|
||||||
|
```
|
||||||
|
|
||||||
|
或者更彻底的清理(删除整个 build 目录):
|
||||||
|
```bash
|
||||||
|
xmake clean
|
||||||
|
rmdir /s /q build # Windows
|
||||||
|
# 或者
|
||||||
|
rm -rf build # Linux/Mac
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2: 重新配置项目(可选)
|
||||||
|
为了确保配置也是最新的:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xmake f -c
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3: 重新构建 Switch 版本
|
||||||
|
```bash
|
||||||
|
xmake build -p switch
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证步骤
|
||||||
|
1. 清理完成后,查看 build 目录是否被删除
|
||||||
|
2. 重新构建时,应该能看到所有源文件被重新编译
|
||||||
|
3. 运行新构建的 NRO 文件,验证 `printf("LoadA")` 是否输出
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
- 所有源文件被重新编译
|
||||||
|
- 新的修改生效
|
||||||
|
- `printf("LoadA")` 能够正常输出
|
||||||
|
- 可以进一步定位真正的崩溃位置
|
||||||
66
.opencode/plans/fix_sdl_image_link.md
Normal file
66
.opencode/plans/fix_sdl_image_link.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# 修复 SDL2_image 链接错误计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
链接时出现大量 undefined reference 错误,主要是关于 libjpeg 的函数:
|
||||||
|
- `jpeg_calc_output_dimensions`
|
||||||
|
- `jpeg_CreateDecompress`
|
||||||
|
- `jpeg_destroy_decompress`
|
||||||
|
- `jpeg_finish_decompress`
|
||||||
|
等等
|
||||||
|
|
||||||
|
## 问题原因
|
||||||
|
SDL2_image 依赖多个第三方库来支持不同的图片格式:
|
||||||
|
- libjpeg - JPEG 格式支持
|
||||||
|
- libpng - PNG 格式支持
|
||||||
|
- libtiff - TIFF 格式支持
|
||||||
|
- libwebp - WebP 格式支持
|
||||||
|
等等
|
||||||
|
|
||||||
|
需要在 switch.lua 中添加这些依赖库的链接。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 修改 platform/switch.lua
|
||||||
|
在 `add_syslinks` 中添加 SDL2_image 所需的依赖库。
|
||||||
|
|
||||||
|
根据 devkitPro/portlibs 的通常配置,需要添加:
|
||||||
|
- libpng
|
||||||
|
- libjpeg
|
||||||
|
- zlib (libpng 依赖)
|
||||||
|
- 可能还需要其他库
|
||||||
|
|
||||||
|
## 具体修复代码
|
||||||
|
|
||||||
|
修改 `platform/switch.lua` 中的 `add_syslinks` 部分:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 修改前
|
||||||
|
add_syslinks("SDL2_mixer", "SDL2_image", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
||||||
|
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
|
{public = true})
|
||||||
|
|
||||||
|
-- 修改后
|
||||||
|
add_syslinks("SDL2_mixer", "SDL2_image", "SDL2",
|
||||||
|
"png", "jpeg", "z", -- SDL2_image 依赖
|
||||||
|
"opusfile", "opus", "vorbisidec", "ogg",
|
||||||
|
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
|
{public = true})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 库的顺序说明
|
||||||
|
链接顺序很重要,被依赖的库要放在后面:
|
||||||
|
- SDL2_image 依赖 png、jpeg、z
|
||||||
|
- png 依赖 z
|
||||||
|
- 所以顺序是: SDL2_image → png → jpeg → z
|
||||||
|
|
||||||
|
## 备选方案
|
||||||
|
如果上面的库还不够,可以尝试添加更多库:
|
||||||
|
- `tiff` - TIFF 支持
|
||||||
|
- `webp` - WebP 支持
|
||||||
|
- `lzma` - 某些图片格式可能需要
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
1. 应用修复
|
||||||
|
2. 清理缓存
|
||||||
|
3. 重新编译
|
||||||
|
4. 验证链接成功
|
||||||
169
.opencode/plans/replace_stb_with_sdl_image.md
Normal file
169
.opencode/plans/replace_stb_with_sdl_image.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# 将 stb_image 替换为 SDL_image 完整计划
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
将项目中的 stb_image 完全替换为 SDL_image,以解决 Switch 平台兼容性问题。
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
### 1. 平台配置文件(3个文件)
|
||||||
|
- `platform/mingw.lua` - Windows/Mingw 配置
|
||||||
|
- `platform/linux.lua` - Linux 配置
|
||||||
|
- `platform/switch.lua` - Switch 配置
|
||||||
|
|
||||||
|
### 2. 核心代码文件(1个文件)
|
||||||
|
- `Frostbite2D/src/frostbite2D/graphics/texture.cpp` - 纹理加载实现
|
||||||
|
|
||||||
|
## 详细修改方案
|
||||||
|
|
||||||
|
### 修改 1: platform/mingw.lua
|
||||||
|
**变更**: 添加 SDL_image 依赖
|
||||||
|
```lua
|
||||||
|
-- 修改前
|
||||||
|
add_requires("libsdl2", {configs = {shared = true}})
|
||||||
|
add_requires("glm")
|
||||||
|
|
||||||
|
-- 修改后
|
||||||
|
add_requires("libsdl2", {configs = {shared = true}})
|
||||||
|
add_requires("libsdl2_image", {configs = {shared = true}})
|
||||||
|
add_requires("glm")
|
||||||
|
|
||||||
|
-- 在 add_packages 部分添加
|
||||||
|
add_packages("libsdl2")
|
||||||
|
add_packages("libsdl2_image")
|
||||||
|
add_packages("glm")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 2: platform/linux.lua
|
||||||
|
**变更**: 添加 SDL_image 依赖
|
||||||
|
```lua
|
||||||
|
-- 修改前
|
||||||
|
add_requires("libsdl2", {configs = {shared = true,wayland = true}})
|
||||||
|
add_requires("glm")
|
||||||
|
|
||||||
|
-- 修改后
|
||||||
|
add_requires("libsdl2", {configs = {shared = true,wayland = true}})
|
||||||
|
add_requires("libsdl2_image")
|
||||||
|
add_requires("glm")
|
||||||
|
|
||||||
|
-- 在 add_packages 部分添加
|
||||||
|
add_packages("libsdl2")
|
||||||
|
add_packages("libsdl2_image")
|
||||||
|
add_packages("glm")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 3: platform/switch.lua
|
||||||
|
**变更**: 添加 SDL2_image 库链接
|
||||||
|
```lua
|
||||||
|
-- 修改前
|
||||||
|
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
||||||
|
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
|
{public = true})
|
||||||
|
|
||||||
|
-- 修改后
|
||||||
|
add_syslinks("SDL2_mixer", "SDL2_image", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
||||||
|
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
|
{public = true})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改 4: texture.cpp
|
||||||
|
**变更**: 完全重写 `loadFromFile` 函数,使用 SDL_image
|
||||||
|
```cpp
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_image.h>
|
||||||
|
|
||||||
|
// 移除所有 stb_image 相关的定义和 include
|
||||||
|
// #define STB_IMAGE_IMPLEMENTATION
|
||||||
|
// #include <stb/stb_image.h>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::string resolvedPath = asset.resolveAssetPath(path);
|
||||||
|
std::vector<uint8> fileData;
|
||||||
|
|
||||||
|
SDL_Log("LoadQ");
|
||||||
|
if (!asset.readBinaryFile(path, fileData)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read texture file: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
SDL_Log("LoadQ1: file size = %zu bytes", fileData.size());
|
||||||
|
|
||||||
|
if (fileData.empty()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Texture file is empty: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 SDL_image 从内存加载
|
||||||
|
SDL_RWops* rw = SDL_RWFromConstMem(fileData.data(), static_cast<int>(fileData.size()));
|
||||||
|
if (!rw) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops: %s", SDL_GetError());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface* surface = IMG_Load_RW(rw, 1);
|
||||||
|
if (!surface) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image: %s", IMG_GetError());
|
||||||
|
SDL_FreeRW(rw);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("LoadQ2: %dx%d, format: %d", surface->w, surface->h, surface->format->format);
|
||||||
|
|
||||||
|
// 确定格式
|
||||||
|
GLenum format;
|
||||||
|
int channels;
|
||||||
|
if (surface->format->BytesPerPixel == 4) {
|
||||||
|
format = GL_RGBA;
|
||||||
|
channels = 4;
|
||||||
|
} else {
|
||||||
|
format = GL_RGB;
|
||||||
|
channels = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = surface->w;
|
||||||
|
int height = surface->h;
|
||||||
|
|
||||||
|
uint32_t textureID;
|
||||||
|
glGenTextures(1, &textureID);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||||
|
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, surface->pixels);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
|
||||||
|
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
|
||||||
|
texture->path_ = resolvedPath;
|
||||||
|
texture->channels_ = channels;
|
||||||
|
|
||||||
|
SDL_Log("Loaded texture: %s (%dx%d, %d channels)", resolvedPath.c_str(), width, height, channels);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其余函数保持不变
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 修改 3 个平台配置文件,添加 SDL_image 依赖
|
||||||
|
2. 修改 texture.cpp,替换 stb_image 为 SDL_image
|
||||||
|
3. 清理 xmake 缓存
|
||||||
|
4. 重新编译项目
|
||||||
|
5. 测试所有平台
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- SDL_image 支持的图片格式包括: PNG, JPEG, BMP, GIF, TIFF, WebP 等
|
||||||
|
- 确保所有平台都能正确链接 SDL_image 库
|
||||||
|
- Switch 平台的 devkitPro/portlibs 应该已经包含 SDL2_image
|
||||||
74
.opencode/plans/stbi_debug_fix.md
Normal file
74
.opencode/plans/stbi_debug_fix.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# stbi_load_from_memory 失败调试和修复计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
`stbi_load_from_memory()` 执行失败,但不知道具体失败原因。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 1. 添加详细的调试信息
|
||||||
|
修改 `texture.cpp`,添加以下调试信息:
|
||||||
|
- 输出读取到的文件大小
|
||||||
|
- 输出 stbi 的失败原因(使用 `stbi_failure_reason()`)
|
||||||
|
|
||||||
|
### 2. 修改代码
|
||||||
|
**文件**: `Frostbite2D/src/frostbite2D/graphics/texture.cpp`
|
||||||
|
|
||||||
|
**变更**:
|
||||||
|
```cpp
|
||||||
|
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::string resolvedPath = asset.resolveAssetPath(path);
|
||||||
|
std::vector<uint8> fileData;
|
||||||
|
SDL_Log("LoadQ");
|
||||||
|
if (!asset.readBinaryFile(path, fileData)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read texture file: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
SDL_Log("LoadQ1: file size = %zu bytes", fileData.size());
|
||||||
|
|
||||||
|
// 检查文件是否为空
|
||||||
|
if (fileData.empty()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Texture file is empty: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width, height, channels;
|
||||||
|
uint8* data = stbi_load_from_memory(fileData.data(), static_cast<int>(fileData.size()), &width, &height, &channels, 0);
|
||||||
|
SDL_Log("LoadQ2");
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
const char* failureReason = stbi_failure_reason();
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s, reason: %s", resolvedPath.c_str(), failureReason ? failureReason : "unknown");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("LoadQ3: %dx%d, %d channels", width, height, channels);
|
||||||
|
|
||||||
|
// ... 其余代码保持不变
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试步骤
|
||||||
|
1. 应用上述修改
|
||||||
|
2. 重新编译并运行
|
||||||
|
3. 查看日志输出,特别注意:
|
||||||
|
- `file size` 的值是多少
|
||||||
|
- `stbi_failure_reason()` 输出的失败原因是什么
|
||||||
|
|
||||||
|
## 根据失败原因的可能解决方案
|
||||||
|
|
||||||
|
### 如果失败原因是 "png" 相关
|
||||||
|
- 检查 PNG 文件是否损坏
|
||||||
|
- 尝试用其他图片格式(如 BMP)测试
|
||||||
|
|
||||||
|
### 如果失败原因是 "out of memory"
|
||||||
|
- 检查图片是否太大
|
||||||
|
- 考虑调整 stbi 的内存分配设置
|
||||||
|
|
||||||
|
### 如果文件大小为 0
|
||||||
|
- 检查文件路径是否正确
|
||||||
|
- 检查 Asset 类的读取是否有问题
|
||||||
|
- 检查文件是否存在于 Switch 上
|
||||||
|
|
||||||
|
### 如果是其他原因
|
||||||
|
- 根据具体的失败原因进一步分析
|
||||||
68
.opencode/plans/stbi_switch_fix.md
Normal file
68
.opencode/plans/stbi_switch_fix.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Switch 平台 stbi_load_from_memory 崩溃修复计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
- 文件读取成功:129244 字节
|
||||||
|
- 但调用 `stbi_load_from_memory()` 时直接崩溃
|
||||||
|
- 说明 stbi_image 在 Switch 平台上有兼容性问题
|
||||||
|
|
||||||
|
## 可能的原因
|
||||||
|
|
||||||
|
1. **stbi_image 的内存分配问题** - stbi 默认使用 malloc/free,在 Switch 上可能有问题
|
||||||
|
2. **缺少 stbi_image 的平台特定配置**
|
||||||
|
3. **SIMD 优化问题** - stbi_image 的 ARM NEON 优化在 Switch 上可能有问题
|
||||||
|
4. **栈溢出** - stbi_image 可能在栈上分配了太大的结构
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 方案 1: 配置 stbi_image 使用自定义内存分配(推荐)
|
||||||
|
修改 texture.cpp,在 include stb_image.h 之前定义自定义的内存分配函数。
|
||||||
|
|
||||||
|
### 方案 2: 禁用 stbi_image 的 SIMD 优化
|
||||||
|
定义 `STBI_NO_SIMD` 来禁用 NEON 优化。
|
||||||
|
|
||||||
|
### 方案 3: 两者结合(最稳妥)
|
||||||
|
同时使用自定义内存分配和禁用 SIMD 优化。
|
||||||
|
|
||||||
|
## 具体修复代码
|
||||||
|
|
||||||
|
修改 `Frostbite2D/src/frostbite2D/graphics/texture.cpp` 的开头部分:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
// 自定义 stbi_image 的内存分配函数
|
||||||
|
#define STBI_MALLOC(sz) malloc(sz)
|
||||||
|
#define STBI_REALLOC(p,sz) realloc(p,sz)
|
||||||
|
#define STBI_FREE(p) free(p)
|
||||||
|
|
||||||
|
// 禁用 SIMD 优化,避免 Switch 平台兼容性问题
|
||||||
|
#define STBI_NO_SIMD
|
||||||
|
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "SDL_log.h"
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <frostbite2D/graphics/texture.h>
|
||||||
|
#include <frostbite2D/utils/asset.h>
|
||||||
|
#include <stb/stb_image.h>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 额外调试建议
|
||||||
|
|
||||||
|
如果上述修复后仍然崩溃,可以添加更多调试来定位具体位置:
|
||||||
|
|
||||||
|
1. 在 stbi_load_from_memory 调用前后添加 printf
|
||||||
|
2. 尝试用最简单的图片(小尺寸 BMP)测试
|
||||||
|
3. 检查 stbi_image 的版本
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
1. 应用修复
|
||||||
|
2. 清理缓存并重新编译
|
||||||
|
3. 运行测试
|
||||||
|
4. 查看是否仍然崩溃
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
- stbi_load_from_memory 不再崩溃
|
||||||
|
- 图片能够正常加载
|
||||||
|
- LoadQ2 和 LoadQ3 日志能够正常输出
|
||||||
74
.opencode/plans/switch_asset_fix.md
Normal file
74
.opencode/plans/switch_asset_fix.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Switch 平台 Asset 类崩溃修复计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
调用 `Sprite::createFromFile("assets/player.png")` 时崩溃,甚至连 printf 都没执行。崩溃发生在 `Asset::readBinaryFile()` 内部的 `fs::exists()` 调用上。
|
||||||
|
|
||||||
|
## 问题根因分析
|
||||||
|
|
||||||
|
Switch 平台上的 `std::filesystem` 实现可能不稳定或不完整,导致 `fs::exists()` 调用直接崩溃。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
修改 `Asset::readBinaryFile()` 函数,**移除文件存在性检查**,直接尝试打开文件。这样可以避免调用有问题的 `fs::exists()`。
|
||||||
|
|
||||||
|
### 修复: 修改 asset.cpp
|
||||||
|
**文件**: `Frostbite2D/src/frostbite2D/utils/asset.cpp`
|
||||||
|
|
||||||
|
**变更**:
|
||||||
|
- 移除 `readBinaryFile()` 中的 `exists()` 检查
|
||||||
|
- 直接尝试打开文件,通过 `file.is_open()` 来判断是否成功
|
||||||
|
|
||||||
|
**代码修改**:
|
||||||
|
```cpp
|
||||||
|
bool Asset::readBinaryFile(const std::string &path,
|
||||||
|
std::vector<uint8> &outData) {
|
||||||
|
std::string fullPath = resolveFullPath(path);
|
||||||
|
|
||||||
|
// 移除 fs::exists() 检查,直接尝试打开文件
|
||||||
|
std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.seekg(0, std::ios::end);
|
||||||
|
auto size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
outData.resize(static_cast<size_t>(size));
|
||||||
|
file.read(reinterpret_cast<char *>(outData.data()), size);
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 可选: 同时修复 readTextFile
|
||||||
|
为了保持一致性,也可以对 `readTextFile()` 做同样的修改:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Asset::readTextFile(const std::string &path, std::string &outContent) {
|
||||||
|
std::string fullPath = resolveFullPath(path);
|
||||||
|
|
||||||
|
// 移除 fs::exists() 检查,直接尝试打开文件
|
||||||
|
std::ifstream file(toPath(fullPath), std::ios::in | std::ios::binary);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << file.rdbuf();
|
||||||
|
outContent = ss.str();
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
1. 应用修复
|
||||||
|
2. 重新构建 Switch 版本: `xmake build -p switch`
|
||||||
|
3. 在 Switch 上运行生成的 NRO 文件
|
||||||
|
4. 验证图片能正常加载
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
- 不再崩溃
|
||||||
|
- 文件能正常读取
|
||||||
|
- 图片能正常加载和显示
|
||||||
51
.opencode/plans/switch_stb_fix.md
Normal file
51
.opencode/plans/switch_stb_fix.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Switch 平台 stbi_load 崩溃修复计划
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
在 Switch 平台运行时,调用 `Sprite::createFromFile("assets/player.png")` 会在 `stbi_load` 这一行崩溃。
|
||||||
|
|
||||||
|
## 修复方案(简化版)
|
||||||
|
|
||||||
|
只修改一个文件:使用 Asset 类读取文件到内存,再用 `stbi_load_from_memory` 加载。
|
||||||
|
|
||||||
|
### 修复: 修改 texture.cpp
|
||||||
|
**文件**: `Frostbite2D/src/frostbite2D/graphics/texture.cpp`
|
||||||
|
|
||||||
|
**变更**:
|
||||||
|
- 使用 `Asset::readBinaryFile()` 读取文件到内存
|
||||||
|
- 使用 `stbi_load_from_memory()` 替代 `stbi_load()`
|
||||||
|
|
||||||
|
**代码修改**:
|
||||||
|
```cpp
|
||||||
|
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
||||||
|
Asset& asset = Asset::get();
|
||||||
|
std::vector<uint8> fileData;
|
||||||
|
|
||||||
|
if (!asset.readBinaryFile(path, fileData)) {
|
||||||
|
std::string resolvedPath = asset.resolveAssetPath(path);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read texture file: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width, height, channels;
|
||||||
|
uint8* data = stbi_load_from_memory(fileData.data(), static_cast<int>(fileData.size()), &width, &height, &channels, 0);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
std::string resolvedPath = asset.resolveAssetPath(path);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... 其余代码保持不变
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
1. 应用修复
|
||||||
|
2. 重新构建 Switch 版本: `xmake build -p switch`
|
||||||
|
3. 在 Switch 上运行生成的 NRO 文件
|
||||||
|
4. 验证图片能正常加载
|
||||||
|
|
||||||
|
## 预期结果
|
||||||
|
- stbi_load 不再崩溃
|
||||||
|
- 图片能正常加载和显示
|
||||||
|
- 程序能正常运行
|
||||||
222
Frostbite2D/include/frostbite2D/utils/pvf_archive.h
Normal file
222
Frostbite2D/include/frostbite2D/utils/pvf_archive.h
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_alias.h>
|
||||||
|
#include <frostbite2D/utils/binary_reader.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PVF 文件信息结构体
|
||||||
|
*/
|
||||||
|
struct PvfFileInfo {
|
||||||
|
size_t offset = 0; ///< 相对于数据起始位置的偏移
|
||||||
|
uint32 crc32 = 0; ///< CRC32 校验值
|
||||||
|
size_t length = 0; ///< 文件长度(字节)
|
||||||
|
bool decoded = false; ///< 是否已解码
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 原始文件数据结构体
|
||||||
|
*/
|
||||||
|
struct RawData {
|
||||||
|
std::unique_ptr<char[]> data; ///< 原始数据指针(智能指针自动管理内存)
|
||||||
|
size_t size; ///< 数据大小(字节)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PVF 资源包归档类
|
||||||
|
*
|
||||||
|
* 用于读取和解析 PVF 格式的游戏资源包文件。
|
||||||
|
* 提供文件索引管理、字符串资源访问和文件内容读取功能。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* auto& archive = PvfArchive::get();
|
||||||
|
* if (archive.open("Script.pvf")) {
|
||||||
|
* archive.init();
|
||||||
|
* auto content = archive.getFileContent("script/example.txt");
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
class PvfArchive {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief 获取单例实例
|
||||||
|
* @return PVF 归档实例引用
|
||||||
|
*/
|
||||||
|
static PvfArchive& get();
|
||||||
|
|
||||||
|
PvfArchive(const PvfArchive&) = delete;
|
||||||
|
PvfArchive& operator=(const PvfArchive&) = delete;
|
||||||
|
PvfArchive(PvfArchive&&) = delete;
|
||||||
|
PvfArchive& operator=(PvfArchive&&) = delete;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 文件操作
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开 PVF 文件
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @return 打开成功返回 true
|
||||||
|
*/
|
||||||
|
bool open(const std::string& filePath = "Script.pvf");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 关闭并清空所有数据
|
||||||
|
*/
|
||||||
|
void close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查是否已打开
|
||||||
|
* @return 已打开返回 true
|
||||||
|
*/
|
||||||
|
bool isOpen() const;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 初始化
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 完整初始化(解析头部、加载字符串表)
|
||||||
|
*/
|
||||||
|
void init();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析 PVF 文件头部并建立文件索引
|
||||||
|
*/
|
||||||
|
void initHeader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载二进制字符串表 (stringtable.bin)
|
||||||
|
*/
|
||||||
|
void initBinStringTable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载本地化字符串 (n_string.lst)
|
||||||
|
*/
|
||||||
|
void initLoadStrings();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 文件信息查询
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查文件是否存在
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return 存在返回 true
|
||||||
|
*/
|
||||||
|
bool hasFile(const std::string& path) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件信息
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return 文件信息,不存在返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<PvfFileInfo> getFileInfo(const std::string& path) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取所有文件路径列表
|
||||||
|
* @return 文件路径列表
|
||||||
|
*/
|
||||||
|
std::vector<std::string> listFiles() const;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 文件内容读取
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件内容作为字符串
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return 文件内容,失败返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<std::string> getFileContent(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件内容作为字节数组
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return 字节数组,失败返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<std::vector<uint8>> getFileBytes(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件原始数据(使用智能指针管理内存,已包含 CRC 解密)
|
||||||
|
*
|
||||||
|
* 与原始 GetFileContentChar 功能相同,但使用智能指针避免内存泄漏。
|
||||||
|
* 数据会在首次访问时自动进行 CRC 解密。
|
||||||
|
*
|
||||||
|
* @param path 文件路径
|
||||||
|
* @return RawData 结构体,包含数据指针和大小;失败返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<RawData> getFileRawData(const std::string& path);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 字符串资源访问
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取二进制字符串
|
||||||
|
* @param key 字符串键
|
||||||
|
* @return 字符串,不存在返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<std::string> getBinString(int key) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取本地化字符串
|
||||||
|
* @param type 字符串类型
|
||||||
|
* @param key 字符串键
|
||||||
|
* @return 字符串,不存在返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<std::string> getLoadString(const std::string& type, const std::string& key) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查二进制字符串是否存在
|
||||||
|
* @param key 字符串键
|
||||||
|
* @return 存在返回 true
|
||||||
|
*/
|
||||||
|
bool hasBinString(int key) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查本地化字符串是否存在
|
||||||
|
* @param type 字符串类型
|
||||||
|
* @param key 字符串键
|
||||||
|
* @return 存在返回 true
|
||||||
|
*/
|
||||||
|
bool hasLoadString(const std::string& type, const std::string& key) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
PvfArchive() = default;
|
||||||
|
~PvfArchive() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 规范化路径(转为小写)
|
||||||
|
* @param path 原始路径
|
||||||
|
* @return 规范化后的路径
|
||||||
|
*/
|
||||||
|
std::string normalizePath(const std::string& path) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 分割字符串
|
||||||
|
* @param str 要分割的字符串
|
||||||
|
* @param delimiter 分隔符
|
||||||
|
* @return 分割后的字符串列表
|
||||||
|
*/
|
||||||
|
std::vector<std::string> splitString(const std::string& str, const std::string& delimiter) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解码文件内容(内部使用)
|
||||||
|
* @param info 文件信息(会修改 decoded 标志)
|
||||||
|
* @return 成功返回 true
|
||||||
|
*/
|
||||||
|
bool decodeFile(PvfFileInfo& info);
|
||||||
|
|
||||||
|
BinaryReader reader_; ///< 二进制读取器
|
||||||
|
size_t dataStartPos_ = 0; ///< 数据起始位置
|
||||||
|
std::map<std::string, PvfFileInfo> fileInfo_; ///< 文件信息映射
|
||||||
|
std::map<int, std::string> binStringTable_; ///< 二进制字符串表
|
||||||
|
std::map<std::string, std::map<std::string, std::string>> loadStrings_; ///< 本地化字符串
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
208
Frostbite2D/include/frostbite2D/utils/script_parser.h
Normal file
208
Frostbite2D/include/frostbite2D/utils/script_parser.h
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <frostbite2D/types/type_alias.h>
|
||||||
|
#include <frostbite2D/utils/pvf_archive.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 脚本指令操作码枚举
|
||||||
|
*/
|
||||||
|
enum class ScriptOpcode : uint8 {
|
||||||
|
Integer = 2, ///< 整数值
|
||||||
|
Float = 4, ///< 浮点值
|
||||||
|
StringRef5 = 5, ///< 二进制字符串引用(类型5)
|
||||||
|
StringRef6 = 6, ///< 二进制字符串引用(类型6)
|
||||||
|
StringRef7 = 7, ///< 二进制字符串引用(类型7)
|
||||||
|
StringRef8 = 8, ///< 二进制字符串引用(类型8)
|
||||||
|
ExtendedString9 = 9, ///< 扩展字符串(带额外字节)
|
||||||
|
ExtendedString10 = 10, ///< 扩展字符串
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 脚本值类型
|
||||||
|
*/
|
||||||
|
enum class ScriptValueType {
|
||||||
|
Empty, ///< 空值
|
||||||
|
Integer, ///< 整数值
|
||||||
|
Float, ///< 浮点值
|
||||||
|
String, ///< 字符串值
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 脚本解析结果值
|
||||||
|
*/
|
||||||
|
struct ScriptValue {
|
||||||
|
ScriptValueType type = ScriptValueType::Empty; ///< 值类型
|
||||||
|
int32 intValue = 0; ///< 整数值(type == Integer 时有效)
|
||||||
|
double floatValue = 0.0; ///< 浮点值(type == Float 时有效)
|
||||||
|
std::string stringValue; ///< 字符串值(type == String 时有效)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断是否为空值
|
||||||
|
*/
|
||||||
|
bool isEmpty() const { return type == ScriptValueType::Empty; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 转换为字符串表示
|
||||||
|
*/
|
||||||
|
std::string toString() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 脚本二进制数据解析器
|
||||||
|
*
|
||||||
|
* 用于解析 PVF 存档中的脚本二进制数据。
|
||||||
|
* 每条指令为 5 字节:1 字节操作码 + 4 字节数据。
|
||||||
|
* 支持与 PvfArchive 交互获取字符串资源。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* auto& archive = PvfArchive::get();
|
||||||
|
* if (auto rawData = archive.getFileRawData("script/example.bin")) {
|
||||||
|
* ScriptParser parser(*rawData, "script/example.bin");
|
||||||
|
*
|
||||||
|
* // 迭代解析
|
||||||
|
* while (!parser.isEnd()) {
|
||||||
|
* if (auto value = parser.next()) {
|
||||||
|
* // 处理 value
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // 或者一次性解析所有
|
||||||
|
* auto allValues = parser.parseAll();
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
class ScriptParser {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief 从 RawData 构造
|
||||||
|
* @param data 原始数据
|
||||||
|
* @param filePath 文件路径(用于提取文件类型)
|
||||||
|
*/
|
||||||
|
ScriptParser(const RawData& data, const std::string& filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从字节数组构造
|
||||||
|
* @param data 字节数组
|
||||||
|
* @param filePath 文件路径(用于提取文件类型)
|
||||||
|
*/
|
||||||
|
ScriptParser(const std::vector<uint8>& data, const std::string& filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从指针和大小构造
|
||||||
|
* @param data 数据指针
|
||||||
|
* @param size 数据大小
|
||||||
|
* @param filePath 文件路径(用于提取文件类型)
|
||||||
|
*/
|
||||||
|
ScriptParser(const char* data, size_t size, const std::string& filePath);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 迭代器风格 API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取下一个值
|
||||||
|
* @return 解析的值,失败或结束返回 std::nullopt
|
||||||
|
*/
|
||||||
|
std::optional<ScriptValue> next();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 回退一个指令
|
||||||
|
*/
|
||||||
|
void back();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查是否已到达数据末尾
|
||||||
|
* @return 到达末尾返回 true
|
||||||
|
*/
|
||||||
|
bool isEnd() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置解析位置到开头
|
||||||
|
*/
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 批量解析
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析所有值
|
||||||
|
* @return 所有解析值的列表
|
||||||
|
*/
|
||||||
|
std::vector<ScriptValue> parseAll();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 状态查询
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前解析位置
|
||||||
|
* @return 当前位置(字节偏移)
|
||||||
|
*/
|
||||||
|
size_t position() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取数据总大小
|
||||||
|
* @return 数据大小(字节)
|
||||||
|
*/
|
||||||
|
size_t size() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件路径
|
||||||
|
* @return 文件路径
|
||||||
|
*/
|
||||||
|
const std::string& filePath() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取文件类型(从路径提取)
|
||||||
|
* @return 文件类型
|
||||||
|
*/
|
||||||
|
const std::string& fileType() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查解析器是否有效(有数据)
|
||||||
|
* @return 有效返回 true
|
||||||
|
*/
|
||||||
|
bool isValid() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* @brief 从文件路径提取文件类型
|
||||||
|
* @param filePath 文件路径
|
||||||
|
* @return 文件类型
|
||||||
|
*/
|
||||||
|
std::string extractFileType(const std::string& filePath) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 读取 1 字节
|
||||||
|
* @param offset 偏移位置
|
||||||
|
* @return 字节值
|
||||||
|
*/
|
||||||
|
uint8 readByte(size_t offset) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 读取 4 字节整数
|
||||||
|
* @param offset 偏移位置
|
||||||
|
* @return 整数值
|
||||||
|
*/
|
||||||
|
int32 readInt32(size_t offset) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析单个值
|
||||||
|
* @param offset 偏移位置
|
||||||
|
* @return 解析的值
|
||||||
|
*/
|
||||||
|
std::optional<ScriptValue> parseValueAt(size_t offset) const;
|
||||||
|
|
||||||
|
std::vector<char> data_; ///< 数据副本
|
||||||
|
size_t position_ = 2; ///< 当前解析位置(从第 2 字节开始)
|
||||||
|
std::string filePath_; ///< 文件路径
|
||||||
|
std::string fileType_; ///< 文件类型
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
@@ -18,9 +18,9 @@ Sprite::Sprite(Ptr<Texture> texture)
|
|||||||
Sprite::~Sprite() {
|
Sprite::~Sprite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ptr<Sprite> Sprite::createFromFile(const std::string& path) {
|
Ptr<Sprite> Sprite::createFromFile(const std::string &path) {
|
||||||
Ptr<Texture> texture = Texture::loadFromFile(path);
|
Ptr<Texture> texture = Texture::loadFromFile(path);
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#define STB_IMAGE_IMPLEMENTATION
|
|
||||||
#include "SDL_log.h"
|
#include "SDL_log.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_image.h>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <frostbite2D/graphics/texture.h>
|
#include <frostbite2D/graphics/texture.h>
|
||||||
#include <frostbite2D/utils/asset.h>
|
#include <frostbite2D/utils/asset.h>
|
||||||
#include <stb/stb_image.h>
|
|
||||||
#include <glad/glad.h>
|
#include <glad/glad.h>
|
||||||
|
|
||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
@@ -21,22 +22,55 @@ Texture::~Texture() {
|
|||||||
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
||||||
Asset& asset = Asset::get();
|
Asset& asset = Asset::get();
|
||||||
std::string resolvedPath = asset.resolveAssetPath(path);
|
std::string resolvedPath = asset.resolveAssetPath(path);
|
||||||
|
std::vector<uint8> fileData;
|
||||||
|
|
||||||
int width, height, channels;
|
if (!asset.readBinaryFile(path, fileData)) {
|
||||||
uint8* data = stbi_load(resolvedPath.c_str(), &width, &height, &channels, 0);
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to read texture file: %s", resolvedPath.c_str());
|
||||||
if (!data) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", resolvedPath.c_str());
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t format = channels == 4 ? GL_RGBA : GL_RGB;
|
if (fileData.empty()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Texture file is empty: %s",
|
||||||
|
resolvedPath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RWops *rw =
|
||||||
|
SDL_RWFromConstMem(fileData.data(), static_cast<int>(fileData.size()));
|
||||||
|
if (!rw) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create RWops: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface *surface = IMG_Load_RW(rw, 1);
|
||||||
|
if (!surface) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image: %s",
|
||||||
|
IMG_GetError());
|
||||||
|
SDL_FreeRW(rw);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum format;
|
||||||
|
int channels;
|
||||||
|
if (surface->format->BytesPerPixel == 4) {
|
||||||
|
format = GL_RGBA;
|
||||||
|
channels = 4;
|
||||||
|
} else {
|
||||||
|
format = GL_RGB;
|
||||||
|
channels = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = surface->w;
|
||||||
|
int height = surface->h;
|
||||||
|
|
||||||
uint32_t textureID;
|
uint32_t textureID;
|
||||||
glGenTextures(1, &textureID);
|
glGenTextures(1, &textureID);
|
||||||
glBindTexture(GL_TEXTURE_2D, textureID);
|
glBindTexture(GL_TEXTURE_2D, textureID);
|
||||||
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
|
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format,
|
||||||
|
GL_UNSIGNED_BYTE, surface->pixels);
|
||||||
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
@@ -44,14 +78,13 @@ Ptr<Texture> Texture::loadFromFile(const std::string& path) {
|
|||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
stbi_image_free(data);
|
|
||||||
|
SDL_FreeSurface(surface);
|
||||||
|
|
||||||
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
|
auto texture = Ptr<Texture>(new Texture(width, height, textureID));
|
||||||
texture->path_ = resolvedPath;
|
texture->path_ = resolvedPath;
|
||||||
texture->channels_ = channels;
|
texture->channels_ = channels;
|
||||||
|
|
||||||
SDL_Log("Loaded texture: %s (%dx%d, %d channels)", resolvedPath.c_str(), width, height, channels);
|
|
||||||
|
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
334
Frostbite2D/src/frostbite2D/utils/pvf_archive.cpp
Normal file
334
Frostbite2D/src/frostbite2D/utils/pvf_archive.cpp
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
#include <frostbite2D/utils/pvf_archive.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
PvfArchive& PvfArchive::get() {
|
||||||
|
static PvfArchive instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::open(const std::string& filePath) {
|
||||||
|
close();
|
||||||
|
|
||||||
|
if (!reader_.open(filePath)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 无法打开文件: %s", filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::close() {
|
||||||
|
reader_.close();
|
||||||
|
dataStartPos_ = 0;
|
||||||
|
fileInfo_.clear();
|
||||||
|
binStringTable_.clear();
|
||||||
|
loadStrings_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::isOpen() const {
|
||||||
|
return reader_.isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::init() {
|
||||||
|
if (!isOpen()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: 请先打开文件再初始化");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
initHeader();
|
||||||
|
initBinStringTable();
|
||||||
|
initLoadStrings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::initHeader() {
|
||||||
|
if (!isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(0);
|
||||||
|
|
||||||
|
int32 uuidLength = reader_.readInt32();
|
||||||
|
std::string uuid = reader_.readString(uuidLength);
|
||||||
|
(void)uuid;
|
||||||
|
|
||||||
|
int32 version = reader_.readInt32();
|
||||||
|
(void)version;
|
||||||
|
|
||||||
|
int32 alignedIndexHeaderSize = reader_.readInt32();
|
||||||
|
int32 indexHeaderCrc = reader_.readInt32();
|
||||||
|
int32 indexSize = reader_.readInt32();
|
||||||
|
|
||||||
|
size_t firstPos = reader_.tell();
|
||||||
|
reader_.crcDecode(alignedIndexHeaderSize, indexHeaderCrc);
|
||||||
|
|
||||||
|
dataStartPos_ = alignedIndexHeaderSize + 56;
|
||||||
|
size_t currPos = 0;
|
||||||
|
|
||||||
|
for (int32 i = 0; i < indexSize; ++i) {
|
||||||
|
reader_.seek(firstPos + currPos);
|
||||||
|
|
||||||
|
int32 fileNumber = reader_.readInt32();
|
||||||
|
(void)fileNumber;
|
||||||
|
|
||||||
|
int32 filePathLength = reader_.readInt32();
|
||||||
|
std::string fileName = normalizePath(reader_.readString(filePathLength));
|
||||||
|
|
||||||
|
int32 fileLength = reader_.readInt32();
|
||||||
|
int32 crc32 = reader_.readInt32();
|
||||||
|
int32 relativeOffset = reader_.readInt32();
|
||||||
|
|
||||||
|
if (fileLength > 0) {
|
||||||
|
int32 realFileLength = (fileLength + 3) & 0xFFFFFFFC;
|
||||||
|
PvfFileInfo info;
|
||||||
|
info.offset = relativeOffset;
|
||||||
|
info.crc32 = static_cast<uint32>(crc32);
|
||||||
|
info.length = realFileLength;
|
||||||
|
info.decoded = false;
|
||||||
|
fileInfo_[fileName] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
currPos += 20;
|
||||||
|
currPos += filePathLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::initBinStringTable() {
|
||||||
|
std::string tablePath = "stringtable.bin";
|
||||||
|
auto infoOpt = getFileInfo(tablePath);
|
||||||
|
|
||||||
|
if (!infoOpt.has_value()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: stringtable.bin 文件不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PvfFileInfo info = infoOpt.value();
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
reader_.crcDecode(info.length, info.crc32);
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
|
||||||
|
size_t fileHeaderPos = reader_.tell();
|
||||||
|
int32 count = reader_.readInt32();
|
||||||
|
|
||||||
|
for (int32 i = 0; i < count; ++i) {
|
||||||
|
reader_.seek(fileHeaderPos + i * 4 + 4);
|
||||||
|
int32 startPos = reader_.readInt32();
|
||||||
|
int32 endPos = reader_.readInt32();
|
||||||
|
int32 len = endPos - startPos;
|
||||||
|
|
||||||
|
reader_.seek(fileHeaderPos + startPos + 4);
|
||||||
|
std::string str = reader_.readString(len);
|
||||||
|
binStringTable_[i] = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PvfArchive::initLoadStrings() {
|
||||||
|
std::string lstPath = "n_string.lst";
|
||||||
|
auto infoOpt = getFileInfo(lstPath);
|
||||||
|
|
||||||
|
if (!infoOpt.has_value()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "PvfArchive: n_string.lst 文件不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PvfFileInfo info = infoOpt.value();
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
reader_.crcDecode(info.length, info.crc32);
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
|
||||||
|
size_t fileHeaderPos = reader_.tell();
|
||||||
|
int16 flag = reader_.readInt16();
|
||||||
|
(void)flag;
|
||||||
|
|
||||||
|
size_t i = 2;
|
||||||
|
while (i < info.length) {
|
||||||
|
if ((info.length - i) >= 10) {
|
||||||
|
reader_.seek(fileHeaderPos + i + 6);
|
||||||
|
int32 findKey = reader_.readInt32();
|
||||||
|
|
||||||
|
auto binStrOpt = getBinString(findKey);
|
||||||
|
if (!binStrOpt.has_value()) {
|
||||||
|
i += 10;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string key = binStrOpt.value();
|
||||||
|
size_t slashPos = key.find('/');
|
||||||
|
std::string type = (slashPos != std::string::npos) ? normalizePath(key.substr(0, slashPos)) : "";
|
||||||
|
|
||||||
|
if (!key.empty()) {
|
||||||
|
auto fileInfoOpt = getFileInfo(key);
|
||||||
|
if (fileInfoOpt.has_value()) {
|
||||||
|
auto contentOpt = getFileContent(key);
|
||||||
|
if (contentOpt.has_value()) {
|
||||||
|
std::string content = contentOpt.value();
|
||||||
|
std::vector<std::string> lines = splitString(content, "\n");
|
||||||
|
|
||||||
|
for (const auto& line : lines) {
|
||||||
|
size_t gtPos = line.find('>');
|
||||||
|
if (gtPos != std::string::npos && gtPos + 1 < line.length()) {
|
||||||
|
std::string strKey = line.substr(0, gtPos);
|
||||||
|
std::string strValue = line.substr(gtPos + 1);
|
||||||
|
loadStrings_[type][strKey] = strValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::hasFile(const std::string& path) const {
|
||||||
|
return fileInfo_.find(normalizePath(path)) != fileInfo_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<PvfFileInfo> PvfArchive::getFileInfo(const std::string& path) const {
|
||||||
|
std::string normalizedPath = normalizePath(path);
|
||||||
|
auto it = fileInfo_.find(normalizedPath);
|
||||||
|
if (it != fileInfo_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> PvfArchive::listFiles() const {
|
||||||
|
std::vector<std::string> files;
|
||||||
|
files.reserve(fileInfo_.size());
|
||||||
|
for (const auto& pair : fileInfo_) {
|
||||||
|
files.push_back(pair.first);
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> PvfArchive::getFileContent(const std::string& path) {
|
||||||
|
std::string normalizedPath = normalizePath(path);
|
||||||
|
auto it = fileInfo_.find(normalizedPath);
|
||||||
|
if (it == fileInfo_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
PvfFileInfo& info = it->second;
|
||||||
|
if (!decodeFile(info)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
std::vector<uint8> bytes = reader_.readBytes(info.length);
|
||||||
|
return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<uint8>> PvfArchive::getFileBytes(const std::string& path) {
|
||||||
|
std::string normalizedPath = normalizePath(path);
|
||||||
|
auto it = fileInfo_.find(normalizedPath);
|
||||||
|
if (it == fileInfo_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
PvfFileInfo& info = it->second;
|
||||||
|
if (!decodeFile(info)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
return reader_.readBytes(info.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<RawData> PvfArchive::getFileRawData(const std::string& path) {
|
||||||
|
std::string normalizedPath = normalizePath(path);
|
||||||
|
auto it = fileInfo_.find(normalizedPath);
|
||||||
|
if (it == fileInfo_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
PvfFileInfo& info = it->second;
|
||||||
|
if (!decodeFile(info)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
|
||||||
|
RawData result;
|
||||||
|
result.size = info.length;
|
||||||
|
result.data = std::make_unique<char[]>(info.length);
|
||||||
|
reader_.read(result.data.get(), info.length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> PvfArchive::getBinString(int key) const {
|
||||||
|
auto it = binStringTable_.find(key);
|
||||||
|
if (it != binStringTable_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> PvfArchive::getLoadString(const std::string& type, const std::string& key) const {
|
||||||
|
std::string normalizedType = normalizePath(type);
|
||||||
|
auto typeIt = loadStrings_.find(normalizedType);
|
||||||
|
if (typeIt != loadStrings_.end()) {
|
||||||
|
auto keyIt = typeIt->second.find(key);
|
||||||
|
if (keyIt != typeIt->second.end()) {
|
||||||
|
return keyIt->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::hasBinString(int key) const {
|
||||||
|
return binStringTable_.find(key) != binStringTable_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::hasLoadString(const std::string& type, const std::string& key) const {
|
||||||
|
std::string normalizedType = normalizePath(type);
|
||||||
|
auto typeIt = loadStrings_.find(normalizedType);
|
||||||
|
if (typeIt != loadStrings_.end()) {
|
||||||
|
return typeIt->second.find(key) != typeIt->second.end();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PvfArchive::normalizePath(const std::string& path) const {
|
||||||
|
std::string result = path;
|
||||||
|
std::transform(result.begin(), result.end(), result.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> PvfArchive::splitString(const std::string& str, const std::string& delimiter) const {
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
size_t pos = 0;
|
||||||
|
size_t found;
|
||||||
|
|
||||||
|
while ((found = str.find(delimiter, pos)) != std::string::npos) {
|
||||||
|
tokens.push_back(str.substr(pos, found - pos));
|
||||||
|
pos = found + delimiter.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < str.length()) {
|
||||||
|
tokens.push_back(str.substr(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PvfArchive::decodeFile(PvfFileInfo& info) {
|
||||||
|
if (info.decoded) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader_.seek(dataStartPos_ + info.offset);
|
||||||
|
reader_.crcDecode(info.length, info.crc32);
|
||||||
|
info.decoded = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
222
Frostbite2D/src/frostbite2D/utils/script_parser.cpp
Normal file
222
Frostbite2D/src/frostbite2D/utils/script_parser.cpp
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#include <frostbite2D/utils/script_parser.h>
|
||||||
|
#include <frostbite2D/utils/pvf_archive.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
|
||||||
|
std::string ScriptValue::toString() const {
|
||||||
|
switch (type) {
|
||||||
|
case ScriptValueType::Integer:
|
||||||
|
return std::to_string(intValue);
|
||||||
|
case ScriptValueType::Float:
|
||||||
|
return std::to_string(floatValue);
|
||||||
|
case ScriptValueType::String:
|
||||||
|
return stringValue;
|
||||||
|
case ScriptValueType::Empty:
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptParser::ScriptParser(const RawData& data, const std::string& filePath)
|
||||||
|
: filePath_(filePath)
|
||||||
|
, fileType_(extractFileType(filePath)) {
|
||||||
|
if (data.data && data.size > 0) {
|
||||||
|
data_.resize(data.size);
|
||||||
|
std::memcpy(data_.data(), data.data.get(), data.size);
|
||||||
|
}
|
||||||
|
if (data_.size() < 7) {
|
||||||
|
data_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptParser::ScriptParser(const std::vector<uint8>& data, const std::string& filePath)
|
||||||
|
: filePath_(filePath)
|
||||||
|
, fileType_(extractFileType(filePath)) {
|
||||||
|
if (data.size() > 0) {
|
||||||
|
data_.resize(data.size());
|
||||||
|
std::memcpy(data_.data(), data.data(), data.size());
|
||||||
|
}
|
||||||
|
if (data_.size() < 7) {
|
||||||
|
data_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptParser::ScriptParser(const char* data, size_t size, const std::string& filePath)
|
||||||
|
: filePath_(filePath)
|
||||||
|
, fileType_(extractFileType(filePath)) {
|
||||||
|
if (data && size > 0) {
|
||||||
|
data_.resize(size);
|
||||||
|
std::memcpy(data_.data(), data, size);
|
||||||
|
}
|
||||||
|
if (data_.size() < 7) {
|
||||||
|
data_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ScriptValue> ScriptParser::next() {
|
||||||
|
if (!isValid() || isEnd()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto value = parseValueAt(position_);
|
||||||
|
if (value.has_value()) {
|
||||||
|
position_ += 5;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptParser::back() {
|
||||||
|
if (position_ >= 7) {
|
||||||
|
position_ -= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScriptParser::isEnd() const {
|
||||||
|
return !isValid() || position_ >= data_.size() || (data_.size() - position_) < 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptParser::reset() {
|
||||||
|
position_ = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ScriptValue> ScriptParser::parseAll() {
|
||||||
|
std::vector<ScriptValue> values;
|
||||||
|
reset();
|
||||||
|
|
||||||
|
while (!isEnd()) {
|
||||||
|
if (auto value = next()) {
|
||||||
|
values.push_back(*value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ScriptParser::position() const {
|
||||||
|
return position_;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ScriptParser::size() const {
|
||||||
|
return data_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ScriptParser::filePath() const {
|
||||||
|
return filePath_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& ScriptParser::fileType() const {
|
||||||
|
return fileType_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScriptParser::isValid() const {
|
||||||
|
return !data_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ScriptParser::extractFileType(const std::string& filePath) const {
|
||||||
|
size_t slashPos = filePath.find_first_of("/");
|
||||||
|
if (slashPos == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string type = filePath.substr(0, slashPos);
|
||||||
|
std::transform(type.begin(), type.end(), type.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8 ScriptParser::readByte(size_t offset) const {
|
||||||
|
if (offset >= data_.size()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return static_cast<uint8>(data_[offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 ScriptParser::readInt32(size_t offset) const {
|
||||||
|
if (offset + 4 > data_.size()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int32 value;
|
||||||
|
std::memcpy(&value, data_.data() + offset, sizeof(int32));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<ScriptValue> ScriptParser::parseValueAt(size_t offset) const {
|
||||||
|
if (offset + 5 > data_.size()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptValue result;
|
||||||
|
PvfArchive& archive = PvfArchive::get();
|
||||||
|
|
||||||
|
uint8 opcode = readByte(offset);
|
||||||
|
int32 data = readInt32(offset + 1);
|
||||||
|
|
||||||
|
switch (static_cast<ScriptOpcode>(opcode)) {
|
||||||
|
case ScriptOpcode::Integer: {
|
||||||
|
result.type = ScriptValueType::Integer;
|
||||||
|
result.intValue = data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ScriptOpcode::Float: {
|
||||||
|
result.type = ScriptValueType::Float;
|
||||||
|
float floatValue;
|
||||||
|
std::memcpy(&floatValue, &data, sizeof(float));
|
||||||
|
result.floatValue = floatValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ScriptOpcode::StringRef5:
|
||||||
|
case ScriptOpcode::StringRef6:
|
||||||
|
case ScriptOpcode::StringRef7:
|
||||||
|
case ScriptOpcode::StringRef8: {
|
||||||
|
result.type = ScriptValueType::String;
|
||||||
|
if (auto str = archive.getBinString(data)) {
|
||||||
|
result.stringValue = *str;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ScriptOpcode::ExtendedString9: {
|
||||||
|
result.type = ScriptValueType::String;
|
||||||
|
if (offset + 10 <= data_.size()) {
|
||||||
|
uint8 newOpcode = readByte(offset + 5);
|
||||||
|
(void)newOpcode;
|
||||||
|
int32 newData = readInt32(offset + 6);
|
||||||
|
|
||||||
|
if (auto binStr = archive.getBinString(newData)) {
|
||||||
|
if (!binStr->empty()) {
|
||||||
|
if (auto loadStr = archive.getLoadString(fileType_, *binStr)) {
|
||||||
|
result.stringValue = *loadStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ScriptOpcode::ExtendedString10: {
|
||||||
|
result.type = ScriptValueType::String;
|
||||||
|
if (auto binStr = archive.getBinString(data)) {
|
||||||
|
if (!binStr->empty()) {
|
||||||
|
if (auto loadStr = archive.getLoadString(fileType_, *binStr)) {
|
||||||
|
result.stringValue = *loadStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "ScriptParser: 未知的操作码: %d", opcode);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
#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/binary_reader.h>
|
#include <frostbite2D/utils/binary_reader.h>
|
||||||
|
#include <frostbite2D/utils/pvf_archive.h>
|
||||||
|
#include <frostbite2D/utils/script_parser.h>
|
||||||
|
|
||||||
using namespace frostbite2D;
|
using namespace frostbite2D;
|
||||||
|
|
||||||
@@ -18,8 +19,8 @@ int main(int argc, char **argv) {
|
|||||||
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";
|
||||||
config.windowConfig.width = 800;
|
config.windowConfig.width = 1920;
|
||||||
config.windowConfig.height = 600;
|
config.windowConfig.height = 1080;
|
||||||
config.windowConfig.title = "Frostbite2D - Renderer Test";
|
config.windowConfig.title = "Frostbite2D - Renderer Test";
|
||||||
|
|
||||||
Application& app = Application::get();
|
Application& app = Application::get();
|
||||||
@@ -34,9 +35,6 @@ int main(int argc, char **argv) {
|
|||||||
auto menuScene = MakePtr<Scene>();
|
auto menuScene = MakePtr<Scene>();
|
||||||
SceneManager::get().PushScene(menuScene);
|
SceneManager::get().PushScene(menuScene);
|
||||||
|
|
||||||
// 先测试彩色四边形,排除纹理问题
|
|
||||||
SDL_Log("Testing colored quad...");
|
|
||||||
|
|
||||||
// 尝试加载精灵
|
// 尝试加载精灵
|
||||||
auto sprite = Sprite::createFromFile("assets/player.png");
|
auto sprite = Sprite::createFromFile("assets/player.png");
|
||||||
if (sprite) {
|
if (sprite) {
|
||||||
@@ -47,6 +45,52 @@ int main(int argc, char **argv) {
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create sprite from file!");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create sprite from file!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto &archive = PvfArchive::get();
|
||||||
|
if (archive.open("assets/Script.pvf")) {
|
||||||
|
archive.init();
|
||||||
|
// 文件内容读取
|
||||||
|
if (auto rawData = archive.getFileRawData("region/balmayer_north.rgn")) {
|
||||||
|
ScriptParser parser(*rawData, "script/example.bin");
|
||||||
|
// // 方式1:迭代解析
|
||||||
|
// while (!parser.isEnd()) {
|
||||||
|
// if (auto value = parser.next()) {
|
||||||
|
// switch (value->type) {
|
||||||
|
// case ScriptValueType::Integer:
|
||||||
|
// SDL_Log("Integer: %d", value->intValue);
|
||||||
|
// break;
|
||||||
|
// case ScriptValueType::Float:
|
||||||
|
// SDL_Log("Float: %f", value->floatValue);
|
||||||
|
// break;
|
||||||
|
// case ScriptValueType::String:
|
||||||
|
// SDL_Log("String: %s", value->stringValue.c_str());
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 方式2:批量解析
|
||||||
|
// parser.reset();
|
||||||
|
auto allValues = parser.parseAll();
|
||||||
|
for (const auto &value : allValues) {
|
||||||
|
// 处理 value
|
||||||
|
switch (value.type) {
|
||||||
|
case ScriptValueType::Integer:
|
||||||
|
SDL_Log("Integer: %d", value.intValue);
|
||||||
|
break;
|
||||||
|
case ScriptValueType::Float:
|
||||||
|
SDL_Log("Float: %f", value.floatValue);
|
||||||
|
break;
|
||||||
|
case ScriptValueType::String:
|
||||||
|
SDL_Log("String: %s", value.stringValue.c_str());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.run();
|
app.run();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
add_requires("libsdl2", {configs = {shared = true,wayland = true}})
|
add_requires("libsdl2", {configs = {shared = true,wayland = true}})
|
||||||
|
add_requires("libsdl2_image")
|
||||||
add_requires("glm")
|
add_requires("glm")
|
||||||
|
|
||||||
target("Frostbite2D")
|
target("Frostbite2D")
|
||||||
@@ -12,6 +13,7 @@ target("Frostbite2D")
|
|||||||
add_includedirs(path.join(os.projectdir(), "Game/include"))
|
add_includedirs(path.join(os.projectdir(), "Game/include"))
|
||||||
|
|
||||||
add_packages("libsdl2")
|
add_packages("libsdl2")
|
||||||
|
add_packages("libsdl2_image")
|
||||||
add_packages("glm")
|
add_packages("glm")
|
||||||
|
|
||||||
-- 复制着色器文件到输出目录
|
-- 复制着色器文件到输出目录
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
set_toolchains("mingw")
|
set_toolchains("mingw")
|
||||||
|
|
||||||
add_requires("libsdl2", {configs = {shared = true}})
|
add_requires("libsdl2", {configs = {shared = true}})
|
||||||
|
add_requires("libsdl2_image", {configs = {shared = true}})
|
||||||
add_requires("glm")
|
add_requires("glm")
|
||||||
|
|
||||||
target("Frostbite2D")
|
target("Frostbite2D")
|
||||||
@@ -14,6 +15,7 @@ target("Frostbite2D")
|
|||||||
add_includedirs(path.join(os.projectdir(), "Game/include"))
|
add_includedirs(path.join(os.projectdir(), "Game/include"))
|
||||||
|
|
||||||
add_packages("libsdl2")
|
add_packages("libsdl2")
|
||||||
|
add_packages("libsdl2_image")
|
||||||
add_packages("glm")
|
add_packages("glm")
|
||||||
|
|
||||||
-- 复制 assets 目录到输出目录
|
-- 复制 assets 目录到输出目录
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ target("Frostbite2D")
|
|||||||
add_includedirs(path.join(devkitPro, "portlibs/switch/include/SDL2"))
|
add_includedirs(path.join(devkitPro, "portlibs/switch/include/SDL2"))
|
||||||
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))
|
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))
|
||||||
|
|
||||||
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
|
add_syslinks("SDL2_mixer", "SDL2_image", "SDL2", "webp", "png", "jpeg", "z", "opusfile", "opus", "vorbisidec", "ogg",
|
||||||
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
|
||||||
{public = true})
|
{public = true})
|
||||||
add_syslinks("nx", "m")
|
add_syslinks("nx", "m")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user