MetaCore/Source/MetaCoreRuntimeInput/Private/MetaCoreRuntimeInput.cpp
2026-07-16 10:26:51 +08:00

125 lines
8.7 KiB
C++

#include "MetaCoreRuntimeInput/MetaCoreRuntimeInput.h"
#include "MetaCorePlatform/MetaCoreInput.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
namespace MetaCore {
class MetaCoreRuntimeInput::Impl {
public:
MetaCoreInputMapDocument Map{};
std::unordered_map<std::string, MetaCoreInputActionState> Actions{};
std::unordered_map<std::string, float> Axes{};
std::unordered_map<std::string, bool> Contexts{};
std::vector<std::string> ContextStack{};
std::unordered_map<std::string, std::string> Overrides{};
std::unordered_map<std::uint64_t, ActionCallback> Subscribers{};
std::vector<std::string> 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<Impl>()) {}
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<std::string, bool> nextActions;
std::unordered_map<std::string, float> nextAxes;
std::unordered_set<std::string> consumedPaths;
std::vector<const MetaCoreInputContextDefinition*> 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("<Keyboard>")) || (consumed.Mouse && path.starts_with("<Mouse>")) || (consumed.Gamepad && path.starts_with("<Gamepad>"))) 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<std::string>())) 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<std::string>& 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<unsigned char>(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