#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h" #include "MetaCorePlatform/MetaCoreInput.h" #include #include #include #include #include #include #include namespace MetaCore { class MetaCoreRuntimeInput::Impl { public: MetaCoreInputMapDocument Map{}; std::unordered_map Actions{}; std::unordered_map Axes{}; std::unordered_map Contexts{}; std::vector ContextStack{}; std::unordered_map Overrides{}; std::unordered_map Subscribers{}; std::vector Warnings{}; std::uint64_t NextSubscription = 1; void Emit(std::string id, MetaCoreInputActionPhase phase) const { MetaCoreInputActionEvent event{std::move(id), phase}; for (const auto& [_, callback] : Subscribers) if (callback) callback(event); } }; MetaCoreRuntimeInput::MetaCoreRuntimeInput() : Impl_(std::make_unique()) {} MetaCoreRuntimeInput::~MetaCoreRuntimeInput() = default; void MetaCoreRuntimeInput::SetInputMap(MetaCoreInputMapDocument document) { Impl_->Map = std::move(document); Impl_->Actions.clear(); Impl_->Axes.clear(); Impl_->Contexts.clear(); for (const auto& action : Impl_->Map.Actions) Impl_->Actions[action.Id] = {}; for (const auto& axis : Impl_->Map.Axes) Impl_->Axes[axis.Id] = 0.0F; for (const auto& context : Impl_->Map.Contexts) Impl_->Contexts[context.Id] = context.EnabledByDefault; } const MetaCoreInputMapDocument& MetaCoreRuntimeInput::GetInputMap() const { return Impl_->Map; } void MetaCoreRuntimeInput::Update(const MetaCoreInput& input, const MetaCoreInputConsumption& consumed) { if (!input.IsWindowFocused()) { CancelAll(); return; } std::unordered_map nextActions; std::unordered_map nextAxes; std::unordered_set consumedPaths; std::vector contexts; for (const auto& context : Impl_->Map.Contexts) if (Impl_->Contexts[context.Id]) contexts.push_back(&context); std::stable_sort(contexts.begin(), contexts.end(), [](auto* a, auto* b) { return a->Priority > b->Priority; }); const auto bindingById = [&](std::string_view id) -> const MetaCoreInputBindingDefinition* { const auto it = std::find_if(Impl_->Map.Bindings.begin(), Impl_->Map.Bindings.end(), [&](const auto& value) { return value.BindingId == id; }); return it == Impl_->Map.Bindings.end() ? nullptr : &*it; }; for (const auto* context : contexts) { for (const auto& bindingId : context->BindingIds) { const auto* binding = bindingById(bindingId); if (!binding) continue; const auto overrideIt = Impl_->Overrides.find(binding->BindingId); const std::string& path = overrideIt == Impl_->Overrides.end() ? binding->ControlPath : overrideIt->second; if (consumedPaths.contains(path)) continue; if ((consumed.Keyboard && path.starts_with("")) || (consumed.Mouse && path.starts_with("")) || (consumed.Gamepad && path.starts_with(""))) continue; bool modifiersHeld = true; for (const auto& modifier : binding->Modifiers) modifiersHeld = modifiersHeld && std::abs(input.GetControlValue(modifier)) > 0.5F; const float value = modifiersHeld ? input.GetControlValue(path) * binding->Scale : 0.0F; if (binding->Target == MetaCoreInputBindingTarget::Action) nextActions[binding->TargetId] = nextActions[binding->TargetId] || std::abs(value) > 0.5F; else nextAxes[binding->TargetId] += value; if (context->ConsumeLowerPriority && std::abs(value) > 0.0001F) consumedPaths.insert(path); } } for (const auto& definition : Impl_->Map.Actions) { auto& state = Impl_->Actions[definition.Id]; const bool wasHeld = state.Held; const bool held = nextActions[definition.Id]; state = {!wasHeld && held, held, wasHeld && !held}; if (state.Pressed) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Started); if (held) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Performed); if (state.Released) Impl_->Emit(definition.Id, MetaCoreInputActionPhase::Canceled); } for (const auto& definition : Impl_->Map.Axes) { float value = std::clamp(nextAxes[definition.Id], -1.0F, 1.0F); if (std::abs(value) < definition.DeadZone) value = 0.0F; Impl_->Axes[definition.Id] = std::clamp(value * definition.Sensitivity, definition.Minimum, definition.Maximum); } } void MetaCoreRuntimeInput::CancelAll() { for (auto& [id, state] : Impl_->Actions) { if (state.Held) Impl_->Emit(id, MetaCoreInputActionPhase::Canceled); state = {false, false, state.Held}; } for (auto& [_, value] : Impl_->Axes) value = 0.0F; } MetaCoreInputActionState MetaCoreRuntimeInput::GetAction(std::string_view id) const { const auto it = Impl_->Actions.find(std::string(id)); return it == Impl_->Actions.end() ? MetaCoreInputActionState{} : it->second; } float MetaCoreRuntimeInput::GetAxis(std::string_view id) const { const auto it = Impl_->Axes.find(std::string(id)); return it == Impl_->Axes.end() ? 0.0F : it->second; } bool MetaCoreRuntimeInput::SetContextEnabled(std::string_view id, bool enabled) { auto it = Impl_->Contexts.find(std::string(id)); if (it == Impl_->Contexts.end()) return false; it->second = enabled; return true; } bool MetaCoreRuntimeInput::PushContext(std::string_view id) { if (!SetContextEnabled(id, true)) return false; Impl_->ContextStack.emplace_back(id); return true; } bool MetaCoreRuntimeInput::PopContext(std::string_view id) { auto it = std::find(Impl_->ContextStack.rbegin(), Impl_->ContextStack.rend(), id); if (it == Impl_->ContextStack.rend()) return false; Impl_->ContextStack.erase(std::next(it).base()); return SetContextEnabled(id, false); } std::uint64_t MetaCoreRuntimeInput::Subscribe(ActionCallback callback) { const auto id = Impl_->NextSubscription++; Impl_->Subscribers.emplace(id, std::move(callback)); return id; } void MetaCoreRuntimeInput::Unsubscribe(std::uint64_t id) { Impl_->Subscribers.erase(id); } bool MetaCoreRuntimeInput::Rebind(std::string_view id, std::string path) { if (path.empty() || path.front() != '<') return false; const auto it = std::find_if(Impl_->Map.Bindings.begin(), Impl_->Map.Bindings.end(), [&](const auto& b){ return b.BindingId == id; }); if (it == Impl_->Map.Bindings.end()) return false; Impl_->Overrides[std::string(id)] = std::move(path); return true; } bool MetaCoreRuntimeInput::ResetBinding(std::string_view id) { return Impl_->Overrides.erase(std::string(id)) > 0; } void MetaCoreRuntimeInput::ResetAllBindings() { Impl_->Overrides.clear(); } bool MetaCoreRuntimeInput::LoadOverrides(const std::filesystem::path& path) { Impl_->Warnings.clear(); std::ifstream input(path); if (!input) return true; try { nlohmann::json json; input >> json; for (auto it = json.begin(); it != json.end(); ++it) if (!Rebind(it.key(), it.value().get())) Impl_->Warnings.push_back("忽略无效输入覆盖: " + it.key()); } catch (const std::exception& error) { Impl_->Warnings.push_back(std::string("输入覆盖损坏,已使用默认绑定: ") + error.what()); Impl_->Overrides.clear(); return false; } return true; } bool MetaCoreRuntimeInput::SaveOverrides(const std::filesystem::path& path) const { std::error_code error; std::filesystem::create_directories(path.parent_path(), error); const auto temporary = path.string() + ".tmp"; nlohmann::json json = Impl_->Overrides; { std::ofstream output(temporary, std::ios::trunc); if (!output) return false; output << json.dump(2); if (!output.good()) return false; } std::filesystem::rename(temporary, path, error); if (!error) return true; std::filesystem::remove(path, error); error.clear(); std::filesystem::rename(temporary, path, error); return !error; } const std::vector& MetaCoreRuntimeInput::GetWarnings() const { return Impl_->Warnings; } std::filesystem::path MetaCoreGetInputOverridePath(std::string_view projectName) { std::string safe(projectName); for (char& value : safe) if (!std::isalnum(static_cast(value)) && value != '-' && value != '_') value = '_'; #if defined(_WIN32) const char* root = std::getenv("LOCALAPPDATA"); return std::filesystem::path(root ? root : ".") / "MetaCore" / safe / "InputBindings.json"; #else const char* xdg = std::getenv("XDG_DATA_HOME"); const char* home = std::getenv("HOME"); const std::filesystem::path root = xdg ? xdg : (home ? std::filesystem::path(home) / ".local" / "share" : std::filesystem::path(".")); return root / "MetaCore" / safe / "InputBindings.json"; #endif } } // namespace MetaCore