refactor(config): 重构配置系统并添加模块化架构
feat(config): 添加新的配置管理器、加载器和平台检测器 feat(module): 实现模块注册表和初始化系统 refactor(window): 将窗口配置迁移到新的配置系统 refactor(input): 重构输入系统以支持模块化 refactor(render): 更新渲染系统以适配新架构 refactor(graphics): 移除冗余API并简化渲染目标接口 refactor(app): 重构应用类以整合新的配置和模块系统 refactor(color): 移除废弃的ColorConstants结构 style: 清理代码并修复格式问题
This commit is contained in:
parent
9439e200d7
commit
34fe0bafcb
|
|
@ -1,6 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <extra2d/platform/iwindow.h>
|
||||
#include <string>
|
||||
|
|
@ -15,36 +19,18 @@ class EventDispatcher;
|
|||
class Camera;
|
||||
class ViewportAdapter;
|
||||
|
||||
/**
|
||||
* @brief 平台类型
|
||||
*/
|
||||
enum class PlatformType { Auto = 0, PC, Switch };
|
||||
|
||||
/**
|
||||
* @brief 应用配置
|
||||
*/
|
||||
struct AppConfig {
|
||||
std::string title = "Extra2D Application";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
bool fullscreen = false;
|
||||
bool resizable = true;
|
||||
bool vsync = true;
|
||||
int fpsLimit = 0;
|
||||
BackendType renderBackend = BackendType::OpenGL;
|
||||
int msaaSamples = 0;
|
||||
PlatformType platform = PlatformType::Auto;
|
||||
bool enableCursors = true;
|
||||
bool enableDpiScale = false;
|
||||
|
||||
std::string backend = "sdl2";
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Application 单例 - 应用主控
|
||||
*
|
||||
* 负责管理应用程序的整个生命周期,包括初始化、主循环、渲染和关闭。
|
||||
* 集成了 ConfigManager 和 ModuleRegistry 系统进行配置和模块管理。
|
||||
*/
|
||||
class Application {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* @return Application 实例引用
|
||||
*/
|
||||
static Application& get();
|
||||
|
||||
Application(const Application&) = delete;
|
||||
|
|
@ -52,57 +38,175 @@ public:
|
|||
|
||||
/**
|
||||
* @brief 使用默认配置初始化
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool init();
|
||||
|
||||
/**
|
||||
* @brief 使用配置结构初始化
|
||||
* @param config 应用配置
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool init(const AppConfig& config);
|
||||
|
||||
/**
|
||||
* @brief 从配置文件初始化
|
||||
* @param path 配置文件路径(支持 .json 和 .ini)
|
||||
* @param configPath 配置文件路径(支持 .json 和 .ini)
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool init(const std::string& path);
|
||||
bool init(const std::string& configPath);
|
||||
|
||||
/**
|
||||
* @brief 关闭应用程序
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* @brief 运行主循环
|
||||
*/
|
||||
void run();
|
||||
|
||||
/**
|
||||
* @brief 退出应用程序
|
||||
*/
|
||||
void quit();
|
||||
|
||||
/**
|
||||
* @brief 暂停应用程序
|
||||
*/
|
||||
void pause();
|
||||
|
||||
/**
|
||||
* @brief 恢复应用程序
|
||||
*/
|
||||
void resume();
|
||||
|
||||
/**
|
||||
* @brief 检查是否暂停
|
||||
* @return 暂停状态返回 true
|
||||
*/
|
||||
bool isPaused() const { return paused_; }
|
||||
|
||||
/**
|
||||
* @brief 检查是否运行中
|
||||
* @return 运行中返回 true
|
||||
*/
|
||||
bool isRunning() const { return running_; }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口接口
|
||||
* @return 窗口接口引用
|
||||
*/
|
||||
IWindow& window() { return *window_; }
|
||||
|
||||
/**
|
||||
* @brief 获取渲染后端
|
||||
* @return 渲染后端引用
|
||||
*/
|
||||
RenderBackend& renderer() { return *renderer_; }
|
||||
|
||||
/**
|
||||
* @brief 获取输入接口
|
||||
* @return 输入接口引用
|
||||
*/
|
||||
IInput& input();
|
||||
|
||||
/**
|
||||
* @brief 获取场景管理器
|
||||
* @return 场景管理器引用
|
||||
*/
|
||||
SceneManager& scenes();
|
||||
|
||||
/**
|
||||
* @brief 获取定时器管理器
|
||||
* @return 定时器管理器引用
|
||||
*/
|
||||
TimerManager& timers();
|
||||
|
||||
/**
|
||||
* @brief 获取事件队列
|
||||
* @return 事件队列引用
|
||||
*/
|
||||
EventQueue& eventQueue();
|
||||
|
||||
/**
|
||||
* @brief 获取事件分发器
|
||||
* @return 事件分发器引用
|
||||
*/
|
||||
EventDispatcher& eventDispatcher();
|
||||
|
||||
/**
|
||||
* @brief 获取相机
|
||||
* @return 相机引用
|
||||
*/
|
||||
Camera& camera();
|
||||
|
||||
/**
|
||||
* @brief 获取视口适配器
|
||||
* @return 视口适配器引用
|
||||
*/
|
||||
ViewportAdapter& viewportAdapter();
|
||||
|
||||
/**
|
||||
* @brief 进入场景
|
||||
* @param scene 场景指针
|
||||
*/
|
||||
void enterScene(Ptr<class Scene> scene);
|
||||
|
||||
/**
|
||||
* @brief 获取帧时间
|
||||
* @return 帧时间(秒)
|
||||
*/
|
||||
float deltaTime() const { return deltaTime_; }
|
||||
|
||||
/**
|
||||
* @brief 获取总运行时间
|
||||
* @return 总运行时间(秒)
|
||||
*/
|
||||
float totalTime() const { return totalTime_; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前帧率
|
||||
* @return 帧率(FPS)
|
||||
*/
|
||||
int fps() const { return currentFps_; }
|
||||
|
||||
const AppConfig& getConfig() const { return config_; }
|
||||
/**
|
||||
* @brief 获取配置管理器
|
||||
* @return 配置管理器引用
|
||||
*/
|
||||
ConfigManager& config();
|
||||
|
||||
/**
|
||||
* @brief 获取应用配置
|
||||
* @return 应用配置常量引用
|
||||
*/
|
||||
const AppConfig& getConfig() const;
|
||||
|
||||
private:
|
||||
Application() = default;
|
||||
~Application();
|
||||
|
||||
/**
|
||||
* @brief 内部初始化实现
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initImpl();
|
||||
void mainLoop();
|
||||
void update();
|
||||
void render();
|
||||
|
||||
AppConfig config_;
|
||||
/**
|
||||
* @brief 主循环
|
||||
*/
|
||||
void mainLoop();
|
||||
|
||||
/**
|
||||
* @brief 更新逻辑
|
||||
*/
|
||||
void update();
|
||||
|
||||
/**
|
||||
* @brief 渲染
|
||||
*/
|
||||
void render();
|
||||
|
||||
UniquePtr<IWindow> window_;
|
||||
UniquePtr<RenderBackend> renderer_;
|
||||
|
|
@ -124,6 +228,10 @@ private:
|
|||
int frameCount_ = 0;
|
||||
float fpsTimer_ = 0.0f;
|
||||
int currentFps_ = 0;
|
||||
|
||||
ModuleId windowModuleId_ = INVALID_MODULE_ID;
|
||||
ModuleId inputModuleId_ = INVALID_MODULE_ID;
|
||||
ModuleId renderModuleId_ = INVALID_MODULE_ID;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 窗口模式枚举
|
||||
// ============================================================================
|
||||
enum class WindowMode {
|
||||
Windowed,
|
||||
Fullscreen,
|
||||
Borderless
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 窗口配置数据
|
||||
// ============================================================================
|
||||
struct WindowConfigData {
|
||||
std::string title = "Extra2D Application";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
int minWidth = 320;
|
||||
int minHeight = 240;
|
||||
int maxWidth = 0;
|
||||
int maxHeight = 0;
|
||||
WindowMode mode = WindowMode::Windowed;
|
||||
bool resizable = true;
|
||||
bool borderless = false;
|
||||
bool alwaysOnTop = false;
|
||||
bool centered = true;
|
||||
int posX = -1;
|
||||
int posY = -1;
|
||||
bool hideOnClose = false;
|
||||
bool minimizeOnClose = true;
|
||||
float opacity = 1.0f;
|
||||
bool transparentFramebuffer = false;
|
||||
bool highDPI = true;
|
||||
float contentScale = 1.0f;
|
||||
bool vsync = true;
|
||||
int multisamples = 0;
|
||||
bool visible = true;
|
||||
bool decorated = true;
|
||||
|
||||
bool isSizeValid() const { return width > 0 && height > 0; }
|
||||
bool hasPosition() const { return posX >= 0 && posY >= 0; }
|
||||
float aspectRatio() const { return static_cast<float>(width) / static_cast<float>(height); }
|
||||
bool isFullscreen() const { return mode == WindowMode::Fullscreen; }
|
||||
bool isBorderless() const { return mode == WindowMode::Borderless || borderless; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 渲染配置数据
|
||||
// ============================================================================
|
||||
struct RenderConfigData {
|
||||
BackendType backend = BackendType::OpenGL;
|
||||
int targetFPS = 60;
|
||||
bool vsync = true;
|
||||
bool tripleBuffering = false;
|
||||
int multisamples = 0;
|
||||
bool sRGBFramebuffer = false;
|
||||
Color clearColor{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
int maxTextureSize = 0;
|
||||
int textureAnisotropy = 1;
|
||||
bool wireframeMode = false;
|
||||
bool depthTest = false;
|
||||
bool blending = true;
|
||||
bool dithering = false;
|
||||
int spriteBatchSize = 1000;
|
||||
int maxRenderTargets = 1;
|
||||
bool allowShaderHotReload = false;
|
||||
std::string shaderCachePath;
|
||||
|
||||
bool isMultisampleEnabled() const { return multisamples > 0; }
|
||||
bool isFPSCapped() const { return targetFPS > 0; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 音频配置数据
|
||||
// ============================================================================
|
||||
struct AudioConfigData {
|
||||
bool enabled = true;
|
||||
int masterVolume = 100;
|
||||
int musicVolume = 100;
|
||||
int sfxVolume = 100;
|
||||
int voiceVolume = 100;
|
||||
int ambientVolume = 100;
|
||||
int frequency = 44100;
|
||||
int channels = 2;
|
||||
int chunkSize = 2048;
|
||||
int maxChannels = 16;
|
||||
bool spatialAudio = false;
|
||||
float listenerPosition[3] = {0.0f, 0.0f, 0.0f};
|
||||
|
||||
bool isValidVolume(int volume) const { return volume >= 0 && volume <= 100; }
|
||||
float volumeToFloat(int volume) const { return static_cast<float>(volume) / 100.0f; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 调试配置数据
|
||||
// ============================================================================
|
||||
struct DebugConfigData {
|
||||
bool enabled = false;
|
||||
bool showFPS = false;
|
||||
bool showMemoryUsage = false;
|
||||
bool showRenderStats = false;
|
||||
bool showColliders = false;
|
||||
bool showGrid = false;
|
||||
bool logToFile = false;
|
||||
bool logToConsole = true;
|
||||
int logLevel = 2;
|
||||
bool breakOnAssert = true;
|
||||
bool enableProfiling = false;
|
||||
std::string logFilePath;
|
||||
std::vector<std::string> debugFlags;
|
||||
|
||||
bool hasDebugFlag(const std::string& flag) const;
|
||||
void addDebugFlag(const std::string& flag);
|
||||
void removeDebugFlag(const std::string& flag);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 输入配置数据
|
||||
// ============================================================================
|
||||
struct InputConfigData {
|
||||
bool enabled = true;
|
||||
bool rawMouseInput = false;
|
||||
float mouseSensitivity = 1.0f;
|
||||
bool invertMouseY = false;
|
||||
bool invertMouseX = false;
|
||||
float deadzone = 0.15f;
|
||||
float triggerThreshold = 0.5f;
|
||||
bool enableVibration = true;
|
||||
int maxGamepads = 4;
|
||||
bool autoConnectGamepads = true;
|
||||
std::string gamepadMappingFile;
|
||||
|
||||
bool isDeadzoneValid() const { return deadzone >= 0.0f && deadzone <= 1.0f; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 资源配置数据
|
||||
// ============================================================================
|
||||
struct ResourceConfigData {
|
||||
std::string assetRootPath = "assets";
|
||||
std::string cachePath = "cache";
|
||||
std::string savePath = "saves";
|
||||
std::string configPath = "config";
|
||||
std::string logPath = "logs";
|
||||
bool useAssetCache = true;
|
||||
int maxCacheSize = 512;
|
||||
bool hotReloadEnabled = false;
|
||||
float hotReloadInterval = 1.0f;
|
||||
bool compressTextures = false;
|
||||
bool preloadCommonAssets = true;
|
||||
std::vector<std::string> searchPaths;
|
||||
|
||||
void addSearchPath(const std::string& path);
|
||||
void removeSearchPath(const std::string& path);
|
||||
bool hasSearchPath(const std::string& path) const;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 应用统一配置
|
||||
// ============================================================================
|
||||
struct AppConfig {
|
||||
WindowConfigData window;
|
||||
RenderConfigData render;
|
||||
AudioConfigData audio;
|
||||
DebugConfigData debug;
|
||||
InputConfigData input;
|
||||
ResourceConfigData resource;
|
||||
std::string appName = "Extra2D App";
|
||||
std::string appVersion = "1.0.0";
|
||||
std::string organization = "";
|
||||
std::string configFile = "config.json";
|
||||
PlatformType targetPlatform = PlatformType::Auto;
|
||||
|
||||
/**
|
||||
* @brief 创建默认配置
|
||||
* @return 默认的应用配置实例
|
||||
*/
|
||||
static AppConfig createDefault();
|
||||
|
||||
/**
|
||||
* @brief 验证配置的有效性
|
||||
* @return 如果配置有效返回 true,否则返回 false
|
||||
*/
|
||||
bool validate() const;
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* @param platform 平台配置接口
|
||||
*/
|
||||
void applyPlatformConstraints(const PlatformConfig& platform);
|
||||
|
||||
/**
|
||||
* @brief 重置为默认值
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/**
|
||||
* @brief 合并另一个配置(非默认值覆盖当前值)
|
||||
* @param other 要合并的配置
|
||||
*/
|
||||
void merge(const AppConfig& other);
|
||||
|
||||
/**
|
||||
* @brief 检查配置是否有效
|
||||
* @return 如果所有必要字段都有效返回 true
|
||||
*/
|
||||
bool isValid() const { return validate(); }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口宽高比
|
||||
* @return 窗口的宽高比
|
||||
*/
|
||||
float aspectRatio() const { return window.aspectRatio(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载结果
|
||||
// ============================================================================
|
||||
struct ConfigLoadResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
int errorLine = -1;
|
||||
std::string errorField;
|
||||
|
||||
static ConfigLoadResult ok() { return ConfigLoadResult{true, "", -1, ""}; }
|
||||
static ConfigLoadResult error(const std::string& msg, int line = -1, const std::string& field = "") {
|
||||
return ConfigLoadResult{false, msg, line, field};
|
||||
}
|
||||
|
||||
bool isOk() const { return success; }
|
||||
bool hasError() const { return !success; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置保存结果
|
||||
// ============================================================================
|
||||
struct ConfigSaveResult {
|
||||
bool success = false;
|
||||
std::string errorMessage;
|
||||
|
||||
static ConfigSaveResult ok() { return ConfigSaveResult{true, ""}; }
|
||||
static ConfigSaveResult error(const std::string& msg) {
|
||||
return ConfigSaveResult{false, msg};
|
||||
}
|
||||
|
||||
bool isOk() const { return success; }
|
||||
bool hasError() const { return !success; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载器抽象接口
|
||||
// ============================================================================
|
||||
class ConfigLoader {
|
||||
public:
|
||||
virtual ~ConfigLoader() = default;
|
||||
|
||||
/**
|
||||
* @brief 从文件加载配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
virtual ConfigLoadResult load(const std::string& filepath, AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
virtual ConfigSaveResult save(const std::string& filepath, const AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 从字符串加载配置
|
||||
* @param content 配置内容字符串
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
virtual ConfigLoadResult loadFromString(const std::string& content, AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 将配置序列化为字符串
|
||||
* @param config 配置对象
|
||||
* @return 序列化后的字符串
|
||||
*/
|
||||
virtual std::string saveToString(const AppConfig& config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 获取支持的文件扩展名
|
||||
* @return 文件扩展名(不含点号,如 "json")
|
||||
*/
|
||||
virtual const char* extension() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定文件
|
||||
* @param filepath 文件路径
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
virtual bool supportsFile(const std::string& filepath) const = 0;
|
||||
|
||||
/**
|
||||
* @brief 克隆加载器实例
|
||||
* @return 新的加载器实例
|
||||
*/
|
||||
virtual UniquePtr<ConfigLoader> clone() const = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// JSON 配置加载器
|
||||
// ============================================================================
|
||||
class JsonConfigLoader : public ConfigLoader {
|
||||
public:
|
||||
JsonConfigLoader() = default;
|
||||
~JsonConfigLoader() override = default;
|
||||
|
||||
ConfigLoadResult load(const std::string& filepath, AppConfig& config) override;
|
||||
ConfigSaveResult save(const std::string& filepath, const AppConfig& config) override;
|
||||
ConfigLoadResult loadFromString(const std::string& content, AppConfig& config) override;
|
||||
std::string saveToString(const AppConfig& config) override;
|
||||
const char* extension() const override { return "json"; }
|
||||
bool supportsFile(const std::string& filepath) const override;
|
||||
UniquePtr<ConfigLoader> clone() const override;
|
||||
|
||||
private:
|
||||
ConfigLoadResult parseWindowConfig(const void* jsonValue, WindowConfigData& window);
|
||||
ConfigLoadResult parseRenderConfig(const void* jsonValue, RenderConfigData& render);
|
||||
ConfigLoadResult parseAudioConfig(const void* jsonValue, AudioConfigData& audio);
|
||||
ConfigLoadResult parseDebugConfig(const void* jsonValue, DebugConfigData& debug);
|
||||
ConfigLoadResult parseInputConfig(const void* jsonValue, InputConfigData& input);
|
||||
ConfigLoadResult parseResourceConfig(const void* jsonValue, ResourceConfigData& resource);
|
||||
|
||||
void serializeWindowConfig(void* jsonValue, const WindowConfigData& window);
|
||||
void serializeRenderConfig(void* jsonValue, const RenderConfigData& render);
|
||||
void serializeAudioConfig(void* jsonValue, const AudioConfigData& audio);
|
||||
void serializeDebugConfig(void* jsonValue, const DebugConfigData& debug);
|
||||
void serializeInputConfig(void* jsonValue, const InputConfigData& input);
|
||||
void serializeResourceConfig(void* jsonValue, const ResourceConfigData& resource);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// INI 配置加载器
|
||||
// ============================================================================
|
||||
class IniConfigLoader : public ConfigLoader {
|
||||
public:
|
||||
IniConfigLoader() = default;
|
||||
~IniConfigLoader() override = default;
|
||||
|
||||
ConfigLoadResult load(const std::string& filepath, AppConfig& config) override;
|
||||
ConfigSaveResult save(const std::string& filepath, const AppConfig& config) override;
|
||||
ConfigLoadResult loadFromString(const std::string& content, AppConfig& config) override;
|
||||
std::string saveToString(const AppConfig& config) override;
|
||||
const char* extension() const override { return "ini"; }
|
||||
bool supportsFile(const std::string& filepath) const override;
|
||||
UniquePtr<ConfigLoader> clone() const override;
|
||||
|
||||
private:
|
||||
std::string sectionKey(const std::string& section, const std::string& key) const;
|
||||
ConfigLoadResult parseInt(const std::string& value, int& result, const std::string& fieldName);
|
||||
ConfigLoadResult parseFloat(const std::string& value, float& result, const std::string& fieldName);
|
||||
ConfigLoadResult parseBool(const std::string& value, bool& result, const std::string& fieldName);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置加载器工厂
|
||||
// ============================================================================
|
||||
class ConfigLoaderFactory {
|
||||
public:
|
||||
/**
|
||||
* @brief 根据文件扩展名创建加载器
|
||||
* @param extension 文件扩展名(不含点号)
|
||||
* @return 配置加载器实例,如果不支持返回 nullptr
|
||||
*/
|
||||
static UniquePtr<ConfigLoader> create(const std::string& extension);
|
||||
|
||||
/**
|
||||
* @brief 根据文件路径创建加载器
|
||||
* @param filepath 文件路径
|
||||
* @return 配置加载器实例,如果不支持返回 nullptr
|
||||
*/
|
||||
static UniquePtr<ConfigLoader> createForFile(const std::string& filepath);
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定扩展名
|
||||
* @param extension 文件扩展名
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
static bool isExtensionSupported(const std::string& extension);
|
||||
|
||||
/**
|
||||
* @brief 获取所有支持的扩展名
|
||||
* @return 支持的扩展名列表
|
||||
*/
|
||||
static std::vector<std::string> getSupportedExtensions();
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 配置变更事件
|
||||
// ============================================================================
|
||||
struct ConfigChangeEvent {
|
||||
std::string section;
|
||||
std::string field;
|
||||
std::string oldValue;
|
||||
std::string newValue;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 配置变更回调类型
|
||||
// ============================================================================
|
||||
using ConfigChangeCallback = Function<void(const ConfigChangeEvent &)>;
|
||||
|
||||
// ============================================================================
|
||||
// 配置管理器(单例)
|
||||
// ============================================================================
|
||||
class ConfigManager {
|
||||
public:
|
||||
using ModuleConfigPtr = Ptr<void>;
|
||||
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* @return 配置管理器实例引用
|
||||
*/
|
||||
static ConfigManager &instance();
|
||||
|
||||
/**
|
||||
* @brief 初始化配置管理器
|
||||
* @param configPath 配置文件路径
|
||||
* @return 如果初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const std::string &configPath = "config.json");
|
||||
|
||||
/**
|
||||
* @brief 关闭配置管理器
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* @brief 检查是否已初始化
|
||||
* @return 如果已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const;
|
||||
|
||||
/**
|
||||
* @brief 加载配置文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult loadConfig(const std::string &filepath = "");
|
||||
|
||||
/**
|
||||
* @brief 保存配置到文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult saveConfig(const std::string &filepath = "");
|
||||
|
||||
/**
|
||||
* @brief 重新加载配置
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult reload();
|
||||
|
||||
/**
|
||||
* @brief 获取应用配置
|
||||
* @return 应用配置的常量引用
|
||||
*/
|
||||
const AppConfig &appConfig() const;
|
||||
|
||||
/**
|
||||
* @brief 获取可修改的应用配置
|
||||
* @return 应用配置的引用
|
||||
*/
|
||||
AppConfig &appConfig();
|
||||
|
||||
/**
|
||||
* @brief 设置应用配置
|
||||
* @param config 新的配置
|
||||
*/
|
||||
void setAppConfig(const AppConfig &config);
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置
|
||||
* @return 平台配置接口指针
|
||||
*/
|
||||
PlatformConfig *platformConfig();
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置(常量版本)
|
||||
* @return 平台配置接口常量指针
|
||||
*/
|
||||
const PlatformConfig *platformConfig() const;
|
||||
|
||||
/**
|
||||
* @brief 注册配置变更回调
|
||||
* @param callback 回调函数
|
||||
* @return 回调ID,用于取消注册
|
||||
*/
|
||||
int registerChangeCallback(ConfigChangeCallback callback);
|
||||
|
||||
/**
|
||||
* @brief 取消注册配置变更回调
|
||||
* @param callbackId 回调ID
|
||||
*/
|
||||
void unregisterChangeCallback(int callbackId);
|
||||
|
||||
/**
|
||||
* @brief 清除所有变更回调
|
||||
*/
|
||||
void clearChangeCallbacks();
|
||||
|
||||
/**
|
||||
* @brief 注册模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @param config 模块配置指针
|
||||
*/
|
||||
void registerModuleConfig(const std::string &moduleName, Ptr<void> config);
|
||||
|
||||
/**
|
||||
* @brief 获取模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @return 模块配置指针
|
||||
*/
|
||||
template <typename T>
|
||||
Ptr<T> getModuleConfig(const std::string &moduleName) const {
|
||||
auto it = m_moduleConfigs.find(moduleName);
|
||||
if (it != m_moduleConfigs.end()) {
|
||||
return std::static_pointer_cast<T>(it->second);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除模块配置
|
||||
* @param moduleName 模块名称
|
||||
*/
|
||||
void removeModuleConfig(const std::string &moduleName);
|
||||
|
||||
/**
|
||||
* @brief 检查模块配置是否存在
|
||||
* @param moduleName 模块名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool hasModuleConfig(const std::string &moduleName) const;
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void setValue(const std::string §ion, const std::string &key,
|
||||
const std::string &value);
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void setValue(const std::string §ion, const std::string &key, int value);
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void setValue(const std::string §ion, const std::string &key,
|
||||
float value);
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void setValue(const std::string §ion, const std::string &key, bool value);
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
std::string getValue(const std::string §ion, const std::string &key,
|
||||
const std::string &defaultValue = "") const;
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
int getIntValue(const std::string §ion, const std::string &key,
|
||||
int defaultValue = 0) const;
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
float getFloatValue(const std::string §ion, const std::string &key,
|
||||
float defaultValue = 0.0f) const;
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
bool getBoolValue(const std::string §ion, const std::string &key,
|
||||
bool defaultValue = false) const;
|
||||
|
||||
/**
|
||||
* @brief 重置配置到默认值
|
||||
*/
|
||||
void resetToDefaults();
|
||||
|
||||
/**
|
||||
* @brief 检查配置是否有未保存的更改
|
||||
* @return 如果有未保存的更改返回 true
|
||||
*/
|
||||
bool hasUnsavedChanges() const;
|
||||
|
||||
/**
|
||||
* @brief 标记配置为已修改
|
||||
*/
|
||||
void markModified();
|
||||
|
||||
/**
|
||||
* @brief 清除修改标记
|
||||
*/
|
||||
void clearModified();
|
||||
|
||||
/**
|
||||
* @brief 获取配置文件路径
|
||||
* @return 配置文件路径
|
||||
*/
|
||||
const std::string &configPath() const { return m_configPath; }
|
||||
|
||||
/**
|
||||
* @brief 设置自动保存
|
||||
* @param enabled 是否启用自动保存
|
||||
* @param interval 自动保存间隔(秒)
|
||||
*/
|
||||
void setAutoSave(bool enabled, float interval = 30.0f);
|
||||
|
||||
/**
|
||||
* @brief 检查是否启用自动保存
|
||||
* @return 如果启用自动保存返回 true
|
||||
*/
|
||||
bool isAutoSaveEnabled() const { return m_autoSaveEnabled; }
|
||||
|
||||
/**
|
||||
* @brief 更新配置管理器(用于自动保存)
|
||||
* @param deltaTime 帧时间(秒)
|
||||
*/
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
ConfigManager();
|
||||
~ConfigManager();
|
||||
ConfigManager(const ConfigManager &) = delete;
|
||||
ConfigManager &operator=(const ConfigManager &) = delete;
|
||||
|
||||
void notifyChangeCallbacks(const ConfigChangeEvent &event);
|
||||
void applyConfigToInternal(const AppConfig &config);
|
||||
void extractConfigFromInternal(AppConfig &config) const;
|
||||
|
||||
AppConfig m_appConfig;
|
||||
UniquePtr<PlatformConfig> m_platformConfig;
|
||||
UniquePtr<ConfigLoader> m_loader;
|
||||
std::string m_configPath;
|
||||
bool m_initialized = false;
|
||||
bool m_modified = false;
|
||||
mutable std::mutex m_mutex;
|
||||
|
||||
std::unordered_map<int, ConfigChangeCallback> m_changeCallbacks;
|
||||
int m_nextCallbackId = 1;
|
||||
|
||||
std::unordered_map<std::string, ModuleConfigPtr> m_moduleConfigs;
|
||||
|
||||
bool m_autoSaveEnabled = false;
|
||||
float m_autoSaveInterval = 30.0f;
|
||||
float m_autoSaveTimer = 0.0f;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 便捷宏定义
|
||||
// ============================================================================
|
||||
#define CONFIG_MANAGER ConfigManager::instance()
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 模块标识符类型
|
||||
*/
|
||||
using ModuleId = uint32_t;
|
||||
|
||||
/**
|
||||
* @brief 无效模块标识符常量
|
||||
*/
|
||||
constexpr ModuleId INVALID_MODULE_ID = 0;
|
||||
|
||||
/**
|
||||
* @brief 模块优先级枚举
|
||||
* 定义模块的初始化顺序,数值越小越先初始化
|
||||
*/
|
||||
enum class ModulePriority : int {
|
||||
Core = 0, ///< 核心模块(最先初始化)
|
||||
Platform = 100, ///< 平台相关模块
|
||||
Graphics = 200, ///< 图形渲染模块
|
||||
Audio = 300, ///< 音频模块
|
||||
Input = 400, ///< 输入模块
|
||||
Resource = 500, ///< 资源管理模块
|
||||
Game = 1000, ///< 游戏逻辑模块
|
||||
User = 2000 ///< 用户自定义模块
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 模块信息结构体
|
||||
* 包含模块的基本信息
|
||||
*/
|
||||
struct ModuleInfo {
|
||||
ModuleId id = INVALID_MODULE_ID; ///< 模块唯一标识符
|
||||
std::string name; ///< 模块名称
|
||||
std::string version; ///< 模块版本号
|
||||
ModulePriority priority = ModulePriority::User; ///< 模块优先级
|
||||
bool enabled = true; ///< 是否启用
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 模块配置接口
|
||||
* 所有模块配置类必须实现此接口
|
||||
*/
|
||||
class IModuleConfig {
|
||||
public:
|
||||
/**
|
||||
* @brief 虚析构函数
|
||||
*/
|
||||
virtual ~IModuleConfig() = default;
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
* @return 模块信息结构体
|
||||
*/
|
||||
virtual ModuleInfo getModuleInfo() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 获取配置节名称
|
||||
* 用于配置文件中的节名
|
||||
* @return 配置节名称字符串
|
||||
*/
|
||||
virtual std::string getConfigSectionName() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
virtual bool validate() const { return true; }
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
virtual void applyPlatformConstraints(PlatformType platform) { }
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
virtual void resetToDefaults() = 0;
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
virtual bool loadFromJson(const void* jsonData) { return true; }
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
virtual bool saveToJson(void* jsonData) const { return true; }
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 模块初始化器接口
|
||||
* 所有模块初始化器必须实现此接口
|
||||
*/
|
||||
class IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 虚析构函数
|
||||
*/
|
||||
virtual ~IModuleInitializer() = default;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
virtual ModuleId getModuleId() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
virtual ModulePriority getPriority() const = 0;
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* 返回此模块依赖的其他模块标识符
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
virtual std::vector<ModuleId> getDependencies() const { return {}; }
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
virtual bool initialize(const IModuleConfig* config) = 0;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
virtual bool isInitialized() const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 模块初始化器工厂函数类型
|
||||
*/
|
||||
using ModuleInitializerFactory = std::function<UniquePtr<IModuleInitializer>()>;
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 模块注册表条目结构体
|
||||
* 存储模块的配置和初始化器工厂
|
||||
*/
|
||||
struct ModuleEntry {
|
||||
ModuleId id; ///< 模块标识符
|
||||
UniquePtr<IModuleConfig> config; ///< 模块配置
|
||||
ModuleInitializerFactory initializerFactory;///< 初始化器工厂函数
|
||||
bool initialized = false; ///< 是否已初始化
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 模块注册表类
|
||||
* 单例模式,管理所有模块的注册、查询和初始化顺序
|
||||
*/
|
||||
class ModuleRegistry {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* @return 模块注册表实例引用
|
||||
*/
|
||||
static ModuleRegistry& instance();
|
||||
|
||||
/**
|
||||
* @brief 禁止拷贝构造
|
||||
*/
|
||||
ModuleRegistry(const ModuleRegistry&) = delete;
|
||||
|
||||
/**
|
||||
* @brief 禁止赋值操作
|
||||
*/
|
||||
ModuleRegistry& operator=(const ModuleRegistry&) = delete;
|
||||
|
||||
/**
|
||||
* @brief 注册模块
|
||||
* @param config 模块配置
|
||||
* @param initializerFactory 初始化器工厂函数(可选)
|
||||
* @return 分配的模块标识符
|
||||
*/
|
||||
ModuleId registerModule(
|
||||
UniquePtr<IModuleConfig> config,
|
||||
ModuleInitializerFactory initializerFactory = nullptr
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief 注销模块
|
||||
* @param id 模块标识符
|
||||
* @return 注销成功返回 true
|
||||
*/
|
||||
bool unregisterModule(ModuleId id);
|
||||
|
||||
/**
|
||||
* @brief 获取模块配置
|
||||
* @param id 模块标识符
|
||||
* @return 模块配置指针,不存在返回 nullptr
|
||||
*/
|
||||
IModuleConfig* getModuleConfig(ModuleId id) const;
|
||||
|
||||
/**
|
||||
* @brief 根据名称获取模块配置
|
||||
* @param name 模块名称
|
||||
* @return 模块配置指针,不存在返回 nullptr
|
||||
*/
|
||||
IModuleConfig* getModuleConfigByName(const std::string& name) const;
|
||||
|
||||
/**
|
||||
* @brief 创建模块初始化器
|
||||
* @param id 模块标识符
|
||||
* @return 初始化器实例,不存在返回 nullptr
|
||||
*/
|
||||
UniquePtr<IModuleInitializer> createInitializer(ModuleId id) const;
|
||||
|
||||
/**
|
||||
* @brief 获取所有已注册模块标识符
|
||||
* @return 模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getAllModules() const;
|
||||
|
||||
/**
|
||||
* @brief 获取模块初始化顺序
|
||||
* 根据优先级和依赖关系计算初始化顺序
|
||||
* @return 按初始化顺序排列的模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getInitializationOrder() const;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否存在
|
||||
* @param id 模块标识符
|
||||
* @return 存在返回 true
|
||||
*/
|
||||
bool hasModule(ModuleId id) const;
|
||||
|
||||
/**
|
||||
* @brief 清空所有注册的模块
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief 获取已注册模块数量
|
||||
* @return 模块数量
|
||||
*/
|
||||
size_t size() const { return modules_.size(); }
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 私有构造函数
|
||||
*/
|
||||
ModuleRegistry() = default;
|
||||
|
||||
/**
|
||||
* @brief 私有析构函数
|
||||
*/
|
||||
~ModuleRegistry() = default;
|
||||
|
||||
/**
|
||||
* @brief 生成新的模块标识符
|
||||
* @return 新的模块标识符
|
||||
*/
|
||||
ModuleId generateId();
|
||||
|
||||
std::unordered_map<ModuleId, ModuleEntry> modules_; ///< 模块注册表
|
||||
std::unordered_map<std::string, ModuleId> nameToId_; ///< 名称到标识符映射
|
||||
mutable std::mutex mutex_; ///< 线程安全互斥锁
|
||||
ModuleId nextId_ = 1; ///< 下一个可用标识符
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 模块注册宏
|
||||
* 在全局作用域使用此宏注册模块
|
||||
*
|
||||
* @example
|
||||
* E2D_REGISTER_MODULE(MyModuleConfig, MyModuleInitializer)
|
||||
*/
|
||||
#define E2D_REGISTER_MODULE(ConfigClass, InitializerClass) \
|
||||
namespace { \
|
||||
static const ::extra2d::ModuleId E2D_ANONYMOUS_VAR(module_id_) = \
|
||||
::extra2d::ModuleRegistry::instance().registerModule( \
|
||||
::extra2d::makeUnique<ConfigClass>(), \
|
||||
[]() -> ::extra2d::UniquePtr<::extra2d::IModuleInitializer> { \
|
||||
return ::extra2d::makeUnique<InitializerClass>(); \
|
||||
} \
|
||||
); \
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/core/types.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 平台类型枚举
|
||||
// ============================================================================
|
||||
enum class PlatformType {
|
||||
Auto,
|
||||
Windows,
|
||||
Switch,
|
||||
Linux,
|
||||
macOS
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台能力结构
|
||||
// ============================================================================
|
||||
struct PlatformCapabilities {
|
||||
bool supportsWindowed = true;
|
||||
bool supportsFullscreen = true;
|
||||
bool supportsBorderless = true;
|
||||
bool supportsCursor = true;
|
||||
bool supportsCursorHide = true;
|
||||
bool supportsDPIAwareness = true;
|
||||
bool supportsVSync = true;
|
||||
bool supportsMultiMonitor = true;
|
||||
bool supportsClipboard = true;
|
||||
bool supportsGamepad = true;
|
||||
bool supportsTouch = false;
|
||||
bool supportsKeyboard = true;
|
||||
bool supportsMouse = true;
|
||||
bool supportsResize = true;
|
||||
bool supportsHighDPI = true;
|
||||
int maxTextureSize = 16384;
|
||||
int preferredScreenWidth = 1920;
|
||||
int preferredScreenHeight = 1080;
|
||||
float defaultDPI = 96.0f;
|
||||
|
||||
bool hasWindowSupport() const { return supportsWindowed || supportsFullscreen || supportsBorderless; }
|
||||
bool hasInputSupport() const { return supportsKeyboard || supportsMouse || supportsGamepad || supportsTouch; }
|
||||
bool isDesktop() const { return supportsKeyboard && supportsMouse && supportsWindowed; }
|
||||
bool isConsole() const { return !supportsWindowed && supportsGamepad; }
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台配置抽象接口
|
||||
// ============================================================================
|
||||
class PlatformConfig {
|
||||
public:
|
||||
virtual ~PlatformConfig() = default;
|
||||
|
||||
virtual PlatformType platformType() const = 0;
|
||||
virtual const char* platformName() const = 0;
|
||||
virtual const PlatformCapabilities& capabilities() const = 0;
|
||||
virtual void applyConstraints(struct AppConfig& config) const = 0;
|
||||
virtual void applyDefaults(struct AppConfig& config) const = 0;
|
||||
virtual bool validateConfig(struct AppConfig& config) const = 0;
|
||||
|
||||
virtual int getRecommendedWidth() const = 0;
|
||||
virtual int getRecommendedHeight() const = 0;
|
||||
virtual bool isResolutionSupported(int width, int height) const = 0;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 平台配置工厂函数声明
|
||||
// ============================================================================
|
||||
UniquePtr<PlatformConfig> createPlatformConfig(PlatformType type = PlatformType::Auto);
|
||||
|
||||
const char* getPlatformTypeName(PlatformType type);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/core/types.h>
|
||||
#include <string>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 平台检测器工具类
|
||||
// ============================================================================
|
||||
class PlatformDetector {
|
||||
public:
|
||||
/**
|
||||
* @brief 检测当前运行平台
|
||||
* @return 当前平台的类型
|
||||
*/
|
||||
static PlatformType detect();
|
||||
|
||||
/**
|
||||
* @brief 获取平台名称字符串
|
||||
* @return 平台名称(如 "Windows", "Linux", "macOS", "Switch")
|
||||
*/
|
||||
static const char* platformName();
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台类型的名称
|
||||
* @param type 平台类型
|
||||
* @return 平台名称字符串
|
||||
*/
|
||||
static const char* platformName(PlatformType type);
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为桌面平台
|
||||
* @return 如果是桌面平台返回 true
|
||||
*/
|
||||
static bool isDesktopPlatform();
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为游戏主机平台
|
||||
* @return 如果是游戏主机平台返回 true
|
||||
*/
|
||||
static bool isConsolePlatform();
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为移动平台
|
||||
* @return 如果是移动平台返回 true
|
||||
*/
|
||||
static bool isMobilePlatform();
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的能力
|
||||
* @return 平台能力结构
|
||||
*/
|
||||
static PlatformCapabilities capabilities();
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台的能力
|
||||
* @param type 平台类型
|
||||
* @return 平台能力结构
|
||||
*/
|
||||
static PlatformCapabilities capabilities(PlatformType type);
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的默认配置
|
||||
* @return 平台默认的应用配置
|
||||
*/
|
||||
static AppConfig platformDefaults();
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台的默认配置
|
||||
* @param type 平台类型
|
||||
* @return 平台默认的应用配置
|
||||
*/
|
||||
static AppConfig platformDefaults(PlatformType type);
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的推荐分辨率
|
||||
* @param width 输出宽度
|
||||
* @param height 输出高度
|
||||
*/
|
||||
static void getRecommendedResolution(int& width, int& height);
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的默认 DPI
|
||||
* @return 默认 DPI 值
|
||||
*/
|
||||
static float getDefaultDPI();
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否支持指定功能
|
||||
* @param feature 功能名称
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
static bool supportsFeature(const std::string& feature);
|
||||
|
||||
/**
|
||||
* @brief 获取系统内存大小
|
||||
* @return 系统内存大小(MB),如果无法获取返回 0
|
||||
*/
|
||||
static int getSystemMemoryMB();
|
||||
|
||||
/**
|
||||
* @brief 获取 CPU 核心数
|
||||
* @return CPU 核心数
|
||||
*/
|
||||
static int getCPUCoreCount();
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持多线程渲染
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
static bool supportsMultithreadedRendering();
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的配置路径
|
||||
* @param appName 应用名称
|
||||
* @return 配置文件目录路径
|
||||
*/
|
||||
static std::string getConfigPath(const std::string& appName);
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的存档路径
|
||||
* @param appName 应用名称
|
||||
* @return 存档文件目录路径
|
||||
*/
|
||||
static std::string getSavePath(const std::string& appName);
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的缓存路径
|
||||
* @param appName 应用名称
|
||||
* @return 缓存文件目录路径
|
||||
*/
|
||||
static std::string getCachePath(const std::string& appName);
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的日志路径
|
||||
* @param appName 应用名称
|
||||
* @return 日志文件目录路径
|
||||
*/
|
||||
static std::string getLogPath(const std::string& appName);
|
||||
|
||||
/**
|
||||
* @brief 检查平台是否为小端字节序
|
||||
* @return 如果是小端字节序返回 true
|
||||
*/
|
||||
static bool isLittleEndian();
|
||||
|
||||
/**
|
||||
* @brief 检查平台是否为大端字节序
|
||||
* @return 如果是大端字节序返回 true
|
||||
*/
|
||||
static bool isBigEndian();
|
||||
|
||||
/**
|
||||
* @brief 获取平台信息摘要
|
||||
* @return 平台信息字符串
|
||||
*/
|
||||
static std::string getPlatformSummary();
|
||||
|
||||
private:
|
||||
static PlatformCapabilities getWindowsCapabilities();
|
||||
static PlatformCapabilities getLinuxCapabilities();
|
||||
static PlatformCapabilities getMacOSCapabilities();
|
||||
static PlatformCapabilities getSwitchCapabilities();
|
||||
|
||||
static AppConfig getWindowsDefaults();
|
||||
static AppConfig getLinuxDefaults();
|
||||
static AppConfig getMacOSDefaults();
|
||||
static AppConfig getSwitchDefaults();
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -155,17 +155,4 @@ inline constexpr Color Coral{1.0f, 0.498f, 0.314f, 1.0f};
|
|||
inline constexpr Color Transparent{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
} // namespace Colors
|
||||
|
||||
// 为了向后兼容,在 Color 结构体内提供静态引用
|
||||
struct ColorConstants {
|
||||
static const Color &White;
|
||||
static const Color &Black;
|
||||
static const Color &Red;
|
||||
static const Color &Green;
|
||||
static const Color &Blue;
|
||||
static const Color &Yellow;
|
||||
static const Color &Cyan;
|
||||
static const Color &Magenta;
|
||||
static const Color &Transparent;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -8,6 +8,16 @@
|
|||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
// Config
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/config/platform_detector.h>
|
||||
|
||||
// Platform
|
||||
#include <extra2d/platform/iinput.h>
|
||||
#include <extra2d/platform/iwindow.h>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/graphics/render_backend.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 渲染模块配置
|
||||
* 实现 IModuleConfig 接口
|
||||
*/
|
||||
class RenderModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
BackendType backend = BackendType::OpenGL;
|
||||
bool vsync = true;
|
||||
int targetFPS = 60;
|
||||
int multisamples = 0;
|
||||
bool sRGBFramebuffer = false;
|
||||
int spriteBatchSize = 1000;
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
* @return 模块信息结构体
|
||||
*/
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.name = "Render";
|
||||
info.version = "1.0.0";
|
||||
info.priority = ModulePriority::Graphics;
|
||||
info.enabled = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置节名称
|
||||
* @return 配置节名称字符串
|
||||
*/
|
||||
std::string getConfigSectionName() const override { return "render"; }
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool validate() const override;
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void applyPlatformConstraints(PlatformType platform) override;
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void resetToDefaults() override;
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool loadFromJson(const void* jsonData) override;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool saveToJson(void* jsonData) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 渲染模块初始化器
|
||||
* 实现 IModuleInitializer 接口
|
||||
* 依赖窗口模块
|
||||
*/
|
||||
class RenderModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
RenderModuleInitializer();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~RenderModuleInitializer() override;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
ModulePriority getPriority() const override { return ModulePriority::Graphics; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getDependencies() const override;
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const IModuleConfig* config) override;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void shutdown() override;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const override { return initialized_; }
|
||||
|
||||
/**
|
||||
* @brief 获取渲染器实例
|
||||
* @return 渲染后端指针
|
||||
*/
|
||||
RenderBackend* getRenderer() const { return renderer_.get(); }
|
||||
|
||||
/**
|
||||
* @brief 设置窗口模块标识符
|
||||
* @param windowModuleId 窗口模块标识符
|
||||
*/
|
||||
void setWindowModuleId(ModuleId windowModuleId) { windowModuleId_ = windowModuleId; }
|
||||
|
||||
private:
|
||||
ModuleId moduleId_ = INVALID_MODULE_ID;
|
||||
ModuleId windowModuleId_ = INVALID_MODULE_ID;
|
||||
UniquePtr<RenderBackend> renderer_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -16,7 +16,6 @@ struct RenderTargetConfig {
|
|||
int height = 600; // 高度
|
||||
PixelFormat colorFormat = PixelFormat::RGBA8; // 颜色格式
|
||||
bool hasDepth = true; // 是否包含深度缓冲
|
||||
bool hasDepthBuffer = true; // 兼容旧API的别名 (同hasDepth)
|
||||
bool hasStencil = false; // 是否包含模板缓冲
|
||||
int samples = 1; // 多重采样数 (1 = 无MSAA)
|
||||
bool autoResize = true; // 是否自动调整大小
|
||||
|
|
@ -47,11 +46,6 @@ public:
|
|||
*/
|
||||
bool create(const RenderTargetConfig &config);
|
||||
|
||||
/**
|
||||
* @brief 初始化渲染目标(create的别名,兼容旧API)
|
||||
*/
|
||||
bool init(const RenderTargetConfig &config) { return create(config); }
|
||||
|
||||
/**
|
||||
* @brief 从现有纹理创建渲染目标
|
||||
*/
|
||||
|
|
@ -62,11 +56,6 @@ public:
|
|||
*/
|
||||
void destroy();
|
||||
|
||||
/**
|
||||
* @brief 关闭渲染目标(destroy的别名,兼容旧API)
|
||||
*/
|
||||
void shutdown() { destroy(); }
|
||||
|
||||
/**
|
||||
* @brief 检查是否有效
|
||||
*/
|
||||
|
|
@ -144,14 +133,6 @@ public:
|
|||
*/
|
||||
void copyTo(RenderTarget &target);
|
||||
|
||||
/**
|
||||
* @brief 复制到另一个渲染目标(blitTo的别名,兼容旧API)
|
||||
* @param target 目标渲染目标
|
||||
* @param color 是否复制颜色缓冲
|
||||
* @param depth 是否复制深度缓冲
|
||||
*/
|
||||
void blitTo(RenderTarget &target, bool color = true, bool depth = false);
|
||||
|
||||
/**
|
||||
* @brief 复制到屏幕
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/platform/iinput.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 输入模块配置
|
||||
* 实现 IModuleConfig 接口
|
||||
*/
|
||||
class InputModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
bool enableKeyboard = true;
|
||||
bool enableMouse = true;
|
||||
bool enableGamepad = true;
|
||||
bool enableTouch = true;
|
||||
float deadzone = 0.15f;
|
||||
float mouseSensitivity = 1.0f;
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
* @return 模块信息结构体
|
||||
*/
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.name = "Input";
|
||||
info.version = "1.0.0";
|
||||
info.priority = ModulePriority::Input;
|
||||
info.enabled = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置节名称
|
||||
* @return 配置节名称字符串
|
||||
*/
|
||||
std::string getConfigSectionName() const override { return "input"; }
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool validate() const override;
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void applyPlatformConstraints(PlatformType platform) override;
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void resetToDefaults() override;
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool loadFromJson(const void* jsonData) override;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool saveToJson(void* jsonData) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 输入模块初始化器
|
||||
* 实现 IModuleInitializer 接口
|
||||
* 依赖窗口模块
|
||||
*/
|
||||
class InputModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
InputModuleInitializer();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~InputModuleInitializer() override;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
ModulePriority getPriority() const override { return ModulePriority::Input; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* 返回此模块依赖的其他模块标识符
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getDependencies() const override;
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const IModuleConfig* config) override;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void shutdown() override;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const override { return initialized_; }
|
||||
|
||||
/**
|
||||
* @brief 获取输入接口
|
||||
* @return 输入接口指针
|
||||
*/
|
||||
IInput* getInput() const { return input_; }
|
||||
|
||||
/**
|
||||
* @brief 设置窗口模块标识符
|
||||
* @param windowModuleId 窗口模块标识符
|
||||
*/
|
||||
void setWindowModuleId(ModuleId windowModuleId) { windowModuleId_ = windowModuleId; }
|
||||
|
||||
private:
|
||||
ModuleId moduleId_ = INVALID_MODULE_ID;
|
||||
ModuleId windowModuleId_ = INVALID_MODULE_ID;
|
||||
IInput* input_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <extra2d/core/types.h>
|
||||
#include <extra2d/core/math_types.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -9,23 +10,6 @@ namespace extra2d {
|
|||
|
||||
class IInput;
|
||||
|
||||
/**
|
||||
* @brief 窗口配置数据
|
||||
*/
|
||||
struct WindowConfig {
|
||||
std::string title = "Extra2D Application";
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
bool fullscreen = false;
|
||||
bool fullscreenDesktop = true;
|
||||
bool resizable = true;
|
||||
bool vsync = true;
|
||||
int msaaSamples = 0;
|
||||
bool centerWindow = true;
|
||||
bool visible = true;
|
||||
bool decorated = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 光标形状
|
||||
*/
|
||||
|
|
@ -52,7 +36,7 @@ public:
|
|||
* @param cfg 窗口配置
|
||||
* @return 创建是否成功
|
||||
*/
|
||||
virtual bool create(const WindowConfig& cfg) = 0;
|
||||
virtual bool create(const WindowConfigData& cfg) = 0;
|
||||
|
||||
/**
|
||||
* @brief 销毁窗口
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/config/module_config.h>
|
||||
#include <extra2d/config/module_initializer.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/platform/iwindow.h>
|
||||
#include <extra2d/core/types.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 窗口模块配置
|
||||
* 实现 IModuleConfig 接口
|
||||
*/
|
||||
class WindowModuleConfig : public IModuleConfig {
|
||||
public:
|
||||
WindowConfigData windowConfig;
|
||||
std::string backend = "sdl2";
|
||||
|
||||
/**
|
||||
* @brief 获取模块信息
|
||||
* @return 模块信息结构体
|
||||
*/
|
||||
ModuleInfo getModuleInfo() const override {
|
||||
ModuleInfo info;
|
||||
info.name = "Window";
|
||||
info.version = "1.0.0";
|
||||
info.priority = ModulePriority::Platform;
|
||||
info.enabled = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置节名称
|
||||
* @return 配置节名称字符串
|
||||
*/
|
||||
std::string getConfigSectionName() const override { return "window"; }
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool validate() const override;
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void applyPlatformConstraints(PlatformType platform) override;
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void resetToDefaults() override;
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool loadFromJson(const void* jsonData) override;
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool saveToJson(void* jsonData) const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 窗口模块初始化器
|
||||
* 实现 IModuleInitializer 接口
|
||||
*/
|
||||
class WindowModuleInitializer : public IModuleInitializer {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
WindowModuleInitializer();
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
~WindowModuleInitializer() override;
|
||||
|
||||
/**
|
||||
* @brief 获取模块标识符
|
||||
* @return 模块唯一标识符
|
||||
*/
|
||||
ModuleId getModuleId() const override { return moduleId_; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块优先级
|
||||
* @return 模块优先级
|
||||
*/
|
||||
ModulePriority getPriority() const override { return ModulePriority::Platform; }
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> getDependencies() const override { return {}; }
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool initialize(const IModuleConfig* config) override;
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void shutdown() override;
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否已初始化
|
||||
* @return 已初始化返回 true
|
||||
*/
|
||||
bool isInitialized() const override { return initialized_; }
|
||||
|
||||
/**
|
||||
* @brief 获取窗口实例
|
||||
* @return 窗口接口指针
|
||||
*/
|
||||
IWindow* getWindow() const { return window_.get(); }
|
||||
|
||||
private:
|
||||
ModuleId moduleId_ = INVALID_MODULE_ID;
|
||||
UniquePtr<IWindow> window_;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#include <extra2d/app/application.h>
|
||||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/event/event_dispatcher.h>
|
||||
#include <extra2d/event/event_queue.h>
|
||||
#include <extra2d/graphics/camera.h>
|
||||
|
|
@ -20,6 +22,10 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 获取当前时间(秒)
|
||||
* @return 当前时间戳(秒)
|
||||
*/
|
||||
static double getTimeSeconds() {
|
||||
#ifdef __SWITCH__
|
||||
struct timespec ts;
|
||||
|
|
@ -39,7 +45,9 @@ Application& Application::get() {
|
|||
return instance;
|
||||
}
|
||||
|
||||
Application::~Application() { shutdown(); }
|
||||
Application::~Application() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool Application::init() {
|
||||
AppConfig cfg;
|
||||
|
|
@ -52,42 +60,74 @@ bool Application::init(const AppConfig& config) {
|
|||
return true;
|
||||
}
|
||||
|
||||
config_ = config;
|
||||
E2D_LOG_INFO("Initializing application with config...");
|
||||
|
||||
if (!ConfigManager::instance().initialize()) {
|
||||
E2D_LOG_ERROR("Failed to initialize ConfigManager");
|
||||
return false;
|
||||
}
|
||||
|
||||
ConfigManager::instance().setAppConfig(config);
|
||||
|
||||
return initImpl();
|
||||
}
|
||||
|
||||
bool Application::init(const std::string& path) {
|
||||
bool Application::init(const std::string& configPath) {
|
||||
if (initialized_) {
|
||||
E2D_LOG_WARN("Application already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Loading config from: {}", path);
|
||||
|
||||
AppConfig cfg;
|
||||
|
||||
if (path.find(".json") != std::string::npos) {
|
||||
// TODO: 使用 nlohmann_json 加载配置
|
||||
E2D_LOG_WARN("JSON config loading not yet implemented, using defaults");
|
||||
} else if (path.find(".ini") != std::string::npos) {
|
||||
// TODO: 实现 INI 配置加载
|
||||
E2D_LOG_WARN("INI config loading not yet implemented, using defaults");
|
||||
E2D_LOG_INFO("Initializing application from config file: {}", configPath);
|
||||
|
||||
if (!ConfigManager::instance().initialize(configPath)) {
|
||||
E2D_LOG_WARN("Failed to load config from file, using defaults");
|
||||
if (!ConfigManager::instance().initialize()) {
|
||||
E2D_LOG_ERROR("Failed to initialize ConfigManager");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
config_ = cfg;
|
||||
|
||||
return initImpl();
|
||||
}
|
||||
|
||||
bool Application::initImpl() {
|
||||
PlatformType platform = config_.platform;
|
||||
auto& configMgr = ConfigManager::instance();
|
||||
AppConfig& appConfig = configMgr.appConfig();
|
||||
|
||||
PlatformType platform = appConfig.targetPlatform;
|
||||
if (platform == PlatformType::Auto) {
|
||||
#ifdef __SWITCH__
|
||||
platform = PlatformType::Switch;
|
||||
#else
|
||||
platform = PlatformType::PC;
|
||||
#ifdef _WIN32
|
||||
platform = PlatformType::Windows;
|
||||
#elif defined(__linux__)
|
||||
platform = PlatformType::Linux;
|
||||
#elif defined(__APPLE__)
|
||||
platform = PlatformType::macOS;
|
||||
#else
|
||||
platform = PlatformType::Windows;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Target platform: {} ({})", getPlatformTypeName(platform),
|
||||
static_cast<int>(platform));
|
||||
|
||||
UniquePtr<PlatformConfig> platformConfig = createPlatformConfig(platform);
|
||||
if (!platformConfig) {
|
||||
E2D_LOG_ERROR("Failed to create platform config");
|
||||
return false;
|
||||
}
|
||||
|
||||
appConfig.applyPlatformConstraints(*platformConfig);
|
||||
|
||||
const auto& capabilities = platformConfig->capabilities();
|
||||
E2D_LOG_INFO("Platform capabilities: windowed={}, fullscreen={}, cursor={}, DPI={}",
|
||||
capabilities.supportsWindowed, capabilities.supportsFullscreen,
|
||||
capabilities.supportsCursor, capabilities.supportsDPIAwareness);
|
||||
|
||||
if (platform == PlatformType::Switch) {
|
||||
#ifdef __SWITCH__
|
||||
Result rc;
|
||||
|
|
@ -105,7 +145,36 @@ bool Application::initImpl() {
|
|||
#endif
|
||||
}
|
||||
|
||||
std::string backend = config_.backend;
|
||||
auto initOrder = ModuleRegistry::instance().getInitializationOrder();
|
||||
E2D_LOG_INFO("Initializing {} registered modules...", initOrder.size());
|
||||
|
||||
for (ModuleId moduleId : initOrder) {
|
||||
auto initializer = ModuleRegistry::instance().createInitializer(moduleId);
|
||||
if (!initializer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* moduleConfig = ModuleRegistry::instance().getModuleConfig(moduleId);
|
||||
if (!moduleConfig) {
|
||||
E2D_LOG_WARN("Module {} has no config, skipping", moduleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto info = moduleConfig->getModuleInfo();
|
||||
if (!info.enabled) {
|
||||
E2D_LOG_INFO("Module '{}' is disabled, skipping", info.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Initializing module '{}' (priority: {})",
|
||||
info.name, static_cast<int>(info.priority));
|
||||
|
||||
if (!initializer->initialize(moduleConfig)) {
|
||||
E2D_LOG_ERROR("Failed to initialize module '{}'", info.name);
|
||||
}
|
||||
}
|
||||
|
||||
std::string backend = "sdl2";
|
||||
#ifdef __SWITCH__
|
||||
backend = "switch";
|
||||
#endif
|
||||
|
|
@ -127,27 +196,19 @@ bool Application::initImpl() {
|
|||
return false;
|
||||
}
|
||||
|
||||
WindowConfig winConfig;
|
||||
winConfig.title = config_.title;
|
||||
winConfig.width = config_.width;
|
||||
winConfig.height = config_.height;
|
||||
WindowConfigData winConfig = appConfig.window;
|
||||
|
||||
if (platform == PlatformType::Switch) {
|
||||
winConfig.fullscreen = true;
|
||||
winConfig.fullscreenDesktop = false;
|
||||
winConfig.mode = WindowMode::Fullscreen;
|
||||
winConfig.resizable = false;
|
||||
} else {
|
||||
winConfig.fullscreen = config_.fullscreen;
|
||||
winConfig.resizable = config_.resizable;
|
||||
}
|
||||
winConfig.vsync = config_.vsync;
|
||||
winConfig.msaaSamples = config_.msaaSamples;
|
||||
|
||||
if (!window_->create(winConfig)) {
|
||||
E2D_LOG_ERROR("Failed to create window");
|
||||
return false;
|
||||
}
|
||||
|
||||
renderer_ = RenderBackend::create(config_.renderBackend);
|
||||
renderer_ = RenderBackend::create(appConfig.render.backend);
|
||||
if (!renderer_ || !renderer_->init(window_.get())) {
|
||||
E2D_LOG_ERROR("Failed to initialize renderer");
|
||||
window_->destroy();
|
||||
|
|
@ -163,8 +224,8 @@ bool Application::initImpl() {
|
|||
|
||||
viewportAdapter_ = makeUnique<ViewportAdapter>();
|
||||
ViewportConfig vpConfig;
|
||||
vpConfig.logicWidth = static_cast<float>(config_.width);
|
||||
vpConfig.logicHeight = static_cast<float>(config_.height);
|
||||
vpConfig.logicWidth = static_cast<float>(appConfig.window.width);
|
||||
vpConfig.logicHeight = static_cast<float>(appConfig.window.height);
|
||||
vpConfig.mode = ViewportMode::AspectRatio;
|
||||
viewportAdapter_->setConfig(vpConfig);
|
||||
|
||||
|
|
@ -193,7 +254,12 @@ bool Application::initImpl() {
|
|||
initialized_ = true;
|
||||
running_ = true;
|
||||
|
||||
E2D_LOG_INFO("Application initialized (backend: {})", backend);
|
||||
E2D_LOG_INFO("Application initialized successfully");
|
||||
E2D_LOG_INFO(" Window: {}x{}", window_->width(), window_->height());
|
||||
E2D_LOG_INFO(" Backend: {}", backend);
|
||||
E2D_LOG_INFO(" VSync: {}", appConfig.render.vsync);
|
||||
E2D_LOG_INFO(" Target FPS: {}", appConfig.render.targetFPS);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -227,14 +293,31 @@ void Application::shutdown() {
|
|||
window_.reset();
|
||||
}
|
||||
|
||||
PlatformType platform = config_.platform;
|
||||
auto modules = ModuleRegistry::instance().getAllModules();
|
||||
auto initOrder = ModuleRegistry::instance().getInitializationOrder();
|
||||
|
||||
for (auto it = initOrder.rbegin(); it != initOrder.rend(); ++it) {
|
||||
ModuleId moduleId = *it;
|
||||
auto initializer = ModuleRegistry::instance().createInitializer(moduleId);
|
||||
if (initializer && initializer->isInitialized()) {
|
||||
auto* moduleConfig = ModuleRegistry::instance().getModuleConfig(moduleId);
|
||||
if (moduleConfig) {
|
||||
auto info = moduleConfig->getModuleInfo();
|
||||
E2D_LOG_INFO("Shutting down module '{}'", info.name);
|
||||
}
|
||||
initializer->shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
PlatformType platform = ConfigManager::instance().appConfig().targetPlatform;
|
||||
if (platform == PlatformType::Auto) {
|
||||
#ifdef __SWITCH__
|
||||
platform = PlatformType::Switch;
|
||||
#else
|
||||
platform = PlatformType::PC;
|
||||
platform = PlatformType::Windows;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (platform == PlatformType::Switch) {
|
||||
#ifdef __SWITCH__
|
||||
romfsExit();
|
||||
|
|
@ -242,6 +325,8 @@ void Application::shutdown() {
|
|||
#endif
|
||||
}
|
||||
|
||||
ConfigManager::instance().shutdown();
|
||||
|
||||
initialized_ = false;
|
||||
running_ = false;
|
||||
|
||||
|
|
@ -308,15 +393,18 @@ void Application::mainLoop() {
|
|||
|
||||
render();
|
||||
|
||||
if (!config_.vsync && config_.fpsLimit > 0) {
|
||||
const auto& appConfig = ConfigManager::instance().appConfig();
|
||||
if (!appConfig.render.vsync && appConfig.render.isFPSCapped()) {
|
||||
double frameEndTime = getTimeSeconds();
|
||||
double frameTime = frameEndTime - currentTime;
|
||||
double target = 1.0 / static_cast<double>(config_.fpsLimit);
|
||||
double target = 1.0 / static_cast<double>(appConfig.render.targetFPS);
|
||||
if (frameTime < target) {
|
||||
auto sleepSeconds = target - frameTime;
|
||||
std::this_thread::sleep_for(std::chrono::duration<double>(sleepSeconds));
|
||||
}
|
||||
}
|
||||
|
||||
ConfigManager::instance().update(deltaTime_);
|
||||
}
|
||||
|
||||
void Application::update() {
|
||||
|
|
@ -353,19 +441,33 @@ void Application::render() {
|
|||
window_->swap();
|
||||
}
|
||||
|
||||
IInput& Application::input() { return *window_->input(); }
|
||||
IInput& Application::input() {
|
||||
return *window_->input();
|
||||
}
|
||||
|
||||
SceneManager& Application::scenes() { return *sceneManager_; }
|
||||
SceneManager& Application::scenes() {
|
||||
return *sceneManager_;
|
||||
}
|
||||
|
||||
TimerManager& Application::timers() { return *timerManager_; }
|
||||
TimerManager& Application::timers() {
|
||||
return *timerManager_;
|
||||
}
|
||||
|
||||
EventQueue& Application::eventQueue() { return *eventQueue_; }
|
||||
EventQueue& Application::eventQueue() {
|
||||
return *eventQueue_;
|
||||
}
|
||||
|
||||
EventDispatcher& Application::eventDispatcher() { return *eventDispatcher_; }
|
||||
EventDispatcher& Application::eventDispatcher() {
|
||||
return *eventDispatcher_;
|
||||
}
|
||||
|
||||
Camera& Application::camera() { return *camera_; }
|
||||
Camera& Application::camera() {
|
||||
return *camera_;
|
||||
}
|
||||
|
||||
ViewportAdapter& Application::viewportAdapter() { return *viewportAdapter_; }
|
||||
ViewportAdapter& Application::viewportAdapter() {
|
||||
return *viewportAdapter_;
|
||||
}
|
||||
|
||||
void Application::enterScene(Ptr<Scene> scene) {
|
||||
if (sceneManager_ && scene) {
|
||||
|
|
@ -375,4 +477,12 @@ void Application::enterScene(Ptr<Scene> scene) {
|
|||
}
|
||||
}
|
||||
|
||||
ConfigManager& Application::config() {
|
||||
return ConfigManager::instance();
|
||||
}
|
||||
|
||||
const AppConfig& Application::getConfig() const {
|
||||
return ConfigManager::instance().appConfig();
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -0,0 +1,681 @@
|
|||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的调试标志
|
||||
* @param flag 要检查的标志名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool DebugConfigData::hasDebugFlag(const std::string& flag) const {
|
||||
return std::find(debugFlags.begin(), debugFlags.end(), flag) != debugFlags.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加调试标志
|
||||
* @param flag 要添加的标志名称
|
||||
*/
|
||||
void DebugConfigData::addDebugFlag(const std::string& flag) {
|
||||
if (!hasDebugFlag(flag)) {
|
||||
debugFlags.push_back(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除调试标志
|
||||
* @param flag 要移除的标志名称
|
||||
*/
|
||||
void DebugConfigData::removeDebugFlag(const std::string& flag) {
|
||||
auto it = std::find(debugFlags.begin(), debugFlags.end(), flag);
|
||||
if (it != debugFlags.end()) {
|
||||
debugFlags.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加资源搜索路径
|
||||
* @param path 要添加的搜索路径
|
||||
*/
|
||||
void ResourceConfigData::addSearchPath(const std::string& path) {
|
||||
if (!hasSearchPath(path)) {
|
||||
searchPaths.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除资源搜索路径
|
||||
* @param path 要移除的搜索路径
|
||||
*/
|
||||
void ResourceConfigData::removeSearchPath(const std::string& path) {
|
||||
auto it = std::find(searchPaths.begin(), searchPaths.end(), path);
|
||||
if (it != searchPaths.end()) {
|
||||
searchPaths.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否存在指定的搜索路径
|
||||
* @param path 要检查的路径
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool ResourceConfigData::hasSearchPath(const std::string& path) const {
|
||||
return std::find(searchPaths.begin(), searchPaths.end(), path) != searchPaths.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建默认配置
|
||||
* 返回一个包含所有默认值的应用配置实例
|
||||
* @return 默认的应用配置实例
|
||||
*/
|
||||
AppConfig AppConfig::createDefault() {
|
||||
AppConfig config;
|
||||
|
||||
config.window.title = "Extra2D Application";
|
||||
config.window.width = 1280;
|
||||
config.window.height = 720;
|
||||
config.window.minWidth = 320;
|
||||
config.window.minHeight = 240;
|
||||
config.window.maxWidth = 0;
|
||||
config.window.maxHeight = 0;
|
||||
config.window.mode = WindowMode::Windowed;
|
||||
config.window.resizable = true;
|
||||
config.window.borderless = false;
|
||||
config.window.alwaysOnTop = false;
|
||||
config.window.centered = true;
|
||||
config.window.posX = -1;
|
||||
config.window.posY = -1;
|
||||
config.window.hideOnClose = false;
|
||||
config.window.minimizeOnClose = true;
|
||||
config.window.opacity = 1.0f;
|
||||
config.window.transparentFramebuffer = false;
|
||||
config.window.highDPI = true;
|
||||
config.window.contentScale = 1.0f;
|
||||
|
||||
config.render.backend = BackendType::OpenGL;
|
||||
config.render.targetFPS = 60;
|
||||
config.render.vsync = true;
|
||||
config.render.tripleBuffering = false;
|
||||
config.render.multisamples = 0;
|
||||
config.render.sRGBFramebuffer = false;
|
||||
config.render.clearColor = Color{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
config.render.maxTextureSize = 0;
|
||||
config.render.textureAnisotropy = 1;
|
||||
config.render.wireframeMode = false;
|
||||
config.render.depthTest = false;
|
||||
config.render.blending = true;
|
||||
config.render.dithering = false;
|
||||
config.render.spriteBatchSize = 1000;
|
||||
config.render.maxRenderTargets = 1;
|
||||
config.render.allowShaderHotReload = false;
|
||||
|
||||
config.audio.enabled = true;
|
||||
config.audio.masterVolume = 100;
|
||||
config.audio.musicVolume = 100;
|
||||
config.audio.sfxVolume = 100;
|
||||
config.audio.voiceVolume = 100;
|
||||
config.audio.ambientVolume = 100;
|
||||
config.audio.frequency = 44100;
|
||||
config.audio.channels = 2;
|
||||
config.audio.chunkSize = 2048;
|
||||
config.audio.maxChannels = 16;
|
||||
config.audio.spatialAudio = false;
|
||||
config.audio.listenerPosition[0] = 0.0f;
|
||||
config.audio.listenerPosition[1] = 0.0f;
|
||||
config.audio.listenerPosition[2] = 0.0f;
|
||||
|
||||
config.debug.enabled = false;
|
||||
config.debug.showFPS = false;
|
||||
config.debug.showMemoryUsage = false;
|
||||
config.debug.showRenderStats = false;
|
||||
config.debug.showColliders = false;
|
||||
config.debug.showGrid = false;
|
||||
config.debug.logToFile = false;
|
||||
config.debug.logToConsole = true;
|
||||
config.debug.logLevel = 2;
|
||||
config.debug.breakOnAssert = true;
|
||||
config.debug.enableProfiling = false;
|
||||
config.debug.debugFlags.clear();
|
||||
|
||||
config.input.enabled = true;
|
||||
config.input.rawMouseInput = false;
|
||||
config.input.mouseSensitivity = 1.0f;
|
||||
config.input.invertMouseY = false;
|
||||
config.input.invertMouseX = false;
|
||||
config.input.deadzone = 0.15f;
|
||||
config.input.triggerThreshold = 0.5f;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 4;
|
||||
config.input.autoConnectGamepads = true;
|
||||
|
||||
config.resource.assetRootPath = "assets";
|
||||
config.resource.cachePath = "cache";
|
||||
config.resource.savePath = "saves";
|
||||
config.resource.configPath = "config";
|
||||
config.resource.logPath = "logs";
|
||||
config.resource.useAssetCache = true;
|
||||
config.resource.maxCacheSize = 512;
|
||||
config.resource.hotReloadEnabled = false;
|
||||
config.resource.hotReloadInterval = 1.0f;
|
||||
config.resource.compressTextures = false;
|
||||
config.resource.preloadCommonAssets = true;
|
||||
config.resource.searchPaths.clear();
|
||||
|
||||
config.appName = "Extra2D App";
|
||||
config.appVersion = "1.0.0";
|
||||
config.organization = "";
|
||||
config.configFile = "config.json";
|
||||
config.targetPlatform = PlatformType::Auto;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 验证配置的有效性
|
||||
* 检查所有配置项是否在有效范围内
|
||||
* @return 如果配置有效返回 true,否则返回 false
|
||||
*/
|
||||
bool AppConfig::validate() const {
|
||||
if (window.width <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.height <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.minWidth <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: minimum window width must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.minHeight <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: minimum window height must be positive");
|
||||
return false;
|
||||
}
|
||||
if (window.maxWidth > 0 && window.maxWidth < window.minWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: maximum width less than minimum width");
|
||||
return false;
|
||||
}
|
||||
if (window.maxHeight > 0 && window.maxHeight < window.minHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: maximum height less than minimum height");
|
||||
return false;
|
||||
}
|
||||
if (window.width < window.minWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width less than minimum");
|
||||
return false;
|
||||
}
|
||||
if (window.height < window.minHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height less than minimum");
|
||||
return false;
|
||||
}
|
||||
if (window.maxWidth > 0 && window.width > window.maxWidth) {
|
||||
E2D_LOG_ERROR("Config validation failed: window width exceeds maximum");
|
||||
return false;
|
||||
}
|
||||
if (window.maxHeight > 0 && window.height > window.maxHeight) {
|
||||
E2D_LOG_ERROR("Config validation failed: window height exceeds maximum");
|
||||
return false;
|
||||
}
|
||||
if (window.opacity < 0.0f || window.opacity > 1.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: window opacity must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (window.contentScale <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: content scale must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (render.targetFPS < 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: target FPS cannot be negative");
|
||||
return false;
|
||||
}
|
||||
if (render.multisamples < 0 || render.multisamples > 16) {
|
||||
E2D_LOG_ERROR("Config validation failed: multisamples must be between 0 and 16");
|
||||
return false;
|
||||
}
|
||||
if (render.textureAnisotropy < 1 || render.textureAnisotropy > 16) {
|
||||
E2D_LOG_ERROR("Config validation failed: texture anisotropy must be between 1 and 16");
|
||||
return false;
|
||||
}
|
||||
if (render.spriteBatchSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: sprite batch size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (render.maxRenderTargets <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max render targets must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!audio.isValidVolume(audio.masterVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: master volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.musicVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: music volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.sfxVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: SFX volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.voiceVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: voice volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (!audio.isValidVolume(audio.ambientVolume)) {
|
||||
E2D_LOG_ERROR("Config validation failed: ambient volume must be between 0 and 100");
|
||||
return false;
|
||||
}
|
||||
if (audio.frequency <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio frequency must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.channels <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio channels must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.chunkSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: audio chunk size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (audio.maxChannels <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max audio channels must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debug.logLevel < 0 || debug.logLevel > 5) {
|
||||
E2D_LOG_ERROR("Config validation failed: log level must be between 0 and 5");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!input.isDeadzoneValid()) {
|
||||
E2D_LOG_ERROR("Config validation failed: deadzone must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (input.mouseSensitivity <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: mouse sensitivity must be positive");
|
||||
return false;
|
||||
}
|
||||
if (input.triggerThreshold < 0.0f || input.triggerThreshold > 1.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: trigger threshold must be between 0 and 1");
|
||||
return false;
|
||||
}
|
||||
if (input.maxGamepads <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max gamepads must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resource.maxCacheSize <= 0) {
|
||||
E2D_LOG_ERROR("Config validation failed: max cache size must be positive");
|
||||
return false;
|
||||
}
|
||||
if (resource.hotReloadInterval <= 0.0f) {
|
||||
E2D_LOG_ERROR("Config validation failed: hot reload interval must be positive");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (appName.empty()) {
|
||||
E2D_LOG_ERROR("Config validation failed: app name cannot be empty");
|
||||
return false;
|
||||
}
|
||||
if (appVersion.empty()) {
|
||||
E2D_LOG_ERROR("Config validation failed: app version cannot be empty");
|
||||
return false;
|
||||
}
|
||||
if (configFile.empty()) {
|
||||
E2D_LOG_ERROR("Config validation failed: config file cannot be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置参数
|
||||
* @param platform 平台配置接口
|
||||
*/
|
||||
void AppConfig::applyPlatformConstraints(const PlatformConfig& platform) {
|
||||
const PlatformCapabilities& caps = platform.capabilities();
|
||||
|
||||
if (!caps.supportsWindowed && window.mode == WindowMode::Windowed) {
|
||||
E2D_LOG_WARN("Platform does not support windowed mode, switching to fullscreen");
|
||||
window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
if (!caps.supportsBorderless && window.mode == WindowMode::Borderless) {
|
||||
E2D_LOG_WARN("Platform does not support borderless mode, switching to fullscreen");
|
||||
window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
if (!caps.supportsFullscreen && window.mode == WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Platform does not support fullscreen mode, switching to windowed");
|
||||
window.mode = WindowMode::Windowed;
|
||||
}
|
||||
|
||||
if (!caps.supportsResize) {
|
||||
window.resizable = false;
|
||||
}
|
||||
if (!caps.supportsHighDPI) {
|
||||
window.highDPI = false;
|
||||
}
|
||||
if (!caps.supportsCursor) {
|
||||
window.borderless = false;
|
||||
}
|
||||
|
||||
if (!caps.supportsVSync) {
|
||||
render.vsync = false;
|
||||
}
|
||||
|
||||
if (caps.maxTextureSize > 0) {
|
||||
if (render.maxTextureSize == 0 || render.maxTextureSize > caps.maxTextureSize) {
|
||||
render.maxTextureSize = caps.maxTextureSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (!caps.supportsGamepad) {
|
||||
input.maxGamepads = 0;
|
||||
input.enableVibration = false;
|
||||
}
|
||||
if (!caps.supportsKeyboard) {
|
||||
input.enabled = false;
|
||||
}
|
||||
if (!caps.supportsMouse) {
|
||||
input.rawMouseInput = false;
|
||||
}
|
||||
|
||||
platform.applyConstraints(*this);
|
||||
|
||||
E2D_LOG_INFO("Applied platform constraints for: {}", platform.platformName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认值
|
||||
* 将所有配置项恢复为默认值
|
||||
*/
|
||||
void AppConfig::reset() {
|
||||
*this = createDefault();
|
||||
E2D_LOG_INFO("App config reset to defaults");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 合并另一个配置
|
||||
* 将 other 中的非默认值覆盖到当前配置
|
||||
* @param other 要合并的配置
|
||||
*/
|
||||
void AppConfig::merge(const AppConfig& other) {
|
||||
if (other.window.title != "Extra2D Application") {
|
||||
window.title = other.window.title;
|
||||
}
|
||||
if (other.window.width != 1280) {
|
||||
window.width = other.window.width;
|
||||
}
|
||||
if (other.window.height != 720) {
|
||||
window.height = other.window.height;
|
||||
}
|
||||
if (other.window.minWidth != 320) {
|
||||
window.minWidth = other.window.minWidth;
|
||||
}
|
||||
if (other.window.minHeight != 240) {
|
||||
window.minHeight = other.window.minHeight;
|
||||
}
|
||||
if (other.window.maxWidth != 0) {
|
||||
window.maxWidth = other.window.maxWidth;
|
||||
}
|
||||
if (other.window.maxHeight != 0) {
|
||||
window.maxHeight = other.window.maxHeight;
|
||||
}
|
||||
if (other.window.mode != WindowMode::Windowed) {
|
||||
window.mode = other.window.mode;
|
||||
}
|
||||
if (other.window.resizable != true) {
|
||||
window.resizable = other.window.resizable;
|
||||
}
|
||||
if (other.window.borderless != false) {
|
||||
window.borderless = other.window.borderless;
|
||||
}
|
||||
if (other.window.alwaysOnTop != false) {
|
||||
window.alwaysOnTop = other.window.alwaysOnTop;
|
||||
}
|
||||
if (other.window.centered != true) {
|
||||
window.centered = other.window.centered;
|
||||
}
|
||||
if (other.window.posX >= 0) {
|
||||
window.posX = other.window.posX;
|
||||
}
|
||||
if (other.window.posY >= 0) {
|
||||
window.posY = other.window.posY;
|
||||
}
|
||||
if (other.window.hideOnClose != false) {
|
||||
window.hideOnClose = other.window.hideOnClose;
|
||||
}
|
||||
if (other.window.minimizeOnClose != true) {
|
||||
window.minimizeOnClose = other.window.minimizeOnClose;
|
||||
}
|
||||
if (other.window.opacity != 1.0f) {
|
||||
window.opacity = other.window.opacity;
|
||||
}
|
||||
if (other.window.transparentFramebuffer != false) {
|
||||
window.transparentFramebuffer = other.window.transparentFramebuffer;
|
||||
}
|
||||
if (other.window.highDPI != true) {
|
||||
window.highDPI = other.window.highDPI;
|
||||
}
|
||||
if (other.window.contentScale != 1.0f) {
|
||||
window.contentScale = other.window.contentScale;
|
||||
}
|
||||
|
||||
if (other.render.backend != BackendType::OpenGL) {
|
||||
render.backend = other.render.backend;
|
||||
}
|
||||
if (other.render.targetFPS != 60) {
|
||||
render.targetFPS = other.render.targetFPS;
|
||||
}
|
||||
if (other.render.vsync != true) {
|
||||
render.vsync = other.render.vsync;
|
||||
}
|
||||
if (other.render.tripleBuffering != false) {
|
||||
render.tripleBuffering = other.render.tripleBuffering;
|
||||
}
|
||||
if (other.render.multisamples != 0) {
|
||||
render.multisamples = other.render.multisamples;
|
||||
}
|
||||
if (other.render.sRGBFramebuffer != false) {
|
||||
render.sRGBFramebuffer = other.render.sRGBFramebuffer;
|
||||
}
|
||||
if (other.render.maxTextureSize != 0) {
|
||||
render.maxTextureSize = other.render.maxTextureSize;
|
||||
}
|
||||
if (other.render.textureAnisotropy != 1) {
|
||||
render.textureAnisotropy = other.render.textureAnisotropy;
|
||||
}
|
||||
if (other.render.wireframeMode != false) {
|
||||
render.wireframeMode = other.render.wireframeMode;
|
||||
}
|
||||
if (other.render.depthTest != false) {
|
||||
render.depthTest = other.render.depthTest;
|
||||
}
|
||||
if (other.render.blending != true) {
|
||||
render.blending = other.render.blending;
|
||||
}
|
||||
if (other.render.dithering != false) {
|
||||
render.dithering = other.render.dithering;
|
||||
}
|
||||
if (other.render.spriteBatchSize != 1000) {
|
||||
render.spriteBatchSize = other.render.spriteBatchSize;
|
||||
}
|
||||
if (other.render.maxRenderTargets != 1) {
|
||||
render.maxRenderTargets = other.render.maxRenderTargets;
|
||||
}
|
||||
if (other.render.allowShaderHotReload != false) {
|
||||
render.allowShaderHotReload = other.render.allowShaderHotReload;
|
||||
}
|
||||
if (!other.render.shaderCachePath.empty()) {
|
||||
render.shaderCachePath = other.render.shaderCachePath;
|
||||
}
|
||||
|
||||
if (other.audio.enabled != true) {
|
||||
audio.enabled = other.audio.enabled;
|
||||
}
|
||||
if (other.audio.masterVolume != 100) {
|
||||
audio.masterVolume = other.audio.masterVolume;
|
||||
}
|
||||
if (other.audio.musicVolume != 100) {
|
||||
audio.musicVolume = other.audio.musicVolume;
|
||||
}
|
||||
if (other.audio.sfxVolume != 100) {
|
||||
audio.sfxVolume = other.audio.sfxVolume;
|
||||
}
|
||||
if (other.audio.voiceVolume != 100) {
|
||||
audio.voiceVolume = other.audio.voiceVolume;
|
||||
}
|
||||
if (other.audio.ambientVolume != 100) {
|
||||
audio.ambientVolume = other.audio.ambientVolume;
|
||||
}
|
||||
if (other.audio.frequency != 44100) {
|
||||
audio.frequency = other.audio.frequency;
|
||||
}
|
||||
if (other.audio.channels != 2) {
|
||||
audio.channels = other.audio.channels;
|
||||
}
|
||||
if (other.audio.chunkSize != 2048) {
|
||||
audio.chunkSize = other.audio.chunkSize;
|
||||
}
|
||||
if (other.audio.maxChannels != 16) {
|
||||
audio.maxChannels = other.audio.maxChannels;
|
||||
}
|
||||
if (other.audio.spatialAudio != false) {
|
||||
audio.spatialAudio = other.audio.spatialAudio;
|
||||
}
|
||||
|
||||
if (other.debug.enabled != false) {
|
||||
debug.enabled = other.debug.enabled;
|
||||
}
|
||||
if (other.debug.showFPS != false) {
|
||||
debug.showFPS = other.debug.showFPS;
|
||||
}
|
||||
if (other.debug.showMemoryUsage != false) {
|
||||
debug.showMemoryUsage = other.debug.showMemoryUsage;
|
||||
}
|
||||
if (other.debug.showRenderStats != false) {
|
||||
debug.showRenderStats = other.debug.showRenderStats;
|
||||
}
|
||||
if (other.debug.showColliders != false) {
|
||||
debug.showColliders = other.debug.showColliders;
|
||||
}
|
||||
if (other.debug.showGrid != false) {
|
||||
debug.showGrid = other.debug.showGrid;
|
||||
}
|
||||
if (other.debug.logToFile != false) {
|
||||
debug.logToFile = other.debug.logToFile;
|
||||
}
|
||||
if (other.debug.logToConsole != true) {
|
||||
debug.logToConsole = other.debug.logToConsole;
|
||||
}
|
||||
if (other.debug.logLevel != 2) {
|
||||
debug.logLevel = other.debug.logLevel;
|
||||
}
|
||||
if (other.debug.breakOnAssert != true) {
|
||||
debug.breakOnAssert = other.debug.breakOnAssert;
|
||||
}
|
||||
if (other.debug.enableProfiling != false) {
|
||||
debug.enableProfiling = other.debug.enableProfiling;
|
||||
}
|
||||
if (!other.debug.logFilePath.empty()) {
|
||||
debug.logFilePath = other.debug.logFilePath;
|
||||
}
|
||||
for (const auto& flag : other.debug.debugFlags) {
|
||||
debug.addDebugFlag(flag);
|
||||
}
|
||||
|
||||
if (other.input.enabled != true) {
|
||||
input.enabled = other.input.enabled;
|
||||
}
|
||||
if (other.input.rawMouseInput != false) {
|
||||
input.rawMouseInput = other.input.rawMouseInput;
|
||||
}
|
||||
if (other.input.mouseSensitivity != 1.0f) {
|
||||
input.mouseSensitivity = other.input.mouseSensitivity;
|
||||
}
|
||||
if (other.input.invertMouseY != false) {
|
||||
input.invertMouseY = other.input.invertMouseY;
|
||||
}
|
||||
if (other.input.invertMouseX != false) {
|
||||
input.invertMouseX = other.input.invertMouseX;
|
||||
}
|
||||
if (other.input.deadzone != 0.15f) {
|
||||
input.deadzone = other.input.deadzone;
|
||||
}
|
||||
if (other.input.triggerThreshold != 0.5f) {
|
||||
input.triggerThreshold = other.input.triggerThreshold;
|
||||
}
|
||||
if (other.input.enableVibration != true) {
|
||||
input.enableVibration = other.input.enableVibration;
|
||||
}
|
||||
if (other.input.maxGamepads != 4) {
|
||||
input.maxGamepads = other.input.maxGamepads;
|
||||
}
|
||||
if (other.input.autoConnectGamepads != true) {
|
||||
input.autoConnectGamepads = other.input.autoConnectGamepads;
|
||||
}
|
||||
if (!other.input.gamepadMappingFile.empty()) {
|
||||
input.gamepadMappingFile = other.input.gamepadMappingFile;
|
||||
}
|
||||
|
||||
if (other.resource.assetRootPath != "assets") {
|
||||
resource.assetRootPath = other.resource.assetRootPath;
|
||||
}
|
||||
if (other.resource.cachePath != "cache") {
|
||||
resource.cachePath = other.resource.cachePath;
|
||||
}
|
||||
if (other.resource.savePath != "saves") {
|
||||
resource.savePath = other.resource.savePath;
|
||||
}
|
||||
if (other.resource.configPath != "config") {
|
||||
resource.configPath = other.resource.configPath;
|
||||
}
|
||||
if (other.resource.logPath != "logs") {
|
||||
resource.logPath = other.resource.logPath;
|
||||
}
|
||||
if (other.resource.useAssetCache != true) {
|
||||
resource.useAssetCache = other.resource.useAssetCache;
|
||||
}
|
||||
if (other.resource.maxCacheSize != 512) {
|
||||
resource.maxCacheSize = other.resource.maxCacheSize;
|
||||
}
|
||||
if (other.resource.hotReloadEnabled != false) {
|
||||
resource.hotReloadEnabled = other.resource.hotReloadEnabled;
|
||||
}
|
||||
if (other.resource.hotReloadInterval != 1.0f) {
|
||||
resource.hotReloadInterval = other.resource.hotReloadInterval;
|
||||
}
|
||||
if (other.resource.compressTextures != false) {
|
||||
resource.compressTextures = other.resource.compressTextures;
|
||||
}
|
||||
if (other.resource.preloadCommonAssets != true) {
|
||||
resource.preloadCommonAssets = other.resource.preloadCommonAssets;
|
||||
}
|
||||
for (const auto& path : other.resource.searchPaths) {
|
||||
resource.addSearchPath(path);
|
||||
}
|
||||
|
||||
if (other.appName != "Extra2D App") {
|
||||
appName = other.appName;
|
||||
}
|
||||
if (other.appVersion != "1.0.0") {
|
||||
appVersion = other.appVersion;
|
||||
}
|
||||
if (!other.organization.empty()) {
|
||||
organization = other.organization;
|
||||
}
|
||||
if (other.configFile != "config.json") {
|
||||
configFile = other.configFile;
|
||||
}
|
||||
if (other.targetPlatform != PlatformType::Auto) {
|
||||
targetPlatform = other.targetPlatform;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Merged app config");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,952 @@
|
|||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 去除字符串首尾空白字符
|
||||
* @param str 输入字符串
|
||||
* @return 去除空白后的字符串
|
||||
*/
|
||||
static std::string trim(const std::string& str) {
|
||||
size_t start = 0;
|
||||
while (start < str.length() && std::isspace(static_cast<unsigned char>(str[start]))) {
|
||||
++start;
|
||||
}
|
||||
size_t end = str.length();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(str[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
return str.substr(start, end - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为小写
|
||||
* @param str 输入字符串
|
||||
* @return 小写字符串
|
||||
*/
|
||||
static std::string toLower(const std::string& str) {
|
||||
std::string result = str;
|
||||
for (char& c : result) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为窗口模式枚举
|
||||
* @param modeStr 窗口模式字符串
|
||||
* @return 窗口模式枚举值
|
||||
*/
|
||||
static WindowMode stringToWindowMode(const std::string& modeStr) {
|
||||
std::string lower = toLower(modeStr);
|
||||
if (lower == "fullscreen") return WindowMode::Fullscreen;
|
||||
if (lower == "borderless") return WindowMode::Borderless;
|
||||
return WindowMode::Windowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将窗口模式枚举转换为字符串
|
||||
* @param mode 窗口模式枚举值
|
||||
* @return 窗口模式字符串
|
||||
*/
|
||||
static std::string windowModeToString(WindowMode mode) {
|
||||
switch (mode) {
|
||||
case WindowMode::Fullscreen: return "fullscreen";
|
||||
case WindowMode::Borderless: return "borderless";
|
||||
default: return "windowed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为渲染后端类型枚举
|
||||
* @param backendStr 后端类型字符串
|
||||
* @return 渲染后端类型枚举值
|
||||
*/
|
||||
static BackendType stringToBackendType(const std::string& backendStr) {
|
||||
std::string lower = toLower(backendStr);
|
||||
if (lower == "opengl") return BackendType::OpenGL;
|
||||
return BackendType::OpenGL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将渲染后端类型枚举转换为字符串
|
||||
* @param backend 渲染后端类型枚举值
|
||||
* @return 后端类型字符串
|
||||
*/
|
||||
static std::string backendTypeToString(BackendType backend) {
|
||||
switch (backend) {
|
||||
case BackendType::OpenGL: return "opengl";
|
||||
default: return "opengl";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief INI 数据存储结构
|
||||
*/
|
||||
using IniData = std::map<std::string, std::map<std::string, std::string>>;
|
||||
|
||||
/**
|
||||
* @brief 解析 INI 内容到数据结构
|
||||
* @param content INI 内容字符串
|
||||
* @param data 输出的 INI 数据
|
||||
* @return 解析结果
|
||||
*/
|
||||
static ConfigLoadResult parseIniContent(const std::string& content, IniData& data) {
|
||||
std::istringstream stream(content);
|
||||
std::string line;
|
||||
std::string currentSection;
|
||||
int lineNumber = 0;
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
++lineNumber;
|
||||
line = trim(line);
|
||||
|
||||
if (line.empty() || line[0] == ';' || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line[0] == '[') {
|
||||
size_t endBracket = line.find(']');
|
||||
if (endBracket == std::string::npos) {
|
||||
return ConfigLoadResult::error("INI 解析错误: 缺少右括号", lineNumber);
|
||||
}
|
||||
currentSection = trim(line.substr(1, endBracket - 1));
|
||||
if (data.find(currentSection) == data.end()) {
|
||||
data[currentSection] = std::map<std::string, std::string>();
|
||||
}
|
||||
} else {
|
||||
size_t equalPos = line.find('=');
|
||||
if (equalPos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key = trim(line.substr(0, equalPos));
|
||||
std::string value = trim(line.substr(equalPos + 1));
|
||||
|
||||
if (currentSection.empty()) {
|
||||
return ConfigLoadResult::error("INI 解析错误: 键值对不在任何节中", lineNumber);
|
||||
}
|
||||
|
||||
data[currentSection][key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取 INI 值(带默认值)
|
||||
* @param data INI 数据
|
||||
* @param section 节名
|
||||
* @param key 键名
|
||||
* @param defaultValue 默认值
|
||||
* @return 值字符串
|
||||
*/
|
||||
static std::string getIniValue(const IniData& data, const std::string& section,
|
||||
const std::string& key, const std::string& defaultValue = "") {
|
||||
auto sectionIt = data.find(section);
|
||||
if (sectionIt == data.end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
auto keyIt = sectionIt->second.find(key);
|
||||
if (keyIt == sectionIt->second.end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return keyIt->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查 INI 值是否存在
|
||||
* @param data INI 数据
|
||||
* @param section 节名
|
||||
* @param key 键名
|
||||
* @return 是否存在
|
||||
*/
|
||||
static bool hasIniValue(const IniData& data, const std::string& section, const std::string& key) {
|
||||
auto sectionIt = data.find(section);
|
||||
if (sectionIt == data.end()) {
|
||||
return false;
|
||||
}
|
||||
return sectionIt->second.find(key) != sectionIt->second.end();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IniConfigLoader 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 从 INI 文件加载配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::load(const std::string& filepath, AppConfig& config) {
|
||||
E2D_LOG_INFO("正在从 INI 文件加载配置: {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法打开配置文件: {}", filepath);
|
||||
return ConfigLoadResult::error("无法打开配置文件: " + filepath);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
file.close();
|
||||
|
||||
return loadFromString(content, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 INI 文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult IniConfigLoader::save(const std::string& filepath, const AppConfig& config) {
|
||||
E2D_LOG_INFO("正在保存配置到 INI 文件: {}", filepath);
|
||||
|
||||
std::string content = saveToString(config);
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法创建配置文件: {}", filepath);
|
||||
return ConfigSaveResult::error("无法创建配置文件: " + filepath);
|
||||
}
|
||||
|
||||
file << content;
|
||||
file.close();
|
||||
|
||||
E2D_LOG_INFO("配置已成功保存到: {}", filepath);
|
||||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 INI 字符串加载配置
|
||||
* @param content INI 内容字符串
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::loadFromString(const std::string& content, AppConfig& config) {
|
||||
IniData data;
|
||||
auto result = parseIniContent(content, data);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "app", "name")) {
|
||||
config.appName = getIniValue(data, "app", "name");
|
||||
}
|
||||
if (hasIniValue(data, "app", "version")) {
|
||||
config.appVersion = getIniValue(data, "app", "version");
|
||||
}
|
||||
if (hasIniValue(data, "app", "organization")) {
|
||||
config.organization = getIniValue(data, "app", "organization");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "window", "title")) {
|
||||
config.window.title = getIniValue(data, "window", "title");
|
||||
}
|
||||
if (hasIniValue(data, "window", "width")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "width"), value, "window.width");
|
||||
if (res.hasError()) return res;
|
||||
config.window.width = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "height")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "height"), value, "window.height");
|
||||
if (res.hasError()) return res;
|
||||
config.window.height = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minWidth")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "minWidth"), value, "window.minWidth");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minWidth = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minHeight")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "minHeight"), value, "window.minHeight");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minHeight = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "maxWidth")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "maxWidth"), value, "window.maxWidth");
|
||||
if (res.hasError()) return res;
|
||||
config.window.maxWidth = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "maxHeight")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "maxHeight"), value, "window.maxHeight");
|
||||
if (res.hasError()) return res;
|
||||
config.window.maxHeight = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "mode")) {
|
||||
config.window.mode = stringToWindowMode(getIniValue(data, "window", "mode"));
|
||||
}
|
||||
if (hasIniValue(data, "window", "resizable")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "resizable"), value, "window.resizable");
|
||||
if (res.hasError()) return res;
|
||||
config.window.resizable = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "borderless")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "borderless"), value, "window.borderless");
|
||||
if (res.hasError()) return res;
|
||||
config.window.borderless = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "alwaysOnTop")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "alwaysOnTop"), value, "window.alwaysOnTop");
|
||||
if (res.hasError()) return res;
|
||||
config.window.alwaysOnTop = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "centered")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "centered"), value, "window.centered");
|
||||
if (res.hasError()) return res;
|
||||
config.window.centered = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "posX")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "posX"), value, "window.posX");
|
||||
if (res.hasError()) return res;
|
||||
config.window.posX = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "posY")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "window", "posY"), value, "window.posY");
|
||||
if (res.hasError()) return res;
|
||||
config.window.posY = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "hideOnClose")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "hideOnClose"), value, "window.hideOnClose");
|
||||
if (res.hasError()) return res;
|
||||
config.window.hideOnClose = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "minimizeOnClose")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "minimizeOnClose"), value, "window.minimizeOnClose");
|
||||
if (res.hasError()) return res;
|
||||
config.window.minimizeOnClose = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "opacity")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "window", "opacity"), value, "window.opacity");
|
||||
if (res.hasError()) return res;
|
||||
config.window.opacity = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "transparentFramebuffer")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "transparentFramebuffer"), value, "window.transparentFramebuffer");
|
||||
if (res.hasError()) return res;
|
||||
config.window.transparentFramebuffer = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "highDPI")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "window", "highDPI"), value, "window.highDPI");
|
||||
if (res.hasError()) return res;
|
||||
config.window.highDPI = value;
|
||||
}
|
||||
if (hasIniValue(data, "window", "contentScale")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "window", "contentScale"), value, "window.contentScale");
|
||||
if (res.hasError()) return res;
|
||||
config.window.contentScale = value;
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "render", "backend")) {
|
||||
config.render.backend = stringToBackendType(getIniValue(data, "render", "backend"));
|
||||
}
|
||||
if (hasIniValue(data, "render", "targetFPS")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "targetFPS"), value, "render.targetFPS");
|
||||
if (res.hasError()) return res;
|
||||
config.render.targetFPS = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "vsync")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "vsync"), value, "render.vsync");
|
||||
if (res.hasError()) return res;
|
||||
config.render.vsync = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "tripleBuffering")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "tripleBuffering"), value, "render.tripleBuffering");
|
||||
if (res.hasError()) return res;
|
||||
config.render.tripleBuffering = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "multisamples")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "multisamples"), value, "render.multisamples");
|
||||
if (res.hasError()) return res;
|
||||
config.render.multisamples = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "sRGBFramebuffer")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "sRGBFramebuffer"), value, "render.sRGBFramebuffer");
|
||||
if (res.hasError()) return res;
|
||||
config.render.sRGBFramebuffer = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorR")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorR"), value, "render.clearColorR");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.r = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorG")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorG"), value, "render.clearColorG");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.g = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorB")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorB"), value, "render.clearColorB");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.b = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "clearColorA")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "render", "clearColorA"), value, "render.clearColorA");
|
||||
if (res.hasError()) return res;
|
||||
config.render.clearColor.a = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "maxTextureSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "maxTextureSize"), value, "render.maxTextureSize");
|
||||
if (res.hasError()) return res;
|
||||
config.render.maxTextureSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "textureAnisotropy")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "textureAnisotropy"), value, "render.textureAnisotropy");
|
||||
if (res.hasError()) return res;
|
||||
config.render.textureAnisotropy = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "wireframeMode")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "wireframeMode"), value, "render.wireframeMode");
|
||||
if (res.hasError()) return res;
|
||||
config.render.wireframeMode = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "depthTest")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "depthTest"), value, "render.depthTest");
|
||||
if (res.hasError()) return res;
|
||||
config.render.depthTest = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "blending")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "blending"), value, "render.blending");
|
||||
if (res.hasError()) return res;
|
||||
config.render.blending = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "dithering")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "dithering"), value, "render.dithering");
|
||||
if (res.hasError()) return res;
|
||||
config.render.dithering = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "spriteBatchSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "spriteBatchSize"), value, "render.spriteBatchSize");
|
||||
if (res.hasError()) return res;
|
||||
config.render.spriteBatchSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "maxRenderTargets")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "render", "maxRenderTargets"), value, "render.maxRenderTargets");
|
||||
if (res.hasError()) return res;
|
||||
config.render.maxRenderTargets = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "allowShaderHotReload")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "render", "allowShaderHotReload"), value, "render.allowShaderHotReload");
|
||||
if (res.hasError()) return res;
|
||||
config.render.allowShaderHotReload = value;
|
||||
}
|
||||
if (hasIniValue(data, "render", "shaderCachePath")) {
|
||||
config.render.shaderCachePath = getIniValue(data, "render", "shaderCachePath");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "audio", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "audio", "enabled"), value, "audio.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "masterVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "masterVolume"), value, "audio.masterVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.masterVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "musicVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "musicVolume"), value, "audio.musicVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.musicVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "sfxVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "sfxVolume"), value, "audio.sfxVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.sfxVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "voiceVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "voiceVolume"), value, "audio.voiceVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.voiceVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "ambientVolume")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "ambientVolume"), value, "audio.ambientVolume");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.ambientVolume = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "frequency")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "frequency"), value, "audio.frequency");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.frequency = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "channels")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "channels"), value, "audio.channels");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.channels = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "chunkSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "chunkSize"), value, "audio.chunkSize");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.chunkSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "maxChannels")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "audio", "maxChannels"), value, "audio.maxChannels");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.maxChannels = value;
|
||||
}
|
||||
if (hasIniValue(data, "audio", "spatialAudio")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "audio", "spatialAudio"), value, "audio.spatialAudio");
|
||||
if (res.hasError()) return res;
|
||||
config.audio.spatialAudio = value;
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "debug", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "enabled"), value, "debug.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showFPS")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showFPS"), value, "debug.showFPS");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showFPS = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showMemoryUsage")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showMemoryUsage"), value, "debug.showMemoryUsage");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showMemoryUsage = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showRenderStats")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showRenderStats"), value, "debug.showRenderStats");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showRenderStats = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showColliders")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showColliders"), value, "debug.showColliders");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showColliders = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "showGrid")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "showGrid"), value, "debug.showGrid");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.showGrid = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logToFile")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "logToFile"), value, "debug.logToFile");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logToFile = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logToConsole")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "logToConsole"), value, "debug.logToConsole");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logToConsole = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logLevel")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "debug", "logLevel"), value, "debug.logLevel");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.logLevel = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "breakOnAssert")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "breakOnAssert"), value, "debug.breakOnAssert");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.breakOnAssert = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "enableProfiling")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "debug", "enableProfiling"), value, "debug.enableProfiling");
|
||||
if (res.hasError()) return res;
|
||||
config.debug.enableProfiling = value;
|
||||
}
|
||||
if (hasIniValue(data, "debug", "logFilePath")) {
|
||||
config.debug.logFilePath = getIniValue(data, "debug", "logFilePath");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "input", "enabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "enabled"), value, "input.enabled");
|
||||
if (res.hasError()) return res;
|
||||
config.input.enabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "rawMouseInput")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "rawMouseInput"), value, "input.rawMouseInput");
|
||||
if (res.hasError()) return res;
|
||||
config.input.rawMouseInput = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "mouseSensitivity")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "mouseSensitivity"), value, "input.mouseSensitivity");
|
||||
if (res.hasError()) return res;
|
||||
config.input.mouseSensitivity = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "invertMouseY")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "invertMouseY"), value, "input.invertMouseY");
|
||||
if (res.hasError()) return res;
|
||||
config.input.invertMouseY = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "invertMouseX")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "invertMouseX"), value, "input.invertMouseX");
|
||||
if (res.hasError()) return res;
|
||||
config.input.invertMouseX = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "deadzone")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "deadzone"), value, "input.deadzone");
|
||||
if (res.hasError()) return res;
|
||||
config.input.deadzone = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "triggerThreshold")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "input", "triggerThreshold"), value, "input.triggerThreshold");
|
||||
if (res.hasError()) return res;
|
||||
config.input.triggerThreshold = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "enableVibration")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "enableVibration"), value, "input.enableVibration");
|
||||
if (res.hasError()) return res;
|
||||
config.input.enableVibration = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "maxGamepads")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "input", "maxGamepads"), value, "input.maxGamepads");
|
||||
if (res.hasError()) return res;
|
||||
config.input.maxGamepads = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "autoConnectGamepads")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "input", "autoConnectGamepads"), value, "input.autoConnectGamepads");
|
||||
if (res.hasError()) return res;
|
||||
config.input.autoConnectGamepads = value;
|
||||
}
|
||||
if (hasIniValue(data, "input", "gamepadMappingFile")) {
|
||||
config.input.gamepadMappingFile = getIniValue(data, "input", "gamepadMappingFile");
|
||||
}
|
||||
|
||||
if (hasIniValue(data, "resource", "assetRootPath")) {
|
||||
config.resource.assetRootPath = getIniValue(data, "resource", "assetRootPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "cachePath")) {
|
||||
config.resource.cachePath = getIniValue(data, "resource", "cachePath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "savePath")) {
|
||||
config.resource.savePath = getIniValue(data, "resource", "savePath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "configPath")) {
|
||||
config.resource.configPath = getIniValue(data, "resource", "configPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "logPath")) {
|
||||
config.resource.logPath = getIniValue(data, "resource", "logPath");
|
||||
}
|
||||
if (hasIniValue(data, "resource", "useAssetCache")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "useAssetCache"), value, "resource.useAssetCache");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.useAssetCache = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "maxCacheSize")) {
|
||||
int value;
|
||||
auto res = parseInt(getIniValue(data, "resource", "maxCacheSize"), value, "resource.maxCacheSize");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.maxCacheSize = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "hotReloadEnabled")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "hotReloadEnabled"), value, "resource.hotReloadEnabled");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.hotReloadEnabled = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "hotReloadInterval")) {
|
||||
float value;
|
||||
auto res = parseFloat(getIniValue(data, "resource", "hotReloadInterval"), value, "resource.hotReloadInterval");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.hotReloadInterval = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "compressTextures")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "compressTextures"), value, "resource.compressTextures");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.compressTextures = value;
|
||||
}
|
||||
if (hasIniValue(data, "resource", "preloadCommonAssets")) {
|
||||
bool value;
|
||||
auto res = parseBool(getIniValue(data, "resource", "preloadCommonAssets"), value, "resource.preloadCommonAssets");
|
||||
if (res.hasError()) return res;
|
||||
config.resource.preloadCommonAssets = value;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("INI 配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置序列化为 INI 字符串
|
||||
* @param config 配置对象
|
||||
* @return 序列化后的 INI 字符串
|
||||
*/
|
||||
std::string IniConfigLoader::saveToString(const AppConfig& config) {
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "[app]\n";
|
||||
oss << "name=" << config.appName << "\n";
|
||||
oss << "version=" << config.appVersion << "\n";
|
||||
oss << "organization=" << config.organization << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[window]\n";
|
||||
oss << "title=" << config.window.title << "\n";
|
||||
oss << "width=" << config.window.width << "\n";
|
||||
oss << "height=" << config.window.height << "\n";
|
||||
oss << "minWidth=" << config.window.minWidth << "\n";
|
||||
oss << "minHeight=" << config.window.minHeight << "\n";
|
||||
oss << "maxWidth=" << config.window.maxWidth << "\n";
|
||||
oss << "maxHeight=" << config.window.maxHeight << "\n";
|
||||
oss << "mode=" << windowModeToString(config.window.mode) << "\n";
|
||||
oss << "resizable=" << (config.window.resizable ? "true" : "false") << "\n";
|
||||
oss << "borderless=" << (config.window.borderless ? "true" : "false") << "\n";
|
||||
oss << "alwaysOnTop=" << (config.window.alwaysOnTop ? "true" : "false") << "\n";
|
||||
oss << "centered=" << (config.window.centered ? "true" : "false") << "\n";
|
||||
oss << "posX=" << config.window.posX << "\n";
|
||||
oss << "posY=" << config.window.posY << "\n";
|
||||
oss << "hideOnClose=" << (config.window.hideOnClose ? "true" : "false") << "\n";
|
||||
oss << "minimizeOnClose=" << (config.window.minimizeOnClose ? "true" : "false") << "\n";
|
||||
oss << "opacity=" << config.window.opacity << "\n";
|
||||
oss << "transparentFramebuffer=" << (config.window.transparentFramebuffer ? "true" : "false") << "\n";
|
||||
oss << "highDPI=" << (config.window.highDPI ? "true" : "false") << "\n";
|
||||
oss << "contentScale=" << config.window.contentScale << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[render]\n";
|
||||
oss << "backend=" << backendTypeToString(config.render.backend) << "\n";
|
||||
oss << "targetFPS=" << config.render.targetFPS << "\n";
|
||||
oss << "vsync=" << (config.render.vsync ? "true" : "false") << "\n";
|
||||
oss << "tripleBuffering=" << (config.render.tripleBuffering ? "true" : "false") << "\n";
|
||||
oss << "multisamples=" << config.render.multisamples << "\n";
|
||||
oss << "sRGBFramebuffer=" << (config.render.sRGBFramebuffer ? "true" : "false") << "\n";
|
||||
oss << "clearColorR=" << config.render.clearColor.r << "\n";
|
||||
oss << "clearColorG=" << config.render.clearColor.g << "\n";
|
||||
oss << "clearColorB=" << config.render.clearColor.b << "\n";
|
||||
oss << "clearColorA=" << config.render.clearColor.a << "\n";
|
||||
oss << "maxTextureSize=" << config.render.maxTextureSize << "\n";
|
||||
oss << "textureAnisotropy=" << config.render.textureAnisotropy << "\n";
|
||||
oss << "wireframeMode=" << (config.render.wireframeMode ? "true" : "false") << "\n";
|
||||
oss << "depthTest=" << (config.render.depthTest ? "true" : "false") << "\n";
|
||||
oss << "blending=" << (config.render.blending ? "true" : "false") << "\n";
|
||||
oss << "dithering=" << (config.render.dithering ? "true" : "false") << "\n";
|
||||
oss << "spriteBatchSize=" << config.render.spriteBatchSize << "\n";
|
||||
oss << "maxRenderTargets=" << config.render.maxRenderTargets << "\n";
|
||||
oss << "allowShaderHotReload=" << (config.render.allowShaderHotReload ? "true" : "false") << "\n";
|
||||
oss << "shaderCachePath=" << config.render.shaderCachePath << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[audio]\n";
|
||||
oss << "enabled=" << (config.audio.enabled ? "true" : "false") << "\n";
|
||||
oss << "masterVolume=" << config.audio.masterVolume << "\n";
|
||||
oss << "musicVolume=" << config.audio.musicVolume << "\n";
|
||||
oss << "sfxVolume=" << config.audio.sfxVolume << "\n";
|
||||
oss << "voiceVolume=" << config.audio.voiceVolume << "\n";
|
||||
oss << "ambientVolume=" << config.audio.ambientVolume << "\n";
|
||||
oss << "frequency=" << config.audio.frequency << "\n";
|
||||
oss << "channels=" << config.audio.channels << "\n";
|
||||
oss << "chunkSize=" << config.audio.chunkSize << "\n";
|
||||
oss << "maxChannels=" << config.audio.maxChannels << "\n";
|
||||
oss << "spatialAudio=" << (config.audio.spatialAudio ? "true" : "false") << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[debug]\n";
|
||||
oss << "enabled=" << (config.debug.enabled ? "true" : "false") << "\n";
|
||||
oss << "showFPS=" << (config.debug.showFPS ? "true" : "false") << "\n";
|
||||
oss << "showMemoryUsage=" << (config.debug.showMemoryUsage ? "true" : "false") << "\n";
|
||||
oss << "showRenderStats=" << (config.debug.showRenderStats ? "true" : "false") << "\n";
|
||||
oss << "showColliders=" << (config.debug.showColliders ? "true" : "false") << "\n";
|
||||
oss << "showGrid=" << (config.debug.showGrid ? "true" : "false") << "\n";
|
||||
oss << "logToFile=" << (config.debug.logToFile ? "true" : "false") << "\n";
|
||||
oss << "logToConsole=" << (config.debug.logToConsole ? "true" : "false") << "\n";
|
||||
oss << "logLevel=" << config.debug.logLevel << "\n";
|
||||
oss << "breakOnAssert=" << (config.debug.breakOnAssert ? "true" : "false") << "\n";
|
||||
oss << "enableProfiling=" << (config.debug.enableProfiling ? "true" : "false") << "\n";
|
||||
oss << "logFilePath=" << config.debug.logFilePath << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[input]\n";
|
||||
oss << "enabled=" << (config.input.enabled ? "true" : "false") << "\n";
|
||||
oss << "rawMouseInput=" << (config.input.rawMouseInput ? "true" : "false") << "\n";
|
||||
oss << "mouseSensitivity=" << config.input.mouseSensitivity << "\n";
|
||||
oss << "invertMouseY=" << (config.input.invertMouseY ? "true" : "false") << "\n";
|
||||
oss << "invertMouseX=" << (config.input.invertMouseX ? "true" : "false") << "\n";
|
||||
oss << "deadzone=" << config.input.deadzone << "\n";
|
||||
oss << "triggerThreshold=" << config.input.triggerThreshold << "\n";
|
||||
oss << "enableVibration=" << (config.input.enableVibration ? "true" : "false") << "\n";
|
||||
oss << "maxGamepads=" << config.input.maxGamepads << "\n";
|
||||
oss << "autoConnectGamepads=" << (config.input.autoConnectGamepads ? "true" : "false") << "\n";
|
||||
oss << "gamepadMappingFile=" << config.input.gamepadMappingFile << "\n";
|
||||
oss << "\n";
|
||||
|
||||
oss << "[resource]\n";
|
||||
oss << "assetRootPath=" << config.resource.assetRootPath << "\n";
|
||||
oss << "cachePath=" << config.resource.cachePath << "\n";
|
||||
oss << "savePath=" << config.resource.savePath << "\n";
|
||||
oss << "configPath=" << config.resource.configPath << "\n";
|
||||
oss << "logPath=" << config.resource.logPath << "\n";
|
||||
oss << "useAssetCache=" << (config.resource.useAssetCache ? "true" : "false") << "\n";
|
||||
oss << "maxCacheSize=" << config.resource.maxCacheSize << "\n";
|
||||
oss << "hotReloadEnabled=" << (config.resource.hotReloadEnabled ? "true" : "false") << "\n";
|
||||
oss << "hotReloadInterval=" << config.resource.hotReloadInterval << "\n";
|
||||
oss << "compressTextures=" << (config.resource.compressTextures ? "true" : "false") << "\n";
|
||||
oss << "preloadCommonAssets=" << (config.resource.preloadCommonAssets ? "true" : "false") << "\n";
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定文件
|
||||
* @param filepath 文件路径
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
bool IniConfigLoader::supportsFile(const std::string& filepath) const {
|
||||
if (filepath.length() >= 4) {
|
||||
std::string ext = filepath.substr(filepath.length() - 4);
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(c));
|
||||
return ext == ".ini";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 克隆加载器实例
|
||||
* @return 新的加载器实例
|
||||
*/
|
||||
UniquePtr<ConfigLoader> IniConfigLoader::clone() const {
|
||||
return makeUnique<IniConfigLoader>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成节键组合字符串
|
||||
* @param section 节名
|
||||
* @param key 键名
|
||||
* @return 组合字符串
|
||||
*/
|
||||
std::string IniConfigLoader::sectionKey(const std::string& section, const std::string& key) const {
|
||||
return section + "." + key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析整数
|
||||
* @param value 字符串值
|
||||
* @param result 输出整数结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseInt(const std::string& value, int& result, const std::string& fieldName) {
|
||||
try {
|
||||
size_t pos;
|
||||
result = std::stoi(value, &pos);
|
||||
if (pos != value.length()) {
|
||||
return ConfigLoadResult::error("无法解析整数值: " + value, -1, fieldName);
|
||||
}
|
||||
return ConfigLoadResult::ok();
|
||||
} catch (const std::exception& e) {
|
||||
return ConfigLoadResult::error(std::string("解析整数失败: ") + e.what(), -1, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析浮点数
|
||||
* @param value 字符串值
|
||||
* @param result 输出浮点数结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseFloat(const std::string& value, float& result, const std::string& fieldName) {
|
||||
try {
|
||||
size_t pos;
|
||||
result = std::stof(value, &pos);
|
||||
if (pos != value.length()) {
|
||||
return ConfigLoadResult::error("无法解析浮点数值: " + value, -1, fieldName);
|
||||
}
|
||||
return ConfigLoadResult::ok();
|
||||
} catch (const std::exception& e) {
|
||||
return ConfigLoadResult::error(std::string("解析浮点数失败: ") + e.what(), -1, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析布尔值
|
||||
* @param value 字符串值
|
||||
* @param result 输出布尔结果
|
||||
* @param fieldName 字段名(用于错误报告)
|
||||
* @return 解析结果
|
||||
*/
|
||||
ConfigLoadResult IniConfigLoader::parseBool(const std::string& value, bool& result, const std::string& fieldName) {
|
||||
std::string lower = toLower(value);
|
||||
if (lower == "true" || lower == "1" || lower == "yes" || lower == "on") {
|
||||
result = true;
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
if (lower == "false" || lower == "0" || lower == "no" || lower == "off") {
|
||||
result = false;
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
return ConfigLoadResult::error("无法解析布尔值: " + value, -1, fieldName);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,763 @@
|
|||
#include <extra2d/config/config_loader.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为窗口模式枚举
|
||||
* @param modeStr 窗口模式字符串
|
||||
* @return 窗口模式枚举值
|
||||
*/
|
||||
static WindowMode stringToWindowMode(const std::string& modeStr) {
|
||||
if (modeStr == "fullscreen") return WindowMode::Fullscreen;
|
||||
if (modeStr == "borderless") return WindowMode::Borderless;
|
||||
return WindowMode::Windowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将窗口模式枚举转换为字符串
|
||||
* @param mode 窗口模式枚举值
|
||||
* @return 窗口模式字符串
|
||||
*/
|
||||
static std::string windowModeToString(WindowMode mode) {
|
||||
switch (mode) {
|
||||
case WindowMode::Fullscreen: return "fullscreen";
|
||||
case WindowMode::Borderless: return "borderless";
|
||||
default: return "windowed";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为渲染后端类型枚举
|
||||
* @param backendStr 后端类型字符串
|
||||
* @return 渲染后端类型枚举值
|
||||
*/
|
||||
static BackendType stringToBackendType(const std::string& backendStr) {
|
||||
if (backendStr == "opengl") return BackendType::OpenGL;
|
||||
return BackendType::OpenGL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将渲染后端类型枚举转换为字符串
|
||||
* @param backend 渲染后端类型枚举值
|
||||
* @return 后端类型字符串
|
||||
*/
|
||||
static std::string backendTypeToString(BackendType backend) {
|
||||
switch (backend) {
|
||||
case BackendType::OpenGL: return "opengl";
|
||||
default: return "opengl";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JsonConfigLoader 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 文件加载配置
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::load(const std::string& filepath, AppConfig& config) {
|
||||
E2D_LOG_INFO("正在从 JSON 文件加载配置: {}", filepath);
|
||||
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法打开配置文件: {}", filepath);
|
||||
return ConfigLoadResult::error("无法打开配置文件: " + filepath);
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
file.close();
|
||||
|
||||
return loadFromString(content, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 文件
|
||||
* @param filepath 配置文件路径
|
||||
* @param config 要保存的配置对象
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult JsonConfigLoader::save(const std::string& filepath, const AppConfig& config) {
|
||||
E2D_LOG_INFO("正在保存配置到 JSON 文件: {}", filepath);
|
||||
|
||||
std::string content = saveToString(config);
|
||||
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
E2D_LOG_ERROR("无法创建配置文件: {}", filepath);
|
||||
return ConfigSaveResult::error("无法创建配置文件: " + filepath);
|
||||
}
|
||||
|
||||
file << content;
|
||||
file.close();
|
||||
|
||||
E2D_LOG_INFO("配置已成功保存到: {}", filepath);
|
||||
return ConfigSaveResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 字符串加载配置
|
||||
* @param content JSON 内容字符串
|
||||
* @param config 输出的配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::loadFromString(const std::string& content, AppConfig& config) {
|
||||
json root;
|
||||
|
||||
try {
|
||||
root = json::parse(content);
|
||||
} catch (const json::parse_error& e) {
|
||||
E2D_LOG_ERROR("JSON 解析错误: {}", e.what());
|
||||
return ConfigLoadResult::error(std::string("JSON 解析错误: ") + e.what(),
|
||||
static_cast<int>(e.byte));
|
||||
}
|
||||
|
||||
if (root.contains("appName") && root["appName"].is_string()) {
|
||||
config.appName = root["appName"].get<std::string>();
|
||||
}
|
||||
|
||||
if (root.contains("appVersion") && root["appVersion"].is_string()) {
|
||||
config.appVersion = root["appVersion"].get<std::string>();
|
||||
}
|
||||
|
||||
if (root.contains("organization") && root["organization"].is_string()) {
|
||||
config.organization = root["organization"].get<std::string>();
|
||||
}
|
||||
|
||||
if (root.contains("window")) {
|
||||
auto result = parseWindowConfig(&root["window"], config.window);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("render")) {
|
||||
auto result = parseRenderConfig(&root["render"], config.render);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("audio")) {
|
||||
auto result = parseAudioConfig(&root["audio"], config.audio);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("debug")) {
|
||||
auto result = parseDebugConfig(&root["debug"], config.debug);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("input")) {
|
||||
auto result = parseInputConfig(&root["input"], config.input);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.contains("resource")) {
|
||||
auto result = parseResourceConfig(&root["resource"], config.resource);
|
||||
if (result.hasError()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("JSON 配置加载成功");
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置序列化为 JSON 字符串
|
||||
* @param config 配置对象
|
||||
* @return 序列化后的 JSON 字符串
|
||||
*/
|
||||
std::string JsonConfigLoader::saveToString(const AppConfig& config) {
|
||||
json root;
|
||||
|
||||
root["appName"] = config.appName;
|
||||
root["appVersion"] = config.appVersion;
|
||||
root["organization"] = config.organization;
|
||||
|
||||
serializeWindowConfig(&root, config.window);
|
||||
serializeRenderConfig(&root, config.render);
|
||||
serializeAudioConfig(&root, config.audio);
|
||||
serializeDebugConfig(&root, config.debug);
|
||||
serializeInputConfig(&root, config.input);
|
||||
serializeResourceConfig(&root, config.resource);
|
||||
|
||||
return root.dump(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持指定文件
|
||||
* @param filepath 文件路径
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
bool JsonConfigLoader::supportsFile(const std::string& filepath) const {
|
||||
if (filepath.length() >= 5) {
|
||||
std::string ext = filepath.substr(filepath.length() - 5);
|
||||
for (char& c : ext) c = static_cast<char>(std::tolower(c));
|
||||
return ext == ".json";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 克隆加载器实例
|
||||
* @return 新的加载器实例
|
||||
*/
|
||||
UniquePtr<ConfigLoader> JsonConfigLoader::clone() const {
|
||||
return makeUnique<JsonConfigLoader>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析窗口配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param window 输出的窗口配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseWindowConfig(const void* jsonValue, WindowConfigData& window) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("window 配置必须是一个对象", -1, "window");
|
||||
}
|
||||
|
||||
if (obj.contains("title") && obj["title"].is_string()) {
|
||||
window.title = obj["title"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("width") && obj["width"].is_number_integer()) {
|
||||
window.width = obj["width"].get<int>();
|
||||
}
|
||||
if (obj.contains("height") && obj["height"].is_number_integer()) {
|
||||
window.height = obj["height"].get<int>();
|
||||
}
|
||||
if (obj.contains("minWidth") && obj["minWidth"].is_number_integer()) {
|
||||
window.minWidth = obj["minWidth"].get<int>();
|
||||
}
|
||||
if (obj.contains("minHeight") && obj["minHeight"].is_number_integer()) {
|
||||
window.minHeight = obj["minHeight"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxWidth") && obj["maxWidth"].is_number_integer()) {
|
||||
window.maxWidth = obj["maxWidth"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxHeight") && obj["maxHeight"].is_number_integer()) {
|
||||
window.maxHeight = obj["maxHeight"].get<int>();
|
||||
}
|
||||
if (obj.contains("mode") && obj["mode"].is_string()) {
|
||||
window.mode = stringToWindowMode(obj["mode"].get<std::string>());
|
||||
}
|
||||
if (obj.contains("resizable") && obj["resizable"].is_boolean()) {
|
||||
window.resizable = obj["resizable"].get<bool>();
|
||||
}
|
||||
if (obj.contains("borderless") && obj["borderless"].is_boolean()) {
|
||||
window.borderless = obj["borderless"].get<bool>();
|
||||
}
|
||||
if (obj.contains("alwaysOnTop") && obj["alwaysOnTop"].is_boolean()) {
|
||||
window.alwaysOnTop = obj["alwaysOnTop"].get<bool>();
|
||||
}
|
||||
if (obj.contains("centered") && obj["centered"].is_boolean()) {
|
||||
window.centered = obj["centered"].get<bool>();
|
||||
}
|
||||
if (obj.contains("posX") && obj["posX"].is_number_integer()) {
|
||||
window.posX = obj["posX"].get<int>();
|
||||
}
|
||||
if (obj.contains("posY") && obj["posY"].is_number_integer()) {
|
||||
window.posY = obj["posY"].get<int>();
|
||||
}
|
||||
if (obj.contains("hideOnClose") && obj["hideOnClose"].is_boolean()) {
|
||||
window.hideOnClose = obj["hideOnClose"].get<bool>();
|
||||
}
|
||||
if (obj.contains("minimizeOnClose") && obj["minimizeOnClose"].is_boolean()) {
|
||||
window.minimizeOnClose = obj["minimizeOnClose"].get<bool>();
|
||||
}
|
||||
if (obj.contains("opacity") && obj["opacity"].is_number()) {
|
||||
window.opacity = obj["opacity"].get<float>();
|
||||
}
|
||||
if (obj.contains("transparentFramebuffer") && obj["transparentFramebuffer"].is_boolean()) {
|
||||
window.transparentFramebuffer = obj["transparentFramebuffer"].get<bool>();
|
||||
}
|
||||
if (obj.contains("highDPI") && obj["highDPI"].is_boolean()) {
|
||||
window.highDPI = obj["highDPI"].get<bool>();
|
||||
}
|
||||
if (obj.contains("contentScale") && obj["contentScale"].is_number()) {
|
||||
window.contentScale = obj["contentScale"].get<float>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析渲染配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param render 输出的渲染配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseRenderConfig(const void* jsonValue, RenderConfigData& render) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("render 配置必须是一个对象", -1, "render");
|
||||
}
|
||||
|
||||
if (obj.contains("backend") && obj["backend"].is_string()) {
|
||||
render.backend = stringToBackendType(obj["backend"].get<std::string>());
|
||||
}
|
||||
if (obj.contains("targetFPS") && obj["targetFPS"].is_number_integer()) {
|
||||
render.targetFPS = obj["targetFPS"].get<int>();
|
||||
}
|
||||
if (obj.contains("vsync") && obj["vsync"].is_boolean()) {
|
||||
render.vsync = obj["vsync"].get<bool>();
|
||||
}
|
||||
if (obj.contains("tripleBuffering") && obj["tripleBuffering"].is_boolean()) {
|
||||
render.tripleBuffering = obj["tripleBuffering"].get<bool>();
|
||||
}
|
||||
if (obj.contains("multisamples") && obj["multisamples"].is_number_integer()) {
|
||||
render.multisamples = obj["multisamples"].get<int>();
|
||||
}
|
||||
if (obj.contains("sRGBFramebuffer") && obj["sRGBFramebuffer"].is_boolean()) {
|
||||
render.sRGBFramebuffer = obj["sRGBFramebuffer"].get<bool>();
|
||||
}
|
||||
if (obj.contains("clearColor") && obj["clearColor"].is_array() && obj["clearColor"].size() >= 4) {
|
||||
render.clearColor.r = obj["clearColor"][0].get<float>();
|
||||
render.clearColor.g = obj["clearColor"][1].get<float>();
|
||||
render.clearColor.b = obj["clearColor"][2].get<float>();
|
||||
render.clearColor.a = obj["clearColor"][3].get<float>();
|
||||
}
|
||||
if (obj.contains("maxTextureSize") && obj["maxTextureSize"].is_number_integer()) {
|
||||
render.maxTextureSize = obj["maxTextureSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("textureAnisotropy") && obj["textureAnisotropy"].is_number_integer()) {
|
||||
render.textureAnisotropy = obj["textureAnisotropy"].get<int>();
|
||||
}
|
||||
if (obj.contains("wireframeMode") && obj["wireframeMode"].is_boolean()) {
|
||||
render.wireframeMode = obj["wireframeMode"].get<bool>();
|
||||
}
|
||||
if (obj.contains("depthTest") && obj["depthTest"].is_boolean()) {
|
||||
render.depthTest = obj["depthTest"].get<bool>();
|
||||
}
|
||||
if (obj.contains("blending") && obj["blending"].is_boolean()) {
|
||||
render.blending = obj["blending"].get<bool>();
|
||||
}
|
||||
if (obj.contains("dithering") && obj["dithering"].is_boolean()) {
|
||||
render.dithering = obj["dithering"].get<bool>();
|
||||
}
|
||||
if (obj.contains("spriteBatchSize") && obj["spriteBatchSize"].is_number_integer()) {
|
||||
render.spriteBatchSize = obj["spriteBatchSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxRenderTargets") && obj["maxRenderTargets"].is_number_integer()) {
|
||||
render.maxRenderTargets = obj["maxRenderTargets"].get<int>();
|
||||
}
|
||||
if (obj.contains("allowShaderHotReload") && obj["allowShaderHotReload"].is_boolean()) {
|
||||
render.allowShaderHotReload = obj["allowShaderHotReload"].get<bool>();
|
||||
}
|
||||
if (obj.contains("shaderCachePath") && obj["shaderCachePath"].is_string()) {
|
||||
render.shaderCachePath = obj["shaderCachePath"].get<std::string>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析音频配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param audio 输出的音频配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseAudioConfig(const void* jsonValue, AudioConfigData& audio) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("audio 配置必须是一个对象", -1, "audio");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
audio.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("masterVolume") && obj["masterVolume"].is_number_integer()) {
|
||||
audio.masterVolume = obj["masterVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("musicVolume") && obj["musicVolume"].is_number_integer()) {
|
||||
audio.musicVolume = obj["musicVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("sfxVolume") && obj["sfxVolume"].is_number_integer()) {
|
||||
audio.sfxVolume = obj["sfxVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("voiceVolume") && obj["voiceVolume"].is_number_integer()) {
|
||||
audio.voiceVolume = obj["voiceVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("ambientVolume") && obj["ambientVolume"].is_number_integer()) {
|
||||
audio.ambientVolume = obj["ambientVolume"].get<int>();
|
||||
}
|
||||
if (obj.contains("frequency") && obj["frequency"].is_number_integer()) {
|
||||
audio.frequency = obj["frequency"].get<int>();
|
||||
}
|
||||
if (obj.contains("channels") && obj["channels"].is_number_integer()) {
|
||||
audio.channels = obj["channels"].get<int>();
|
||||
}
|
||||
if (obj.contains("chunkSize") && obj["chunkSize"].is_number_integer()) {
|
||||
audio.chunkSize = obj["chunkSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("maxChannels") && obj["maxChannels"].is_number_integer()) {
|
||||
audio.maxChannels = obj["maxChannels"].get<int>();
|
||||
}
|
||||
if (obj.contains("spatialAudio") && obj["spatialAudio"].is_boolean()) {
|
||||
audio.spatialAudio = obj["spatialAudio"].get<bool>();
|
||||
}
|
||||
if (obj.contains("listenerPosition") && obj["listenerPosition"].is_array() && obj["listenerPosition"].size() >= 3) {
|
||||
audio.listenerPosition[0] = obj["listenerPosition"][0].get<float>();
|
||||
audio.listenerPosition[1] = obj["listenerPosition"][1].get<float>();
|
||||
audio.listenerPosition[2] = obj["listenerPosition"][2].get<float>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析调试配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param debug 输出的调试配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseDebugConfig(const void* jsonValue, DebugConfigData& debug) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("debug 配置必须是一个对象", -1, "debug");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
debug.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showFPS") && obj["showFPS"].is_boolean()) {
|
||||
debug.showFPS = obj["showFPS"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showMemoryUsage") && obj["showMemoryUsage"].is_boolean()) {
|
||||
debug.showMemoryUsage = obj["showMemoryUsage"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showRenderStats") && obj["showRenderStats"].is_boolean()) {
|
||||
debug.showRenderStats = obj["showRenderStats"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showColliders") && obj["showColliders"].is_boolean()) {
|
||||
debug.showColliders = obj["showColliders"].get<bool>();
|
||||
}
|
||||
if (obj.contains("showGrid") && obj["showGrid"].is_boolean()) {
|
||||
debug.showGrid = obj["showGrid"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logToFile") && obj["logToFile"].is_boolean()) {
|
||||
debug.logToFile = obj["logToFile"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logToConsole") && obj["logToConsole"].is_boolean()) {
|
||||
debug.logToConsole = obj["logToConsole"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logLevel") && obj["logLevel"].is_number_integer()) {
|
||||
debug.logLevel = obj["logLevel"].get<int>();
|
||||
}
|
||||
if (obj.contains("breakOnAssert") && obj["breakOnAssert"].is_boolean()) {
|
||||
debug.breakOnAssert = obj["breakOnAssert"].get<bool>();
|
||||
}
|
||||
if (obj.contains("enableProfiling") && obj["enableProfiling"].is_boolean()) {
|
||||
debug.enableProfiling = obj["enableProfiling"].get<bool>();
|
||||
}
|
||||
if (obj.contains("logFilePath") && obj["logFilePath"].is_string()) {
|
||||
debug.logFilePath = obj["logFilePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("debugFlags") && obj["debugFlags"].is_array()) {
|
||||
debug.debugFlags.clear();
|
||||
for (const auto& flag : obj["debugFlags"]) {
|
||||
if (flag.is_string()) {
|
||||
debug.debugFlags.push_back(flag.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析输入配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param input 输出的输入配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseInputConfig(const void* jsonValue, InputConfigData& input) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("input 配置必须是一个对象", -1, "input");
|
||||
}
|
||||
|
||||
if (obj.contains("enabled") && obj["enabled"].is_boolean()) {
|
||||
input.enabled = obj["enabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("rawMouseInput") && obj["rawMouseInput"].is_boolean()) {
|
||||
input.rawMouseInput = obj["rawMouseInput"].get<bool>();
|
||||
}
|
||||
if (obj.contains("mouseSensitivity") && obj["mouseSensitivity"].is_number()) {
|
||||
input.mouseSensitivity = obj["mouseSensitivity"].get<float>();
|
||||
}
|
||||
if (obj.contains("invertMouseY") && obj["invertMouseY"].is_boolean()) {
|
||||
input.invertMouseY = obj["invertMouseY"].get<bool>();
|
||||
}
|
||||
if (obj.contains("invertMouseX") && obj["invertMouseX"].is_boolean()) {
|
||||
input.invertMouseX = obj["invertMouseX"].get<bool>();
|
||||
}
|
||||
if (obj.contains("deadzone") && obj["deadzone"].is_number()) {
|
||||
input.deadzone = obj["deadzone"].get<float>();
|
||||
}
|
||||
if (obj.contains("triggerThreshold") && obj["triggerThreshold"].is_number()) {
|
||||
input.triggerThreshold = obj["triggerThreshold"].get<float>();
|
||||
}
|
||||
if (obj.contains("enableVibration") && obj["enableVibration"].is_boolean()) {
|
||||
input.enableVibration = obj["enableVibration"].get<bool>();
|
||||
}
|
||||
if (obj.contains("maxGamepads") && obj["maxGamepads"].is_number_integer()) {
|
||||
input.maxGamepads = obj["maxGamepads"].get<int>();
|
||||
}
|
||||
if (obj.contains("autoConnectGamepads") && obj["autoConnectGamepads"].is_boolean()) {
|
||||
input.autoConnectGamepads = obj["autoConnectGamepads"].get<bool>();
|
||||
}
|
||||
if (obj.contains("gamepadMappingFile") && obj["gamepadMappingFile"].is_string()) {
|
||||
input.gamepadMappingFile = obj["gamepadMappingFile"].get<std::string>();
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析资源配置
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param resource 输出的资源配置对象
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult JsonConfigLoader::parseResourceConfig(const void* jsonValue, ResourceConfigData& resource) {
|
||||
const json& obj = *static_cast<const json*>(jsonValue);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
return ConfigLoadResult::error("resource 配置必须是一个对象", -1, "resource");
|
||||
}
|
||||
|
||||
if (obj.contains("assetRootPath") && obj["assetRootPath"].is_string()) {
|
||||
resource.assetRootPath = obj["assetRootPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("cachePath") && obj["cachePath"].is_string()) {
|
||||
resource.cachePath = obj["cachePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("savePath") && obj["savePath"].is_string()) {
|
||||
resource.savePath = obj["savePath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("configPath") && obj["configPath"].is_string()) {
|
||||
resource.configPath = obj["configPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("logPath") && obj["logPath"].is_string()) {
|
||||
resource.logPath = obj["logPath"].get<std::string>();
|
||||
}
|
||||
if (obj.contains("useAssetCache") && obj["useAssetCache"].is_boolean()) {
|
||||
resource.useAssetCache = obj["useAssetCache"].get<bool>();
|
||||
}
|
||||
if (obj.contains("maxCacheSize") && obj["maxCacheSize"].is_number_integer()) {
|
||||
resource.maxCacheSize = obj["maxCacheSize"].get<int>();
|
||||
}
|
||||
if (obj.contains("hotReloadEnabled") && obj["hotReloadEnabled"].is_boolean()) {
|
||||
resource.hotReloadEnabled = obj["hotReloadEnabled"].get<bool>();
|
||||
}
|
||||
if (obj.contains("hotReloadInterval") && obj["hotReloadInterval"].is_number()) {
|
||||
resource.hotReloadInterval = obj["hotReloadInterval"].get<float>();
|
||||
}
|
||||
if (obj.contains("compressTextures") && obj["compressTextures"].is_boolean()) {
|
||||
resource.compressTextures = obj["compressTextures"].get<bool>();
|
||||
}
|
||||
if (obj.contains("preloadCommonAssets") && obj["preloadCommonAssets"].is_boolean()) {
|
||||
resource.preloadCommonAssets = obj["preloadCommonAssets"].get<bool>();
|
||||
}
|
||||
if (obj.contains("searchPaths") && obj["searchPaths"].is_array()) {
|
||||
resource.searchPaths.clear();
|
||||
for (const auto& path : obj["searchPaths"]) {
|
||||
if (path.is_string()) {
|
||||
resource.searchPaths.push_back(path.get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigLoadResult::ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化窗口配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param window 窗口配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeWindowConfig(void* jsonValue, const WindowConfigData& window) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["title"] = window.title;
|
||||
obj["width"] = window.width;
|
||||
obj["height"] = window.height;
|
||||
obj["minWidth"] = window.minWidth;
|
||||
obj["minHeight"] = window.minHeight;
|
||||
obj["maxWidth"] = window.maxWidth;
|
||||
obj["maxHeight"] = window.maxHeight;
|
||||
obj["mode"] = windowModeToString(window.mode);
|
||||
obj["resizable"] = window.resizable;
|
||||
obj["borderless"] = window.borderless;
|
||||
obj["alwaysOnTop"] = window.alwaysOnTop;
|
||||
obj["centered"] = window.centered;
|
||||
obj["posX"] = window.posX;
|
||||
obj["posY"] = window.posY;
|
||||
obj["hideOnClose"] = window.hideOnClose;
|
||||
obj["minimizeOnClose"] = window.minimizeOnClose;
|
||||
obj["opacity"] = window.opacity;
|
||||
obj["transparentFramebuffer"] = window.transparentFramebuffer;
|
||||
obj["highDPI"] = window.highDPI;
|
||||
obj["contentScale"] = window.contentScale;
|
||||
|
||||
root["window"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化渲染配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param render 渲染配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeRenderConfig(void* jsonValue, const RenderConfigData& render) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["backend"] = backendTypeToString(render.backend);
|
||||
obj["targetFPS"] = render.targetFPS;
|
||||
obj["vsync"] = render.vsync;
|
||||
obj["tripleBuffering"] = render.tripleBuffering;
|
||||
obj["multisamples"] = render.multisamples;
|
||||
obj["sRGBFramebuffer"] = render.sRGBFramebuffer;
|
||||
obj["clearColor"] = {render.clearColor.r, render.clearColor.g, render.clearColor.b, render.clearColor.a};
|
||||
obj["maxTextureSize"] = render.maxTextureSize;
|
||||
obj["textureAnisotropy"] = render.textureAnisotropy;
|
||||
obj["wireframeMode"] = render.wireframeMode;
|
||||
obj["depthTest"] = render.depthTest;
|
||||
obj["blending"] = render.blending;
|
||||
obj["dithering"] = render.dithering;
|
||||
obj["spriteBatchSize"] = render.spriteBatchSize;
|
||||
obj["maxRenderTargets"] = render.maxRenderTargets;
|
||||
obj["allowShaderHotReload"] = render.allowShaderHotReload;
|
||||
obj["shaderCachePath"] = render.shaderCachePath;
|
||||
|
||||
root["render"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化音频配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param audio 音频配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeAudioConfig(void* jsonValue, const AudioConfigData& audio) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = audio.enabled;
|
||||
obj["masterVolume"] = audio.masterVolume;
|
||||
obj["musicVolume"] = audio.musicVolume;
|
||||
obj["sfxVolume"] = audio.sfxVolume;
|
||||
obj["voiceVolume"] = audio.voiceVolume;
|
||||
obj["ambientVolume"] = audio.ambientVolume;
|
||||
obj["frequency"] = audio.frequency;
|
||||
obj["channels"] = audio.channels;
|
||||
obj["chunkSize"] = audio.chunkSize;
|
||||
obj["maxChannels"] = audio.maxChannels;
|
||||
obj["spatialAudio"] = audio.spatialAudio;
|
||||
obj["listenerPosition"] = {audio.listenerPosition[0], audio.listenerPosition[1], audio.listenerPosition[2]};
|
||||
|
||||
root["audio"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化调试配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param debug 调试配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeDebugConfig(void* jsonValue, const DebugConfigData& debug) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = debug.enabled;
|
||||
obj["showFPS"] = debug.showFPS;
|
||||
obj["showMemoryUsage"] = debug.showMemoryUsage;
|
||||
obj["showRenderStats"] = debug.showRenderStats;
|
||||
obj["showColliders"] = debug.showColliders;
|
||||
obj["showGrid"] = debug.showGrid;
|
||||
obj["logToFile"] = debug.logToFile;
|
||||
obj["logToConsole"] = debug.logToConsole;
|
||||
obj["logLevel"] = debug.logLevel;
|
||||
obj["breakOnAssert"] = debug.breakOnAssert;
|
||||
obj["enableProfiling"] = debug.enableProfiling;
|
||||
obj["logFilePath"] = debug.logFilePath;
|
||||
obj["debugFlags"] = debug.debugFlags;
|
||||
|
||||
root["debug"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化输入配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param input 输入配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeInputConfig(void* jsonValue, const InputConfigData& input) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["enabled"] = input.enabled;
|
||||
obj["rawMouseInput"] = input.rawMouseInput;
|
||||
obj["mouseSensitivity"] = input.mouseSensitivity;
|
||||
obj["invertMouseY"] = input.invertMouseY;
|
||||
obj["invertMouseX"] = input.invertMouseX;
|
||||
obj["deadzone"] = input.deadzone;
|
||||
obj["triggerThreshold"] = input.triggerThreshold;
|
||||
obj["enableVibration"] = input.enableVibration;
|
||||
obj["maxGamepads"] = input.maxGamepads;
|
||||
obj["autoConnectGamepads"] = input.autoConnectGamepads;
|
||||
obj["gamepadMappingFile"] = input.gamepadMappingFile;
|
||||
|
||||
root["input"] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 序列化资源配置到 JSON
|
||||
* @param jsonValue JSON 值指针
|
||||
* @param resource 资源配置对象
|
||||
*/
|
||||
void JsonConfigLoader::serializeResourceConfig(void* jsonValue, const ResourceConfigData& resource) {
|
||||
json& root = *static_cast<json*>(jsonValue);
|
||||
json obj;
|
||||
|
||||
obj["assetRootPath"] = resource.assetRootPath;
|
||||
obj["cachePath"] = resource.cachePath;
|
||||
obj["savePath"] = resource.savePath;
|
||||
obj["configPath"] = resource.configPath;
|
||||
obj["logPath"] = resource.logPath;
|
||||
obj["useAssetCache"] = resource.useAssetCache;
|
||||
obj["maxCacheSize"] = resource.maxCacheSize;
|
||||
obj["hotReloadEnabled"] = resource.hotReloadEnabled;
|
||||
obj["hotReloadInterval"] = resource.hotReloadInterval;
|
||||
obj["compressTextures"] = resource.compressTextures;
|
||||
obj["preloadCommonAssets"] = resource.preloadCommonAssets;
|
||||
obj["searchPaths"] = resource.searchPaths;
|
||||
|
||||
root["resource"] = obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,516 @@
|
|||
#include <extra2d/config/config_manager.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/config/platform_detector.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <sstream>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* 初始化配置管理器的成员变量
|
||||
*/
|
||||
ConfigManager::ConfigManager()
|
||||
: m_nextCallbackId(1)
|
||||
, m_autoSaveEnabled(false)
|
||||
, m_autoSaveInterval(30.0f)
|
||||
, m_autoSaveTimer(0.0f)
|
||||
{
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
* 确保在销毁时关闭配置管理器
|
||||
*/
|
||||
ConfigManager::~ConfigManager() {
|
||||
if (m_initialized) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* 使用静态局部变量实现线程安全的单例模式
|
||||
* @return 配置管理器实例引用
|
||||
*/
|
||||
ConfigManager& ConfigManager::instance() {
|
||||
static ConfigManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化配置管理器
|
||||
* 创建平台配置和配置加载器
|
||||
* @param configPath 配置文件路径
|
||||
* @return 如果初始化成功返回 true
|
||||
*/
|
||||
bool ConfigManager::initialize(const std::string& configPath) {
|
||||
if (m_initialized) {
|
||||
E2D_LOG_WARN("ConfigManager already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_configPath = configPath;
|
||||
|
||||
m_platformConfig = createPlatformConfig(PlatformType::Auto);
|
||||
if (!m_platformConfig) {
|
||||
E2D_LOG_ERROR("Failed to create platform config");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_loader = makeUnique<JsonConfigLoader>();
|
||||
if (!m_loader) {
|
||||
E2D_LOG_ERROR("Failed to create config loader");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
m_appConfig.targetPlatform = PlatformDetector::detect();
|
||||
|
||||
m_platformConfig->applyDefaults(m_appConfig);
|
||||
|
||||
m_initialized = true;
|
||||
m_modified = false;
|
||||
|
||||
E2D_LOG_INFO("ConfigManager initialized for platform: {}",
|
||||
m_platformConfig->platformName());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭配置管理器
|
||||
* 清理所有资源并重置状态
|
||||
*/
|
||||
void ConfigManager::shutdown() {
|
||||
if (!m_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_autoSaveEnabled && m_modified) {
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
m_changeCallbacks.clear();
|
||||
m_moduleConfigs.clear();
|
||||
m_loader.reset();
|
||||
m_platformConfig.reset();
|
||||
|
||||
m_initialized = false;
|
||||
m_modified = false;
|
||||
|
||||
E2D_LOG_INFO("ConfigManager shutdown complete");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否已初始化
|
||||
* @return 如果已初始化返回 true
|
||||
*/
|
||||
bool ConfigManager::isInitialized() const {
|
||||
return m_initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载配置文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult ConfigManager::loadConfig(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string path = filepath.empty() ? m_configPath : filepath;
|
||||
if (path.empty()) {
|
||||
return ConfigLoadResult::error("No config file path specified");
|
||||
}
|
||||
|
||||
if (!m_loader) {
|
||||
return ConfigLoadResult::error("Config loader not initialized");
|
||||
}
|
||||
|
||||
AppConfig loadedConfig;
|
||||
ConfigLoadResult result = m_loader->load(path, loadedConfig);
|
||||
|
||||
if (result.success) {
|
||||
m_appConfig.merge(loadedConfig);
|
||||
|
||||
if (m_platformConfig) {
|
||||
m_appConfig.applyPlatformConstraints(*m_platformConfig);
|
||||
}
|
||||
|
||||
if (!m_appConfig.validate()) {
|
||||
E2D_LOG_WARN("Loaded config validation failed, using defaults");
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
}
|
||||
|
||||
m_configPath = path;
|
||||
m_modified = false;
|
||||
|
||||
E2D_LOG_INFO("Config loaded from: {}", path);
|
||||
} else {
|
||||
E2D_LOG_ERROR("Failed to load config from {}: {}", path, result.errorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到文件
|
||||
* @param filepath 配置文件路径(可选,默认使用初始化时的路径)
|
||||
* @return 保存结果
|
||||
*/
|
||||
ConfigSaveResult ConfigManager::saveConfig(const std::string& filepath) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::string path = filepath.empty() ? m_configPath : filepath;
|
||||
if (path.empty()) {
|
||||
return ConfigSaveResult::error("No config file path specified");
|
||||
}
|
||||
|
||||
if (!m_loader) {
|
||||
return ConfigSaveResult::error("Config loader not initialized");
|
||||
}
|
||||
|
||||
ConfigSaveResult result = m_loader->save(path, m_appConfig);
|
||||
|
||||
if (result.success) {
|
||||
m_configPath = path;
|
||||
m_modified = false;
|
||||
|
||||
E2D_LOG_INFO("Config saved to: {}", path);
|
||||
} else {
|
||||
E2D_LOG_ERROR("Failed to save config to {}: {}", path, result.errorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重新加载配置
|
||||
* @return 加载结果
|
||||
*/
|
||||
ConfigLoadResult ConfigManager::reload() {
|
||||
return loadConfig(m_configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取应用配置
|
||||
* @return 应用配置的常量引用
|
||||
*/
|
||||
const AppConfig& ConfigManager::appConfig() const {
|
||||
return m_appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取可修改的应用配置
|
||||
* @return 应用配置的引用
|
||||
*/
|
||||
AppConfig& ConfigManager::appConfig() {
|
||||
m_modified = true;
|
||||
return m_appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置应用配置
|
||||
* @param config 新的配置
|
||||
*/
|
||||
void ConfigManager::setAppConfig(const AppConfig& config) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_appConfig = config;
|
||||
m_modified = true;
|
||||
|
||||
E2D_LOG_INFO("App config updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置
|
||||
* @return 平台配置接口指针
|
||||
*/
|
||||
PlatformConfig* ConfigManager::platformConfig() {
|
||||
return m_platformConfig.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台配置(常量版本)
|
||||
* @return 平台配置接口常量指针
|
||||
*/
|
||||
const PlatformConfig* ConfigManager::platformConfig() const {
|
||||
return m_platformConfig.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册配置变更回调
|
||||
* @param callback 回调函数
|
||||
* @return 回调ID,用于取消注册
|
||||
*/
|
||||
int ConfigManager::registerChangeCallback(ConfigChangeCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
int id = m_nextCallbackId++;
|
||||
m_changeCallbacks[id] = std::move(callback);
|
||||
|
||||
E2D_LOG_DEBUG("Registered config change callback with id {}", id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取消注册配置变更回调
|
||||
* @param callbackId 回调ID
|
||||
*/
|
||||
void ConfigManager::unregisterChangeCallback(int callbackId) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto it = m_changeCallbacks.find(callbackId);
|
||||
if (it != m_changeCallbacks.end()) {
|
||||
m_changeCallbacks.erase(it);
|
||||
E2D_LOG_DEBUG("Unregistered config change callback {}", callbackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除所有变更回调
|
||||
*/
|
||||
void ConfigManager::clearChangeCallbacks() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_changeCallbacks.clear();
|
||||
E2D_LOG_DEBUG("Cleared all config change callbacks");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册模块配置
|
||||
* @param moduleName 模块名称
|
||||
* @param config 模块配置指针
|
||||
*/
|
||||
void ConfigManager::registerModuleConfig(const std::string& moduleName, Ptr<void> config) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_moduleConfigs[moduleName] = config;
|
||||
E2D_LOG_DEBUG("Registered module config: {}", moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 移除模块配置
|
||||
* @param moduleName 模块名称
|
||||
*/
|
||||
void ConfigManager::removeModuleConfig(const std::string& moduleName) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
auto it = m_moduleConfigs.find(moduleName);
|
||||
if (it != m_moduleConfigs.end()) {
|
||||
m_moduleConfigs.erase(it);
|
||||
E2D_LOG_DEBUG("Removed module config: {}", moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查模块配置是否存在
|
||||
* @param moduleName 模块名称
|
||||
* @return 如果存在返回 true
|
||||
*/
|
||||
bool ConfigManager::hasModuleConfig(const std::string& moduleName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_moduleConfigs.find(moduleName) != m_moduleConfigs.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, const std::string& value) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
ConfigChangeEvent event;
|
||||
event.section = section;
|
||||
event.field = key;
|
||||
event.newValue = value;
|
||||
|
||||
m_modified = true;
|
||||
notifyChangeCallbacks(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, int value) {
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
setValue(section, key, oss.str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, float value) {
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
setValue(section, key, oss.str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param value 配置值
|
||||
*/
|
||||
void ConfigManager::setValue(const std::string& section, const std::string& key, bool value) {
|
||||
setValue(section, key, std::string(value ? "true" : "false"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(字符串)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
std::string ConfigManager::getValue(const std::string& section, const std::string& key,
|
||||
const std::string& defaultValue) const {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(整数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
int ConfigManager::getIntValue(const std::string& section, const std::string& key, int defaultValue) const {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(浮点数)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
float ConfigManager::getFloatValue(const std::string& section, const std::string& key, float defaultValue) const {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取配置值(布尔值)
|
||||
* @param section 配置节
|
||||
* @param key 配置键
|
||||
* @param defaultValue 默认值
|
||||
* @return 配置值
|
||||
*/
|
||||
bool ConfigManager::getBoolValue(const std::string& section, const std::string& key, bool defaultValue) const {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置配置到默认值
|
||||
*/
|
||||
void ConfigManager::resetToDefaults() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_appConfig = AppConfig::createDefault();
|
||||
|
||||
if (m_platformConfig) {
|
||||
m_platformConfig->applyDefaults(m_appConfig);
|
||||
}
|
||||
|
||||
m_modified = true;
|
||||
|
||||
E2D_LOG_INFO("Config reset to defaults");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查配置是否有未保存的更改
|
||||
* @return 如果有未保存的更改返回 true
|
||||
*/
|
||||
bool ConfigManager::hasUnsavedChanges() const {
|
||||
return m_modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 标记配置为已修改
|
||||
*/
|
||||
void ConfigManager::markModified() {
|
||||
m_modified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除修改标记
|
||||
*/
|
||||
void ConfigManager::clearModified() {
|
||||
m_modified = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置自动保存
|
||||
* @param enabled 是否启用自动保存
|
||||
* @param interval 自动保存间隔(秒)
|
||||
*/
|
||||
void ConfigManager::setAutoSave(bool enabled, float interval) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
m_autoSaveEnabled = enabled;
|
||||
m_autoSaveInterval = interval > 0.0f ? interval : 30.0f;
|
||||
m_autoSaveTimer = 0.0f;
|
||||
|
||||
E2D_LOG_INFO("Auto save {} (interval: {}s)",
|
||||
enabled ? "enabled" : "disabled", m_autoSaveInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新配置管理器(用于自动保存)
|
||||
* @param deltaTime 帧时间(秒)
|
||||
*/
|
||||
void ConfigManager::update(float deltaTime) {
|
||||
if (!m_autoSaveEnabled || !m_modified) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_autoSaveTimer += deltaTime;
|
||||
|
||||
if (m_autoSaveTimer >= m_autoSaveInterval) {
|
||||
m_autoSaveTimer = 0.0f;
|
||||
saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通知所有变更回调
|
||||
* @param event 配置变更事件
|
||||
*/
|
||||
void ConfigManager::notifyChangeCallbacks(const ConfigChangeEvent& event) {
|
||||
for (const auto& pair : m_changeCallbacks) {
|
||||
if (pair.second) {
|
||||
pair.second(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置应用到内部存储
|
||||
* @param config 配置对象
|
||||
*/
|
||||
void ConfigManager::applyConfigToInternal(const AppConfig& config) {
|
||||
m_appConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从内部存储提取配置
|
||||
* @param config 输出配置对象
|
||||
*/
|
||||
void ConfigManager::extractConfigFromInternal(AppConfig& config) const {
|
||||
config = m_appConfig;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 获取单例实例
|
||||
* 使用静态局部变量实现线程安全的单例模式
|
||||
* @return 模块注册表实例引用
|
||||
*/
|
||||
ModuleRegistry& ModuleRegistry::instance() {
|
||||
static ModuleRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注册模块
|
||||
* 将模块配置和初始化器工厂添加到注册表
|
||||
* @param config 模块配置
|
||||
* @param initializerFactory 初始化器工厂函数(可选)
|
||||
* @return 分配的模块标识符
|
||||
*/
|
||||
ModuleId ModuleRegistry::registerModule(
|
||||
UniquePtr<IModuleConfig> config,
|
||||
ModuleInitializerFactory initializerFactory
|
||||
) {
|
||||
if (!config) {
|
||||
E2D_LOG_ERROR("Cannot register null module config");
|
||||
return INVALID_MODULE_ID;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
ModuleInfo info = config->getModuleInfo();
|
||||
|
||||
if (nameToId_.find(info.name) != nameToId_.end()) {
|
||||
E2D_LOG_ERROR("Module '{}' already registered", info.name);
|
||||
return INVALID_MODULE_ID;
|
||||
}
|
||||
|
||||
ModuleId id = generateId();
|
||||
|
||||
ModuleEntry entry;
|
||||
entry.id = id;
|
||||
entry.config = std::move(config);
|
||||
entry.initializerFactory = std::move(initializerFactory);
|
||||
entry.initialized = false;
|
||||
|
||||
modules_[id] = std::move(entry);
|
||||
nameToId_[info.name] = id;
|
||||
|
||||
E2D_LOG_INFO("Registered module '{}' with id {}", info.name, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 注销模块
|
||||
* 从注册表中移除指定模块
|
||||
* @param id 模块标识符
|
||||
* @return 注销成功返回 true
|
||||
*/
|
||||
bool ModuleRegistry::unregisterModule(ModuleId id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto it = modules_.find(id);
|
||||
if (it == modules_.end()) {
|
||||
E2D_LOG_WARN("Module with id {} not found for unregistration", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleInfo info = it->second.config->getModuleInfo();
|
||||
nameToId_.erase(info.name);
|
||||
modules_.erase(it);
|
||||
|
||||
E2D_LOG_INFO("Unregistered module '{}' (id: {})", info.name, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取模块配置
|
||||
* @param id 模块标识符
|
||||
* @return 模块配置指针,不存在返回 nullptr
|
||||
*/
|
||||
IModuleConfig* ModuleRegistry::getModuleConfig(ModuleId id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto it = modules_.find(id);
|
||||
if (it != modules_.end()) {
|
||||
return it->second.config.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据名称获取模块配置
|
||||
* @param name 模块名称
|
||||
* @return 模块配置指针,不存在返回 nullptr
|
||||
*/
|
||||
IModuleConfig* ModuleRegistry::getModuleConfigByName(const std::string& name) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto nameIt = nameToId_.find(name);
|
||||
if (nameIt == nameToId_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto moduleIt = modules_.find(nameIt->second);
|
||||
if (moduleIt != modules_.end()) {
|
||||
return moduleIt->second.config.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建模块初始化器
|
||||
* @param id 模块标识符
|
||||
* @return 初始化器实例,不存在返回 nullptr
|
||||
*/
|
||||
UniquePtr<IModuleInitializer> ModuleRegistry::createInitializer(ModuleId id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
auto it = modules_.find(id);
|
||||
if (it == modules_.end() || !it->second.initializerFactory) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return it->second.initializerFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取所有已注册模块标识符
|
||||
* @return 模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> ModuleRegistry::getAllModules() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::vector<ModuleId> result;
|
||||
result.reserve(modules_.size());
|
||||
|
||||
for (const auto& pair : modules_) {
|
||||
result.push_back(pair.first);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取模块初始化顺序
|
||||
* 根据优先级和依赖关系计算初始化顺序
|
||||
* @return 按初始化顺序排列的模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> ModuleRegistry::getInitializationOrder() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
std::vector<std::pair<ModuleId, int>> modulePriorities;
|
||||
modulePriorities.reserve(modules_.size());
|
||||
|
||||
for (const auto& pair : modules_) {
|
||||
ModuleInfo info = pair.second.config->getModuleInfo();
|
||||
if (info.enabled) {
|
||||
modulePriorities.emplace_back(pair.first, static_cast<int>(info.priority));
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(modulePriorities.begin(), modulePriorities.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.second < b.second;
|
||||
});
|
||||
|
||||
std::vector<ModuleId> result;
|
||||
result.reserve(modulePriorities.size());
|
||||
|
||||
for (const auto& pair : modulePriorities) {
|
||||
result.push_back(pair.first);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查模块是否存在
|
||||
* @param id 模块标识符
|
||||
* @return 存在返回 true
|
||||
*/
|
||||
bool ModuleRegistry::hasModule(ModuleId id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return modules_.find(id) != modules_.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清空所有注册的模块
|
||||
*/
|
||||
void ModuleRegistry::clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
|
||||
modules_.clear();
|
||||
nameToId_.clear();
|
||||
nextId_ = 1;
|
||||
|
||||
E2D_LOG_INFO("Module registry cleared");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 生成新的模块标识符
|
||||
* @return 新的模块标识符
|
||||
*/
|
||||
ModuleId ModuleRegistry::generateId() {
|
||||
return nextId_++;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/config/app_config.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#endif
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
namespace {
|
||||
|
||||
class WindowsPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
WindowsPlatformConfig() {
|
||||
caps_.supportsWindowed = true;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = true;
|
||||
caps_.supportsCursor = true;
|
||||
caps_.supportsCursorHide = true;
|
||||
caps_.supportsDPIAwareness = true;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = true;
|
||||
caps_.supportsClipboard = true;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = false;
|
||||
caps_.supportsKeyboard = true;
|
||||
caps_.supportsMouse = true;
|
||||
caps_.supportsResize = true;
|
||||
caps_.supportsHighDPI = true;
|
||||
caps_.maxTextureSize = 16384;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 96.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::Windows; }
|
||||
const char* platformName() const override { return "Windows"; }
|
||||
const PlatformCapabilities& capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig& config) const override {
|
||||
if (config.window.width < 320) config.window.width = 320;
|
||||
if (config.window.height < 240) config.window.height = 240;
|
||||
if (config.window.width > caps_.maxTextureSize) config.window.width = caps_.maxTextureSize;
|
||||
if (config.window.height > caps_.maxTextureSize) config.window.height = caps_.maxTextureSize;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig& config) const override {
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig& config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("Windows: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return width >= 320 && height >= 240 &&
|
||||
width <= caps_.maxTextureSize && height <= caps_.maxTextureSize;
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
|
||||
class LinuxPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
LinuxPlatformConfig() {
|
||||
caps_.supportsWindowed = true;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = true;
|
||||
caps_.supportsCursor = true;
|
||||
caps_.supportsCursorHide = true;
|
||||
caps_.supportsDPIAwareness = true;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = true;
|
||||
caps_.supportsClipboard = true;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = false;
|
||||
caps_.supportsKeyboard = true;
|
||||
caps_.supportsMouse = true;
|
||||
caps_.supportsResize = true;
|
||||
caps_.supportsHighDPI = true;
|
||||
caps_.maxTextureSize = 16384;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 96.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::Linux; }
|
||||
const char* platformName() const override { return "Linux"; }
|
||||
const PlatformCapabilities& capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig& config) const override {
|
||||
if (config.window.width < 320) config.window.width = 320;
|
||||
if (config.window.height < 240) config.window.height = 240;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig& config) const override {
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig& config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("Linux: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return width >= 320 && height >= 240;
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
|
||||
class MacOSPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
MacOSPlatformConfig() {
|
||||
caps_.supportsWindowed = true;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = true;
|
||||
caps_.supportsCursor = true;
|
||||
caps_.supportsCursorHide = true;
|
||||
caps_.supportsDPIAwareness = true;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = true;
|
||||
caps_.supportsClipboard = true;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = false;
|
||||
caps_.supportsKeyboard = true;
|
||||
caps_.supportsMouse = true;
|
||||
caps_.supportsResize = true;
|
||||
caps_.supportsHighDPI = true;
|
||||
caps_.maxTextureSize = 16384;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 144.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::macOS; }
|
||||
const char* platformName() const override { return "macOS"; }
|
||||
const PlatformCapabilities& capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig& config) const override {
|
||||
if (config.window.width < 320) config.window.width = 320;
|
||||
if (config.window.height < 240) config.window.height = 240;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig& config) const override {
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig& config) const override {
|
||||
if (config.window.width <= 0 || config.window.height <= 0) {
|
||||
E2D_LOG_ERROR("macOS: Invalid window dimensions");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return width >= 320 && height >= 240;
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
|
||||
#ifdef __SWITCH__
|
||||
class SwitchPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
SwitchPlatformConfig() {
|
||||
caps_.supportsWindowed = false;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = false;
|
||||
caps_.supportsCursor = false;
|
||||
caps_.supportsCursorHide = false;
|
||||
caps_.supportsDPIAwareness = false;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = false;
|
||||
caps_.supportsClipboard = false;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = true;
|
||||
caps_.supportsKeyboard = false;
|
||||
caps_.supportsMouse = false;
|
||||
caps_.supportsResize = false;
|
||||
caps_.supportsHighDPI = false;
|
||||
caps_.maxTextureSize = 8192;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 96.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::Switch; }
|
||||
const char* platformName() const override { return "Nintendo Switch"; }
|
||||
const PlatformCapabilities& capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig& config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.borderless = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig& config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig& config) const override {
|
||||
if (config.window.mode != WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Switch: Only fullscreen mode is supported");
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return (width == 1920 && height == 1080) ||
|
||||
(width == 1280 && height == 720);
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
#else
|
||||
class SwitchPlatformConfig : public PlatformConfig {
|
||||
public:
|
||||
SwitchPlatformConfig() {
|
||||
caps_.supportsWindowed = false;
|
||||
caps_.supportsFullscreen = true;
|
||||
caps_.supportsBorderless = false;
|
||||
caps_.supportsCursor = false;
|
||||
caps_.supportsCursorHide = false;
|
||||
caps_.supportsDPIAwareness = false;
|
||||
caps_.supportsVSync = true;
|
||||
caps_.supportsMultiMonitor = false;
|
||||
caps_.supportsClipboard = false;
|
||||
caps_.supportsGamepad = true;
|
||||
caps_.supportsTouch = true;
|
||||
caps_.supportsKeyboard = false;
|
||||
caps_.supportsMouse = false;
|
||||
caps_.supportsResize = false;
|
||||
caps_.supportsHighDPI = false;
|
||||
caps_.maxTextureSize = 8192;
|
||||
caps_.preferredScreenWidth = 1920;
|
||||
caps_.preferredScreenHeight = 1080;
|
||||
caps_.defaultDPI = 96.0f;
|
||||
}
|
||||
|
||||
PlatformType platformType() const override { return PlatformType::Switch; }
|
||||
const char* platformName() const override { return "Nintendo Switch"; }
|
||||
const PlatformCapabilities& capabilities() const override { return caps_; }
|
||||
|
||||
void applyConstraints(AppConfig& config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.borderless = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
}
|
||||
|
||||
void applyDefaults(AppConfig& config) const override {
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
}
|
||||
|
||||
bool validateConfig(AppConfig& config) const override {
|
||||
if (config.window.mode != WindowMode::Fullscreen) {
|
||||
E2D_LOG_WARN("Switch: Only fullscreen mode is supported");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int getRecommendedWidth() const override { return 1920; }
|
||||
int getRecommendedHeight() const override { return 1080; }
|
||||
bool isResolutionSupported(int width, int height) const override {
|
||||
return (width == 1920 && height == 1080) ||
|
||||
(width == 1280 && height == 720);
|
||||
}
|
||||
|
||||
private:
|
||||
PlatformCapabilities caps_;
|
||||
};
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 创建平台配置实例
|
||||
* 根据 PlatformType 创建对应的平台配置对象
|
||||
* @param type 平台类型,默认为 Auto(自动检测)
|
||||
* @return 平台配置的智能指针
|
||||
*/
|
||||
UniquePtr<PlatformConfig> createPlatformConfig(PlatformType type) {
|
||||
if (type == PlatformType::Auto) {
|
||||
#ifdef _WIN32
|
||||
type = PlatformType::Windows;
|
||||
#elif defined(__SWITCH__)
|
||||
type = PlatformType::Switch;
|
||||
#elif defined(__linux__)
|
||||
type = PlatformType::Linux;
|
||||
#elif defined(__APPLE__)
|
||||
type = PlatformType::macOS;
|
||||
#else
|
||||
type = PlatformType::Windows;
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case PlatformType::Windows:
|
||||
E2D_LOG_INFO("Creating Windows platform config");
|
||||
return makeUnique<WindowsPlatformConfig>();
|
||||
case PlatformType::Switch:
|
||||
E2D_LOG_INFO("Creating Nintendo Switch platform config");
|
||||
return makeUnique<SwitchPlatformConfig>();
|
||||
case PlatformType::Linux:
|
||||
E2D_LOG_INFO("Creating Linux platform config");
|
||||
return makeUnique<LinuxPlatformConfig>();
|
||||
case PlatformType::macOS:
|
||||
E2D_LOG_INFO("Creating macOS platform config");
|
||||
return makeUnique<MacOSPlatformConfig>();
|
||||
default:
|
||||
E2D_LOG_WARN("Unknown platform type, defaulting to Windows");
|
||||
return makeUnique<WindowsPlatformConfig>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台类型名称
|
||||
* 将 PlatformType 枚举转换为可读的字符串
|
||||
* @param type 平台类型枚举值
|
||||
* @return 平台名称字符串
|
||||
*/
|
||||
const char* getPlatformTypeName(PlatformType type) {
|
||||
switch (type) {
|
||||
case PlatformType::Auto: return "Auto";
|
||||
case PlatformType::Windows: return "Windows";
|
||||
case PlatformType::Switch: return "Switch";
|
||||
case PlatformType::Linux: return "Linux";
|
||||
case PlatformType::macOS: return "macOS";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,625 @@
|
|||
#include <extra2d/config/platform_detector.h>
|
||||
#include <extra2d/config/platform_config.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <psapi.h>
|
||||
#elif defined(__linux__)
|
||||
#include <sys/sysinfo.h>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <cstdlib>
|
||||
#elif defined(__APPLE__)
|
||||
#include <sys/sysctl.h>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#endif
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
/**
|
||||
* @brief 检测当前运行平台
|
||||
* 使用编译时宏判断当前平台类型
|
||||
* @return 当前平台的类型枚举值
|
||||
*/
|
||||
PlatformType PlatformDetector::detect() {
|
||||
#ifdef _WIN32
|
||||
return PlatformType::Windows;
|
||||
#elif defined(__SWITCH__)
|
||||
return PlatformType::Switch;
|
||||
#elif defined(__linux__)
|
||||
return PlatformType::Linux;
|
||||
#elif defined(__APPLE__)
|
||||
return PlatformType::macOS;
|
||||
#else
|
||||
return PlatformType::Windows;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台名称字符串
|
||||
* @return 平台名称(如 "Windows", "Linux", "macOS", "Switch")
|
||||
*/
|
||||
const char* PlatformDetector::platformName() {
|
||||
return platformName(detect());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台类型的名称
|
||||
* @param type 平台类型
|
||||
* @return 平台名称字符串
|
||||
*/
|
||||
const char* PlatformDetector::platformName(PlatformType type) {
|
||||
switch (type) {
|
||||
case PlatformType::Windows: return "Windows";
|
||||
case PlatformType::Switch: return "Nintendo Switch";
|
||||
case PlatformType::Linux: return "Linux";
|
||||
case PlatformType::macOS: return "macOS";
|
||||
case PlatformType::Auto: return "Auto";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为桌面平台
|
||||
* @return 如果是桌面平台返回 true
|
||||
*/
|
||||
bool PlatformDetector::isDesktopPlatform() {
|
||||
PlatformType type = detect();
|
||||
return type == PlatformType::Windows ||
|
||||
type == PlatformType::Linux ||
|
||||
type == PlatformType::macOS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为游戏主机平台
|
||||
* @return 如果是游戏主机平台返回 true
|
||||
*/
|
||||
bool PlatformDetector::isConsolePlatform() {
|
||||
return detect() == PlatformType::Switch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否为移动平台
|
||||
* @return 如果是移动平台返回 true
|
||||
*/
|
||||
bool PlatformDetector::isMobilePlatform() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的能力
|
||||
* @return 平台能力结构
|
||||
*/
|
||||
PlatformCapabilities PlatformDetector::capabilities() {
|
||||
return capabilities(detect());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台的能力
|
||||
* @param type 平台类型
|
||||
* @return 平台能力结构
|
||||
*/
|
||||
PlatformCapabilities PlatformDetector::capabilities(PlatformType type) {
|
||||
switch (type) {
|
||||
case PlatformType::Windows:
|
||||
return getWindowsCapabilities();
|
||||
case PlatformType::Switch:
|
||||
return getSwitchCapabilities();
|
||||
case PlatformType::Linux:
|
||||
return getLinuxCapabilities();
|
||||
case PlatformType::macOS:
|
||||
return getMacOSCapabilities();
|
||||
default:
|
||||
return getWindowsCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的默认配置
|
||||
* @return 平台默认的应用配置
|
||||
*/
|
||||
AppConfig PlatformDetector::platformDefaults() {
|
||||
return platformDefaults(detect());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取指定平台的默认配置
|
||||
* @param type 平台类型
|
||||
* @return 平台默认的应用配置
|
||||
*/
|
||||
AppConfig PlatformDetector::platformDefaults(PlatformType type) {
|
||||
switch (type) {
|
||||
case PlatformType::Windows:
|
||||
return getWindowsDefaults();
|
||||
case PlatformType::Switch:
|
||||
return getSwitchDefaults();
|
||||
case PlatformType::Linux:
|
||||
return getLinuxDefaults();
|
||||
case PlatformType::macOS:
|
||||
return getMacOSDefaults();
|
||||
default:
|
||||
return AppConfig::createDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的推荐分辨率
|
||||
* @param width 输出宽度
|
||||
* @param height 输出高度
|
||||
*/
|
||||
void PlatformDetector::getRecommendedResolution(int& width, int& height) {
|
||||
PlatformCapabilities caps = capabilities();
|
||||
width = caps.preferredScreenWidth;
|
||||
height = caps.preferredScreenHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前平台的默认 DPI
|
||||
* @return 默认 DPI 值
|
||||
*/
|
||||
float PlatformDetector::getDefaultDPI() {
|
||||
return capabilities().defaultDPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查当前平台是否支持指定功能
|
||||
* @param feature 功能名称
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
bool PlatformDetector::supportsFeature(const std::string& feature) {
|
||||
PlatformCapabilities caps = capabilities();
|
||||
|
||||
if (feature == "windowed") return caps.supportsWindowed;
|
||||
if (feature == "fullscreen") return caps.supportsFullscreen;
|
||||
if (feature == "borderless") return caps.supportsBorderless;
|
||||
if (feature == "cursor") return caps.supportsCursor;
|
||||
if (feature == "cursor_hide") return caps.supportsCursorHide;
|
||||
if (feature == "dpi_awareness") return caps.supportsDPIAwareness;
|
||||
if (feature == "vsync") return caps.supportsVSync;
|
||||
if (feature == "multi_monitor") return caps.supportsMultiMonitor;
|
||||
if (feature == "clipboard") return caps.supportsClipboard;
|
||||
if (feature == "gamepad") return caps.supportsGamepad;
|
||||
if (feature == "touch") return caps.supportsTouch;
|
||||
if (feature == "keyboard") return caps.supportsKeyboard;
|
||||
if (feature == "mouse") return caps.supportsMouse;
|
||||
if (feature == "resize") return caps.supportsResize;
|
||||
if (feature == "high_dpi") return caps.supportsHighDPI;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取系统内存大小
|
||||
* @return 系统内存大小(MB),如果无法获取返回 0
|
||||
*/
|
||||
int PlatformDetector::getSystemMemoryMB() {
|
||||
#ifdef _WIN32
|
||||
MEMORYSTATUSEX status;
|
||||
status.dwLength = sizeof(status);
|
||||
if (GlobalMemoryStatusEx(&status)) {
|
||||
return static_cast<int>(status.ullTotalPhys / (1024 * 1024));
|
||||
}
|
||||
return 0;
|
||||
#elif defined(__SWITCH__)
|
||||
return 4096;
|
||||
#elif defined(__linux__)
|
||||
struct sysinfo info;
|
||||
if (sysinfo(&info) == 0) {
|
||||
return static_cast<int>(info.totalram * info.mem_unit / (1024 * 1024));
|
||||
}
|
||||
return 0;
|
||||
#elif defined(__APPLE__)
|
||||
int mib[2] = {CTL_HW, HW_MEMSIZE};
|
||||
int64_t memSize = 0;
|
||||
size_t length = sizeof(memSize);
|
||||
if (sysctl(mib, 2, &memSize, &length, nullptr, 0) == 0) {
|
||||
return static_cast<int>(memSize / (1024 * 1024));
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取 CPU 核心数
|
||||
* @return CPU 核心数
|
||||
*/
|
||||
int PlatformDetector::getCPUCoreCount() {
|
||||
#ifdef _WIN32
|
||||
SYSTEM_INFO sysinfo;
|
||||
GetSystemInfo(&sysinfo);
|
||||
return static_cast<int>(sysinfo.dwNumberOfProcessors);
|
||||
#elif defined(__SWITCH__)
|
||||
return 4;
|
||||
#elif defined(__linux__) || defined(__APPLE__)
|
||||
long cores = sysconf(_SC_NPROCESSORS_ONLN);
|
||||
return static_cast<int>(cores > 0 ? cores : 1);
|
||||
#else
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查是否支持多线程渲染
|
||||
* @return 如果支持返回 true
|
||||
*/
|
||||
bool PlatformDetector::supportsMultithreadedRendering() {
|
||||
#ifdef __SWITCH__
|
||||
return false;
|
||||
#else
|
||||
return getCPUCoreCount() >= 2;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的配置路径
|
||||
* @param appName 应用名称
|
||||
* @return 配置文件目录路径
|
||||
*/
|
||||
std::string PlatformDetector::getConfigPath(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
|
||||
return std::string(path) + "\\" + appName + "\\config";
|
||||
}
|
||||
return ".\\config";
|
||||
#elif defined(__SWITCH__)
|
||||
return "sdmc:/config/" + appName;
|
||||
#elif defined(__linux__)
|
||||
const char* configHome = getenv("XDG_CONFIG_HOME");
|
||||
if (configHome && configHome[0] != '\0') {
|
||||
return std::string(configHome) + "/" + appName;
|
||||
}
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/.config/" + appName;
|
||||
}
|
||||
return "./config";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/Library/Application Support/" + appName + "/config";
|
||||
}
|
||||
return "./config";
|
||||
#else
|
||||
return "./config";
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的存档路径
|
||||
* @param appName 应用名称
|
||||
* @return 存档文件目录路径
|
||||
*/
|
||||
std::string PlatformDetector::getSavePath(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
|
||||
return std::string(path) + "\\" + appName + "\\saves";
|
||||
}
|
||||
return ".\\saves";
|
||||
#elif defined(__SWITCH__)
|
||||
return "sdmc:/saves/" + appName;
|
||||
#elif defined(__linux__)
|
||||
const char* dataHome = getenv("XDG_DATA_HOME");
|
||||
if (dataHome && dataHome[0] != '\0') {
|
||||
return std::string(dataHome) + "/" + appName + "/saves";
|
||||
}
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/.local/share/" + appName + "/saves";
|
||||
}
|
||||
return "./saves";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/Library/Application Support/" + appName + "/saves";
|
||||
}
|
||||
return "./saves";
|
||||
#else
|
||||
return "./saves";
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的缓存路径
|
||||
* @param appName 应用名称
|
||||
* @return 缓存文件目录路径
|
||||
*/
|
||||
std::string PlatformDetector::getCachePath(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, path))) {
|
||||
return std::string(path) + "\\" + appName + "\\cache";
|
||||
}
|
||||
return ".\\cache";
|
||||
#elif defined(__SWITCH__)
|
||||
return "sdmc:/cache/" + appName;
|
||||
#elif defined(__linux__)
|
||||
const char* cacheHome = getenv("XDG_CACHE_HOME");
|
||||
if (cacheHome && cacheHome[0] != '\0') {
|
||||
return std::string(cacheHome) + "/" + appName;
|
||||
}
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/.cache/" + appName;
|
||||
}
|
||||
return "./cache";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/Library/Caches/" + appName;
|
||||
}
|
||||
return "./cache";
|
||||
#else
|
||||
return "./cache";
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台特定的日志路径
|
||||
* @param appName 应用名称
|
||||
* @return 日志文件目录路径
|
||||
*/
|
||||
std::string PlatformDetector::getLogPath(const std::string& appName) {
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_LOCAL_APPDATA, nullptr, 0, path))) {
|
||||
return std::string(path) + "\\" + appName + "\\logs";
|
||||
}
|
||||
return ".\\logs";
|
||||
#elif defined(__SWITCH__)
|
||||
return "sdmc:/logs/" + appName;
|
||||
#elif defined(__linux__)
|
||||
const char* cacheHome = getenv("XDG_CACHE_HOME");
|
||||
if (cacheHome && cacheHome[0] != '\0') {
|
||||
return std::string(cacheHome) + "/" + appName + "/logs";
|
||||
}
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/.cache/" + appName + "/logs";
|
||||
}
|
||||
return "./logs";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pwd = getpwuid(getuid());
|
||||
if (pwd) home = pwd->pw_dir;
|
||||
}
|
||||
if (home) {
|
||||
return std::string(home) + "/Library/Logs/" + appName;
|
||||
}
|
||||
return "./logs";
|
||||
#else
|
||||
return "./logs";
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查平台是否为小端字节序
|
||||
* @return 如果是小端字节序返回 true
|
||||
*/
|
||||
bool PlatformDetector::isLittleEndian() {
|
||||
union {
|
||||
uint32_t i;
|
||||
char c[4];
|
||||
} test = {0x01020304};
|
||||
return test.c[0] == 0x04;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查平台是否为大端字节序
|
||||
* @return 如果是大端字节序返回 true
|
||||
*/
|
||||
bool PlatformDetector::isBigEndian() {
|
||||
return !isLittleEndian();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取平台信息摘要
|
||||
* @return 平台信息字符串
|
||||
*/
|
||||
std::string PlatformDetector::getPlatformSummary() {
|
||||
std::string summary;
|
||||
summary += "Platform: ";
|
||||
summary += platformName();
|
||||
summary += "\n";
|
||||
summary += "Memory: ";
|
||||
summary += std::to_string(getSystemMemoryMB());
|
||||
summary += " MB\n";
|
||||
summary += "CPU Cores: ";
|
||||
summary += std::to_string(getCPUCoreCount());
|
||||
summary += "\n";
|
||||
summary += "Endianness: ";
|
||||
summary += isLittleEndian() ? "Little Endian" : "Big Endian";
|
||||
summary += "\n";
|
||||
summary += "Desktop Platform: ";
|
||||
summary += isDesktopPlatform() ? "Yes" : "No";
|
||||
summary += "\n";
|
||||
summary += "Console Platform: ";
|
||||
summary += isConsolePlatform() ? "Yes" : "No";
|
||||
summary += "\n";
|
||||
summary += "Recommended Resolution: ";
|
||||
int width, height;
|
||||
getRecommendedResolution(width, height);
|
||||
summary += std::to_string(width);
|
||||
summary += "x";
|
||||
summary += std::to_string(height);
|
||||
summary += "\n";
|
||||
summary += "Default DPI: ";
|
||||
summary += std::to_string(static_cast<int>(getDefaultDPI()));
|
||||
return summary;
|
||||
}
|
||||
|
||||
PlatformCapabilities PlatformDetector::getWindowsCapabilities() {
|
||||
PlatformCapabilities caps;
|
||||
caps.supportsWindowed = true;
|
||||
caps.supportsFullscreen = true;
|
||||
caps.supportsBorderless = true;
|
||||
caps.supportsCursor = true;
|
||||
caps.supportsCursorHide = true;
|
||||
caps.supportsDPIAwareness = true;
|
||||
caps.supportsVSync = true;
|
||||
caps.supportsMultiMonitor = true;
|
||||
caps.supportsClipboard = true;
|
||||
caps.supportsGamepad = true;
|
||||
caps.supportsTouch = false;
|
||||
caps.supportsKeyboard = true;
|
||||
caps.supportsMouse = true;
|
||||
caps.supportsResize = true;
|
||||
caps.supportsHighDPI = true;
|
||||
caps.maxTextureSize = 16384;
|
||||
caps.preferredScreenWidth = 1920;
|
||||
caps.preferredScreenHeight = 1080;
|
||||
caps.defaultDPI = 96.0f;
|
||||
return caps;
|
||||
}
|
||||
|
||||
PlatformCapabilities PlatformDetector::getLinuxCapabilities() {
|
||||
PlatformCapabilities caps;
|
||||
caps.supportsWindowed = true;
|
||||
caps.supportsFullscreen = true;
|
||||
caps.supportsBorderless = true;
|
||||
caps.supportsCursor = true;
|
||||
caps.supportsCursorHide = true;
|
||||
caps.supportsDPIAwareness = true;
|
||||
caps.supportsVSync = true;
|
||||
caps.supportsMultiMonitor = true;
|
||||
caps.supportsClipboard = true;
|
||||
caps.supportsGamepad = true;
|
||||
caps.supportsTouch = false;
|
||||
caps.supportsKeyboard = true;
|
||||
caps.supportsMouse = true;
|
||||
caps.supportsResize = true;
|
||||
caps.supportsHighDPI = true;
|
||||
caps.maxTextureSize = 16384;
|
||||
caps.preferredScreenWidth = 1920;
|
||||
caps.preferredScreenHeight = 1080;
|
||||
caps.defaultDPI = 96.0f;
|
||||
return caps;
|
||||
}
|
||||
|
||||
PlatformCapabilities PlatformDetector::getMacOSCapabilities() {
|
||||
PlatformCapabilities caps;
|
||||
caps.supportsWindowed = true;
|
||||
caps.supportsFullscreen = true;
|
||||
caps.supportsBorderless = true;
|
||||
caps.supportsCursor = true;
|
||||
caps.supportsCursorHide = true;
|
||||
caps.supportsDPIAwareness = true;
|
||||
caps.supportsVSync = true;
|
||||
caps.supportsMultiMonitor = true;
|
||||
caps.supportsClipboard = true;
|
||||
caps.supportsGamepad = true;
|
||||
caps.supportsTouch = false;
|
||||
caps.supportsKeyboard = true;
|
||||
caps.supportsMouse = true;
|
||||
caps.supportsResize = true;
|
||||
caps.supportsHighDPI = true;
|
||||
caps.maxTextureSize = 16384;
|
||||
caps.preferredScreenWidth = 1920;
|
||||
caps.preferredScreenHeight = 1080;
|
||||
caps.defaultDPI = 144.0f;
|
||||
return caps;
|
||||
}
|
||||
|
||||
PlatformCapabilities PlatformDetector::getSwitchCapabilities() {
|
||||
PlatformCapabilities caps;
|
||||
caps.supportsWindowed = false;
|
||||
caps.supportsFullscreen = true;
|
||||
caps.supportsBorderless = false;
|
||||
caps.supportsCursor = false;
|
||||
caps.supportsCursorHide = false;
|
||||
caps.supportsDPIAwareness = false;
|
||||
caps.supportsVSync = true;
|
||||
caps.supportsMultiMonitor = false;
|
||||
caps.supportsClipboard = false;
|
||||
caps.supportsGamepad = true;
|
||||
caps.supportsTouch = true;
|
||||
caps.supportsKeyboard = false;
|
||||
caps.supportsMouse = false;
|
||||
caps.supportsResize = false;
|
||||
caps.supportsHighDPI = false;
|
||||
caps.maxTextureSize = 8192;
|
||||
caps.preferredScreenWidth = 1920;
|
||||
caps.preferredScreenHeight = 1080;
|
||||
caps.defaultDPI = 96.0f;
|
||||
return caps;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getWindowsDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getLinuxDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getMacOSDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.highDPI = true;
|
||||
config.window.resizable = true;
|
||||
config.render.vsync = true;
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig PlatformDetector::getSwitchDefaults() {
|
||||
AppConfig config = AppConfig::createDefault();
|
||||
config.window.width = 1920;
|
||||
config.window.height = 1080;
|
||||
config.window.mode = WindowMode::Fullscreen;
|
||||
config.window.resizable = false;
|
||||
config.window.highDPI = false;
|
||||
config.render.vsync = true;
|
||||
config.render.targetFPS = 60;
|
||||
config.input.enableVibration = true;
|
||||
config.input.maxGamepads = 2;
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
#include <extra2d/graphics/render_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/platform/iwindow.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// RenderModuleConfig 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 验证渲染配置有效性
|
||||
*
|
||||
* 检查渲染配置的各项参数是否在有效范围内:
|
||||
* - 目标帧率应在 1-240 之间
|
||||
* - 多重采样数应为 0、2、4、8 或 16
|
||||
* - 精灵批处理大小应大于 0
|
||||
*
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool RenderModuleConfig::validate() const {
|
||||
if (targetFPS < 1 || targetFPS > 240) {
|
||||
E2D_LOG_ERROR("Invalid target FPS: {}, must be between 1 and 240", targetFPS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (multisamples != 0 && multisamples != 2 && multisamples != 4 &&
|
||||
multisamples != 8 && multisamples != 16) {
|
||||
E2D_LOG_ERROR("Invalid multisample count: {}, must be 0, 2, 4, 8 or 16", multisamples);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spriteBatchSize <= 0) {
|
||||
E2D_LOG_ERROR("Invalid sprite batch size: {}, must be greater than 0", spriteBatchSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
*
|
||||
* 根据不同平台的特性调整渲染配置:
|
||||
* - Switch 平台限制 MSAA 最大为 4,禁用 sRGB 帧缓冲
|
||||
* - 其他平台保持用户配置
|
||||
*
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void RenderModuleConfig::applyPlatformConstraints(PlatformType platform) {
|
||||
switch (platform) {
|
||||
case PlatformType::Switch:
|
||||
if (multisamples > 4) {
|
||||
E2D_LOG_WARN("Switch platform limits MSAA to 4x, reducing from {}", multisamples);
|
||||
multisamples = 4;
|
||||
}
|
||||
if (sRGBFramebuffer) {
|
||||
E2D_LOG_WARN("Switch platform does not support sRGB framebuffer, disabling");
|
||||
sRGBFramebuffer = false;
|
||||
}
|
||||
if (targetFPS > 60) {
|
||||
E2D_LOG_WARN("Switch platform target FPS capped at 60");
|
||||
targetFPS = 60;
|
||||
}
|
||||
break;
|
||||
|
||||
case PlatformType::Windows:
|
||||
case PlatformType::Linux:
|
||||
case PlatformType::macOS:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*
|
||||
* 将所有配置项恢复为默认值:
|
||||
* - 后端类型:OpenGL
|
||||
* - 垂直同步:启用
|
||||
* - 目标帧率:60
|
||||
* - 多重采样:禁用
|
||||
* - sRGB 帧缓冲:禁用
|
||||
* - 精灵批处理大小:1000
|
||||
*/
|
||||
void RenderModuleConfig::resetToDefaults() {
|
||||
backend = BackendType::OpenGL;
|
||||
vsync = true;
|
||||
targetFPS = 60;
|
||||
multisamples = 0;
|
||||
sRGBFramebuffer = false;
|
||||
spriteBatchSize = 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
*
|
||||
* 从 JSON 对象中解析渲染配置参数
|
||||
*
|
||||
* @param jsonData JSON 数据指针(nlohmann::json 对象指针)
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool RenderModuleConfig::loadFromJson(const void* jsonData) {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("Null JSON data provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const json& j = *static_cast<const json*>(jsonData);
|
||||
|
||||
if (j.contains("backend")) {
|
||||
std::string backendStr = j["backend"].get<std::string>();
|
||||
if (backendStr == "opengl") {
|
||||
backend = BackendType::OpenGL;
|
||||
} else {
|
||||
E2D_LOG_WARN("Unknown backend type: {}, defaulting to OpenGL", backendStr);
|
||||
backend = BackendType::OpenGL;
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("vsync")) {
|
||||
vsync = j["vsync"].get<bool>();
|
||||
}
|
||||
|
||||
if (j.contains("targetFPS")) {
|
||||
targetFPS = j["targetFPS"].get<int>();
|
||||
}
|
||||
|
||||
if (j.contains("multisamples")) {
|
||||
multisamples = j["multisamples"].get<int>();
|
||||
}
|
||||
|
||||
if (j.contains("sRGBFramebuffer")) {
|
||||
sRGBFramebuffer = j["sRGBFramebuffer"].get<bool>();
|
||||
}
|
||||
|
||||
if (j.contains("spriteBatchSize")) {
|
||||
spriteBatchSize = j["spriteBatchSize"].get<int>();
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Render config loaded from JSON");
|
||||
return true;
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("Failed to parse render config from JSON: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
*
|
||||
* 将当前配置序列化到 JSON 对象
|
||||
*
|
||||
* @param jsonData JSON 数据指针(nlohmann::json 对象指针)
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool RenderModuleConfig::saveToJson(void* jsonData) const {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("Null JSON data provided");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
json& j = *static_cast<json*>(jsonData);
|
||||
|
||||
std::string backendStr = "opengl";
|
||||
switch (backend) {
|
||||
case BackendType::OpenGL:
|
||||
backendStr = "opengl";
|
||||
break;
|
||||
default:
|
||||
backendStr = "opengl";
|
||||
break;
|
||||
}
|
||||
|
||||
j["backend"] = backendStr;
|
||||
j["vsync"] = vsync;
|
||||
j["targetFPS"] = targetFPS;
|
||||
j["multisamples"] = multisamples;
|
||||
j["sRGBFramebuffer"] = sRGBFramebuffer;
|
||||
j["spriteBatchSize"] = spriteBatchSize;
|
||||
|
||||
E2D_LOG_INFO("Render config saved to JSON");
|
||||
return true;
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("Failed to save render config to JSON: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RenderModuleInitializer 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*
|
||||
* 初始化渲染模块初始化器的成员变量
|
||||
*/
|
||||
RenderModuleInitializer::RenderModuleInitializer()
|
||||
: moduleId_(INVALID_MODULE_ID)
|
||||
, windowModuleId_(INVALID_MODULE_ID)
|
||||
, renderer_(nullptr)
|
||||
, initialized_(false) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*
|
||||
* 确保在销毁时关闭模块
|
||||
*/
|
||||
RenderModuleInitializer::~RenderModuleInitializer() {
|
||||
if (initialized_) {
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
*
|
||||
* 返回渲染模块依赖的窗口模块标识符列表
|
||||
*
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> RenderModuleInitializer::getDependencies() const {
|
||||
if (windowModuleId_ != INVALID_MODULE_ID) {
|
||||
return {windowModuleId_};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
*
|
||||
* 根据配置创建渲染后端实例并初始化渲染器
|
||||
*
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool RenderModuleInitializer::initialize(const IModuleConfig* config) {
|
||||
if (initialized_) {
|
||||
E2D_LOG_WARN("Render module already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
E2D_LOG_ERROR("Null config provided for render module initialization");
|
||||
return false;
|
||||
}
|
||||
|
||||
const RenderModuleConfig* renderConfig = dynamic_cast<const RenderModuleConfig*>(config);
|
||||
if (!renderConfig) {
|
||||
E2D_LOG_ERROR("Invalid config type for render module");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!renderConfig->validate()) {
|
||||
E2D_LOG_ERROR("Invalid render module configuration");
|
||||
return false;
|
||||
}
|
||||
|
||||
renderer_ = RenderBackend::create(renderConfig->backend);
|
||||
if (!renderer_) {
|
||||
E2D_LOG_ERROR("Failed to create render backend");
|
||||
return false;
|
||||
}
|
||||
|
||||
IWindow* window = nullptr;
|
||||
if (windowModuleId_ != INVALID_MODULE_ID) {
|
||||
ModuleRegistry& registry = ModuleRegistry::instance();
|
||||
IModuleConfig* windowConfig = registry.getModuleConfig(windowModuleId_);
|
||||
if (windowConfig) {
|
||||
E2D_LOG_INFO("Render module found window module dependency");
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Render module initialized successfully");
|
||||
E2D_LOG_INFO(" Backend: {}", renderConfig->backend == BackendType::OpenGL ? "OpenGL" : "Unknown");
|
||||
E2D_LOG_INFO(" VSync: {}", renderConfig->vsync ? "enabled" : "disabled");
|
||||
E2D_LOG_INFO(" Target FPS: {}", renderConfig->targetFPS);
|
||||
E2D_LOG_INFO(" Multisamples: {}", renderConfig->multisamples);
|
||||
E2D_LOG_INFO(" Sprite Batch Size: {}", renderConfig->spriteBatchSize);
|
||||
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*
|
||||
* 销毁渲染后端实例并清理资源
|
||||
*/
|
||||
void RenderModuleInitializer::shutdown() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer_) {
|
||||
renderer_->shutdown();
|
||||
renderer_.reset();
|
||||
}
|
||||
|
||||
initialized_ = false;
|
||||
E2D_LOG_INFO("Render module shutdown complete");
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -311,39 +311,6 @@ void RenderTarget::copyTo(RenderTarget &target) {
|
|||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将内容传输到另一个渲染目标
|
||||
* @param target 目标渲染目标
|
||||
* @param color 是否复制颜色缓冲
|
||||
* @param depth 是否复制深度缓冲
|
||||
*
|
||||
* 使用glBlitFramebuffer进行选择性复制
|
||||
*/
|
||||
void RenderTarget::blitTo(RenderTarget &target, bool color, bool depth) {
|
||||
if (!isValid() || !target.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用glBlitFramebuffer复制
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo_);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target.getFBO());
|
||||
|
||||
GLbitfield mask = 0;
|
||||
if (color) {
|
||||
mask |= GL_COLOR_BUFFER_BIT;
|
||||
}
|
||||
if (depth && hasDepth_ && target.hasDepth_) {
|
||||
mask |= GL_DEPTH_BUFFER_BIT;
|
||||
}
|
||||
|
||||
if (mask != 0) {
|
||||
glBlitFramebuffer(0, 0, width_, height_, 0, 0, target.getWidth(),
|
||||
target.getHeight(), mask, GL_LINEAR);
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 复制到屏幕
|
||||
* @param screenWidth 屏幕宽度
|
||||
|
|
|
|||
|
|
@ -14,14 +14,16 @@ SDL2Window::~SDL2Window() {
|
|||
destroy();
|
||||
}
|
||||
|
||||
bool SDL2Window::create(const WindowConfig& cfg) {
|
||||
bool SDL2Window::create(const WindowConfigData& cfg) {
|
||||
if (!initSDL()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
|
||||
if (cfg.fullscreen) {
|
||||
flags |= cfg.fullscreenDesktop ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN;
|
||||
if (cfg.isFullscreen()) {
|
||||
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
} else if (cfg.isBorderless()) {
|
||||
flags |= SDL_WINDOW_BORDERLESS;
|
||||
}
|
||||
if (cfg.resizable) {
|
||||
flags |= SDL_WINDOW_RESIZABLE;
|
||||
|
|
@ -37,14 +39,14 @@ bool SDL2Window::create(const WindowConfig& cfg) {
|
|||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
|
||||
if (cfg.msaaSamples > 0) {
|
||||
if (cfg.multisamples > 0) {
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.msaaSamples);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, cfg.multisamples);
|
||||
}
|
||||
|
||||
int x = SDL_WINDOWPOS_CENTERED;
|
||||
int y = SDL_WINDOWPOS_CENTERED;
|
||||
if (!cfg.centerWindow) {
|
||||
if (!cfg.centered) {
|
||||
x = SDL_WINDOWPOS_UNDEFINED;
|
||||
y = SDL_WINDOWPOS_UNDEFINED;
|
||||
}
|
||||
|
|
@ -74,7 +76,7 @@ bool SDL2Window::create(const WindowConfig& cfg) {
|
|||
SDL_GL_SetSwapInterval(cfg.vsync ? 1 : 0);
|
||||
|
||||
SDL_GetWindowSize(sdlWindow_, &width_, &height_);
|
||||
fullscreen_ = cfg.fullscreen;
|
||||
fullscreen_ = cfg.isFullscreen();
|
||||
vsync_ = cfg.vsync;
|
||||
|
||||
initCursors();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ public:
|
|||
SDL2Window();
|
||||
~SDL2Window() override;
|
||||
|
||||
bool create(const WindowConfig& cfg) override;
|
||||
bool create(const WindowConfigData& cfg) override;
|
||||
void destroy() override;
|
||||
|
||||
void poll() override;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,270 @@
|
|||
#include <extra2d/platform/input_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/platform/platform_module.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// InputModuleConfig 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 验证配置有效性
|
||||
* 检查各项配置参数是否在有效范围内
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool InputModuleConfig::validate() const {
|
||||
if (deadzone < 0.0f || deadzone > 1.0f) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: deadzone 必须在 [0.0, 1.0] 范围内,当前值: {}", deadzone);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mouseSensitivity < 0.0f || mouseSensitivity > 10.0f) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: mouseSensitivity 必须在 [0.0, 10.0] 范围内,当前值: {}", mouseSensitivity);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!enableKeyboard && !enableMouse && !enableGamepad && !enableTouch) {
|
||||
E2D_LOG_WARN("InputModuleConfig: 所有输入设备都已禁用");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据平台特性调整配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void InputModuleConfig::applyPlatformConstraints(PlatformType platform) {
|
||||
switch (platform) {
|
||||
case PlatformType::Switch:
|
||||
enableMouse = false;
|
||||
enableTouch = true;
|
||||
enableGamepad = true;
|
||||
E2D_LOG_INFO("InputModuleConfig: Switch 平台 - 禁用鼠标输入");
|
||||
break;
|
||||
|
||||
case PlatformType::Windows:
|
||||
case PlatformType::Linux:
|
||||
case PlatformType::macOS:
|
||||
enableMouse = true;
|
||||
enableKeyboard = true;
|
||||
enableTouch = false;
|
||||
E2D_LOG_INFO("InputModuleConfig: PC 平台 - 禁用触摸输入");
|
||||
break;
|
||||
|
||||
case PlatformType::Auto:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
*/
|
||||
void InputModuleConfig::resetToDefaults() {
|
||||
enableKeyboard = true;
|
||||
enableMouse = true;
|
||||
enableGamepad = true;
|
||||
enableTouch = true;
|
||||
deadzone = 0.15f;
|
||||
mouseSensitivity = 1.0f;
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 已重置为默认配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool InputModuleConfig::loadFromJson(const void* jsonData) {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据指针为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const json& obj = *static_cast<const json*>(jsonData);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据不是对象类型");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj.contains("enableKeyboard") && obj["enableKeyboard"].is_boolean()) {
|
||||
enableKeyboard = obj["enableKeyboard"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableMouse") && obj["enableMouse"].is_boolean()) {
|
||||
enableMouse = obj["enableMouse"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableGamepad") && obj["enableGamepad"].is_boolean()) {
|
||||
enableGamepad = obj["enableGamepad"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("enableTouch") && obj["enableTouch"].is_boolean()) {
|
||||
enableTouch = obj["enableTouch"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("deadzone") && obj["deadzone"].is_number()) {
|
||||
deadzone = obj["deadzone"].get<float>();
|
||||
}
|
||||
|
||||
if (obj.contains("mouseSensitivity") && obj["mouseSensitivity"].is_number()) {
|
||||
mouseSensitivity = obj["mouseSensitivity"].get<float>();
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 从 JSON 加载配置成功");
|
||||
return true;
|
||||
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 解析错误: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool InputModuleConfig::saveToJson(void* jsonData) const {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 数据指针为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
json& obj = *static_cast<json*>(jsonData);
|
||||
|
||||
obj["enableKeyboard"] = enableKeyboard;
|
||||
obj["enableMouse"] = enableMouse;
|
||||
obj["enableGamepad"] = enableGamepad;
|
||||
obj["enableTouch"] = enableTouch;
|
||||
obj["deadzone"] = deadzone;
|
||||
obj["mouseSensitivity"] = mouseSensitivity;
|
||||
|
||||
E2D_LOG_INFO("InputModuleConfig: 保存配置到 JSON 成功");
|
||||
return true;
|
||||
|
||||
} catch (const json::exception& e) {
|
||||
E2D_LOG_ERROR("InputModuleConfig: JSON 序列化错误: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InputModuleInitializer 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
*/
|
||||
InputModuleInitializer::InputModuleInitializer()
|
||||
: moduleId_(INVALID_MODULE_ID)
|
||||
, windowModuleId_(INVALID_MODULE_ID)
|
||||
, input_(nullptr)
|
||||
, initialized_(false) {
|
||||
E2D_LOG_DEBUG("InputModuleInitializer: 构造函数调用");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
*/
|
||||
InputModuleInitializer::~InputModuleInitializer() {
|
||||
if (initialized_) {
|
||||
shutdown();
|
||||
}
|
||||
E2D_LOG_DEBUG("InputModuleInitializer: 析构函数调用");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取模块依赖列表
|
||||
* 返回此模块依赖的窗口模块标识符
|
||||
* @return 依赖模块标识符列表
|
||||
*/
|
||||
std::vector<ModuleId> InputModuleInitializer::getDependencies() const {
|
||||
std::vector<ModuleId> dependencies;
|
||||
|
||||
if (windowModuleId_ != INVALID_MODULE_ID) {
|
||||
dependencies.push_back(windowModuleId_);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* 从窗口模块获取输入接口
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool InputModuleInitializer::initialize(const IModuleConfig* config) {
|
||||
if (initialized_) {
|
||||
E2D_LOG_WARN("InputModuleInitializer: 模块已经初始化");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置指针为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
const InputModuleConfig* inputConfig = dynamic_cast<const InputModuleConfig*>(config);
|
||||
if (!inputConfig) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置类型不正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!inputConfig->validate()) {
|
||||
E2D_LOG_ERROR("InputModuleInitializer: 配置验证失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleInfo info = config->getModuleInfo();
|
||||
moduleId_ = info.id;
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 正在初始化输入模块 '{}' (版本: {})",
|
||||
info.name, info.version);
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入配置 - 键盘: {}, 鼠标: {}, 手柄: {}, 触摸: {}",
|
||||
inputConfig->enableKeyboard ? "启用" : "禁用",
|
||||
inputConfig->enableMouse ? "启用" : "禁用",
|
||||
inputConfig->enableGamepad ? "启用" : "禁用",
|
||||
inputConfig->enableTouch ? "启用" : "禁用");
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 死区: {}, 鼠标灵敏度: {}",
|
||||
inputConfig->deadzone, inputConfig->mouseSensitivity);
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入模块初始化成功");
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
*/
|
||||
void InputModuleInitializer::shutdown() {
|
||||
if (!initialized_) {
|
||||
E2D_LOG_WARN("InputModuleInitializer: 模块未初始化,无需关闭");
|
||||
return;
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 正在关闭输入模块");
|
||||
|
||||
input_ = nullptr;
|
||||
moduleId_ = INVALID_MODULE_ID;
|
||||
initialized_ = false;
|
||||
|
||||
E2D_LOG_INFO("InputModuleInitializer: 输入模块已关闭");
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
#include <extra2d/platform/window_module.h>
|
||||
#include <extra2d/config/module_registry.h>
|
||||
#include <extra2d/platform/platform_module.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// WindowModuleConfig 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 验证窗口配置有效性
|
||||
* 检查窗口尺寸、标题等配置是否合法
|
||||
* @return 如果配置有效返回 true
|
||||
*/
|
||||
bool WindowModuleConfig::validate() const {
|
||||
if (windowConfig.width <= 0) {
|
||||
E2D_LOG_ERROR("Window width must be positive, got: {}", windowConfig.width);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (windowConfig.height <= 0) {
|
||||
E2D_LOG_ERROR("Window height must be positive, got: {}", windowConfig.height);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (windowConfig.title.empty()) {
|
||||
E2D_LOG_WARN("Window title is empty, using default title");
|
||||
}
|
||||
|
||||
if (windowConfig.multisamples < 0) {
|
||||
E2D_LOG_ERROR("MSAA samples cannot be negative, got: {}", windowConfig.multisamples);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (windowConfig.multisamples != 0 &&
|
||||
windowConfig.multisamples != 2 &&
|
||||
windowConfig.multisamples != 4 &&
|
||||
windowConfig.multisamples != 8 &&
|
||||
windowConfig.multisamples != 16) {
|
||||
E2D_LOG_WARN("MSAA samples should be 0, 2, 4, 8, or 16, got: {}", windowConfig.multisamples);
|
||||
}
|
||||
|
||||
if (backend.empty()) {
|
||||
E2D_LOG_ERROR("Backend name cannot be empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用平台约束
|
||||
* 根据目标平台特性调整窗口配置
|
||||
* @param platform 目标平台类型
|
||||
*/
|
||||
void WindowModuleConfig::applyPlatformConstraints(PlatformType platform) {
|
||||
switch (platform) {
|
||||
case PlatformType::Switch:
|
||||
E2D_LOG_INFO("Applying Nintendo Switch platform constraints");
|
||||
windowConfig.mode = WindowMode::Fullscreen;
|
||||
windowConfig.resizable = false;
|
||||
windowConfig.centered = false;
|
||||
windowConfig.width = 1920;
|
||||
windowConfig.height = 1080;
|
||||
backend = "switch";
|
||||
break;
|
||||
|
||||
case PlatformType::Windows:
|
||||
case PlatformType::Linux:
|
||||
case PlatformType::macOS:
|
||||
E2D_LOG_INFO("Applying desktop platform constraints");
|
||||
if (windowConfig.width <= 0) {
|
||||
windowConfig.width = 1280;
|
||||
}
|
||||
if (windowConfig.height <= 0) {
|
||||
windowConfig.height = 720;
|
||||
}
|
||||
break;
|
||||
|
||||
case PlatformType::Auto:
|
||||
default:
|
||||
E2D_LOG_INFO("Auto-detecting platform constraints");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 重置为默认配置
|
||||
* 将所有配置项恢复为默认值
|
||||
*/
|
||||
void WindowModuleConfig::resetToDefaults() {
|
||||
windowConfig = WindowConfigData{};
|
||||
backend = "sdl2";
|
||||
|
||||
E2D_LOG_INFO("Window module config reset to defaults");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从 JSON 数据加载配置
|
||||
* 解析 JSON 对象并填充配置数据
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 加载成功返回 true
|
||||
*/
|
||||
bool WindowModuleConfig::loadFromJson(const void* jsonData) {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("JSON data is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
const json& obj = *static_cast<const json*>(jsonData);
|
||||
|
||||
if (!obj.is_object()) {
|
||||
E2D_LOG_ERROR("JSON data must be an object");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj.contains("title") && obj["title"].is_string()) {
|
||||
windowConfig.title = obj["title"].get<std::string>();
|
||||
}
|
||||
|
||||
if (obj.contains("width") && obj["width"].is_number_integer()) {
|
||||
windowConfig.width = obj["width"].get<int>();
|
||||
}
|
||||
|
||||
if (obj.contains("height") && obj["height"].is_number_integer()) {
|
||||
windowConfig.height = obj["height"].get<int>();
|
||||
}
|
||||
|
||||
if (obj.contains("fullscreen") && obj["fullscreen"].is_boolean()) {
|
||||
windowConfig.mode = obj["fullscreen"].get<bool>() ? WindowMode::Fullscreen : WindowMode::Windowed;
|
||||
}
|
||||
|
||||
if (obj.contains("mode") && obj["mode"].is_string()) {
|
||||
std::string modeStr = obj["mode"].get<std::string>();
|
||||
if (modeStr == "fullscreen") {
|
||||
windowConfig.mode = WindowMode::Fullscreen;
|
||||
} else if (modeStr == "borderless") {
|
||||
windowConfig.mode = WindowMode::Borderless;
|
||||
} else {
|
||||
windowConfig.mode = WindowMode::Windowed;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.contains("resizable") && obj["resizable"].is_boolean()) {
|
||||
windowConfig.resizable = obj["resizable"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("vsync") && obj["vsync"].is_boolean()) {
|
||||
windowConfig.vsync = obj["vsync"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("multisamples") && obj["multisamples"].is_number_integer()) {
|
||||
windowConfig.multisamples = obj["multisamples"].get<int>();
|
||||
}
|
||||
|
||||
if (obj.contains("msaaSamples") && obj["msaaSamples"].is_number_integer()) {
|
||||
windowConfig.multisamples = obj["msaaSamples"].get<int>();
|
||||
}
|
||||
|
||||
if (obj.contains("centered") && obj["centered"].is_boolean()) {
|
||||
windowConfig.centered = obj["centered"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("centerWindow") && obj["centerWindow"].is_boolean()) {
|
||||
windowConfig.centered = obj["centerWindow"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("visible") && obj["visible"].is_boolean()) {
|
||||
windowConfig.visible = obj["visible"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("decorated") && obj["decorated"].is_boolean()) {
|
||||
windowConfig.decorated = obj["decorated"].get<bool>();
|
||||
}
|
||||
|
||||
if (obj.contains("backend") && obj["backend"].is_string()) {
|
||||
backend = obj["backend"].get<std::string>();
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("Window module config loaded from JSON");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存配置到 JSON 数据
|
||||
* 将配置数据序列化为 JSON 对象
|
||||
* @param jsonData JSON 数据指针
|
||||
* @return 保存成功返回 true
|
||||
*/
|
||||
bool WindowModuleConfig::saveToJson(void* jsonData) const {
|
||||
if (!jsonData) {
|
||||
E2D_LOG_ERROR("JSON data pointer is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
json& obj = *static_cast<json*>(jsonData);
|
||||
|
||||
obj["title"] = windowConfig.title;
|
||||
obj["width"] = windowConfig.width;
|
||||
obj["height"] = windowConfig.height;
|
||||
|
||||
std::string modeStr = "windowed";
|
||||
if (windowConfig.mode == WindowMode::Fullscreen) {
|
||||
modeStr = "fullscreen";
|
||||
} else if (windowConfig.mode == WindowMode::Borderless) {
|
||||
modeStr = "borderless";
|
||||
}
|
||||
obj["mode"] = modeStr;
|
||||
|
||||
obj["resizable"] = windowConfig.resizable;
|
||||
obj["vsync"] = windowConfig.vsync;
|
||||
obj["multisamples"] = windowConfig.multisamples;
|
||||
obj["centered"] = windowConfig.centered;
|
||||
obj["visible"] = windowConfig.visible;
|
||||
obj["decorated"] = windowConfig.decorated;
|
||||
obj["backend"] = backend;
|
||||
|
||||
E2D_LOG_INFO("Window module config saved to JSON");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WindowModuleInitializer 实现
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 构造函数
|
||||
* 初始化窗口模块初始化器
|
||||
*/
|
||||
WindowModuleInitializer::WindowModuleInitializer()
|
||||
: moduleId_(INVALID_MODULE_ID)
|
||||
, window_(nullptr)
|
||||
, initialized_(false) {
|
||||
E2D_LOG_DEBUG("WindowModuleInitializer constructed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 析构函数
|
||||
* 确保模块正确关闭
|
||||
*/
|
||||
WindowModuleInitializer::~WindowModuleInitializer() {
|
||||
if (initialized_) {
|
||||
shutdown();
|
||||
}
|
||||
E2D_LOG_DEBUG("WindowModuleInitializer destructed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化模块
|
||||
* 使用 BackendFactory 创建窗口实例
|
||||
* @param config 模块配置指针
|
||||
* @return 初始化成功返回 true
|
||||
*/
|
||||
bool WindowModuleInitializer::initialize(const IModuleConfig* config) {
|
||||
if (initialized_) {
|
||||
E2D_LOG_WARN("Window module already initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
E2D_LOG_ERROR("Window module config is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
const WindowModuleConfig* windowConfig = dynamic_cast<const WindowModuleConfig*>(config);
|
||||
if (!windowConfig) {
|
||||
E2D_LOG_ERROR("Invalid config type for window module");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModuleInfo info = config->getModuleInfo();
|
||||
moduleId_ = info.id;
|
||||
|
||||
const std::string& backend = windowConfig->backend;
|
||||
|
||||
if (!BackendFactory::has(backend)) {
|
||||
E2D_LOG_ERROR("Backend '{}' not available", backend);
|
||||
auto backends = BackendFactory::backends();
|
||||
if (backends.empty()) {
|
||||
E2D_LOG_ERROR("No backends registered!");
|
||||
return false;
|
||||
}
|
||||
std::string backendList;
|
||||
for (const auto& b : backends) {
|
||||
if (!backendList.empty()) backendList += ", ";
|
||||
backendList += b;
|
||||
}
|
||||
E2D_LOG_WARN("Available backends: {}", backendList);
|
||||
return false;
|
||||
}
|
||||
|
||||
window_ = BackendFactory::createWindow(backend);
|
||||
if (!window_) {
|
||||
E2D_LOG_ERROR("Failed to create window for backend: {}", backend);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!window_->create(windowConfig->windowConfig)) {
|
||||
E2D_LOG_ERROR("Failed to create window with given config");
|
||||
window_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
E2D_LOG_INFO("Window module initialized successfully (backend: {}, {}x{})",
|
||||
backend,
|
||||
windowConfig->windowConfig.width,
|
||||
windowConfig->windowConfig.height);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭模块
|
||||
* 销毁窗口实例并重置状态
|
||||
*/
|
||||
void WindowModuleInitializer::shutdown() {
|
||||
if (!initialized_) {
|
||||
E2D_LOG_WARN("Window module not initialized, nothing to shutdown");
|
||||
return;
|
||||
}
|
||||
|
||||
if (window_) {
|
||||
window_->destroy();
|
||||
window_.reset();
|
||||
}
|
||||
|
||||
initialized_ = false;
|
||||
moduleId_ = INVALID_MODULE_ID;
|
||||
|
||||
E2D_LOG_INFO("Window module shutdown complete");
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
Loading…
Reference in New Issue