mirror of https://github.com/OpenMW/openmw.git
Lua actions take 3
parent
ec480db9ac
commit
0e2e386dc9
@ -0,0 +1,65 @@
|
|||||||
|
#include "gmock/gmock.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <components/lua/inputactions.hpp>
|
||||||
|
#include <components/lua/scriptscontainer.hpp>
|
||||||
|
|
||||||
|
#include "../testing_util.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace testing;
|
||||||
|
using namespace TestingOpenMW;
|
||||||
|
|
||||||
|
TEST(LuaInputActionsTest, MultiTree)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
LuaUtil::InputAction::MultiTree tree;
|
||||||
|
auto a = tree.insert();
|
||||||
|
auto b = tree.insert();
|
||||||
|
auto c = tree.insert();
|
||||||
|
auto d = tree.insert();
|
||||||
|
EXPECT_TRUE(tree.multiEdge(c, { a, b }));
|
||||||
|
EXPECT_TRUE(tree.multiEdge(a, { d }));
|
||||||
|
EXPECT_FALSE(tree.multiEdge(d, { c }));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LuaUtil::InputAction::MultiTree tree;
|
||||||
|
auto a = tree.insert();
|
||||||
|
auto b = tree.insert();
|
||||||
|
auto c = tree.insert();
|
||||||
|
EXPECT_TRUE(tree.multiEdge(b, { a }));
|
||||||
|
EXPECT_TRUE(tree.multiEdge(c, { a, b }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaInputActionsTest, Registry)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
LuaUtil::InputAction::Registry registry;
|
||||||
|
LuaUtil::InputAction::Info a({ "a", LuaUtil::InputAction::Type::Boolean, "test", "a_name", "a_description",
|
||||||
|
sol::make_object(lua, false) });
|
||||||
|
registry.insert(a);
|
||||||
|
LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description",
|
||||||
|
sol::make_object(lua, false) });
|
||||||
|
registry.insert(b);
|
||||||
|
LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) });
|
||||||
|
LuaUtil::Callback bindBToA(
|
||||||
|
{ lua.load("return function(_, _, aValue) return aValue end")(), sol::table(lua, sol::create) });
|
||||||
|
EXPECT_TRUE(registry.bind("a", bindA, {}));
|
||||||
|
EXPECT_TRUE(registry.bind("b", bindBToA, { "a" }));
|
||||||
|
registry.update(1.0);
|
||||||
|
sol::object bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean);
|
||||||
|
EXPECT_TRUE(bValue.is<bool>());
|
||||||
|
LuaUtil::Callback badA(
|
||||||
|
{ lua.load("return function() return 'not_a_bool' end")(), sol::table(lua, sol::create) });
|
||||||
|
EXPECT_TRUE(registry.bind("a", badA, {}));
|
||||||
|
testing::internal::CaptureStderr();
|
||||||
|
registry.update(1.0);
|
||||||
|
sol::object aValue = registry.valueOfType("a", LuaUtil::InputAction::Type::Boolean);
|
||||||
|
EXPECT_TRUE(aValue.is<bool>());
|
||||||
|
bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean);
|
||||||
|
EXPECT_TRUE(bValue.is<bool>() && bValue.as<bool>() == aValue.as<bool>());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,288 @@
|
|||||||
|
#include "inputactions.hpp"
|
||||||
|
|
||||||
|
#include <queue>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include <components/debug/debuglog.hpp>
|
||||||
|
#include <components/misc/strings/format.hpp>
|
||||||
|
|
||||||
|
#include "luastate.hpp"
|
||||||
|
|
||||||
|
namespace LuaUtil
|
||||||
|
{
|
||||||
|
namespace InputAction
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string_view typeName(Type actionType)
|
||||||
|
{
|
||||||
|
switch (actionType)
|
||||||
|
{
|
||||||
|
case Type::Boolean:
|
||||||
|
return "Boolean";
|
||||||
|
case Type::Number:
|
||||||
|
return "Number";
|
||||||
|
case Type::Range:
|
||||||
|
return "Range";
|
||||||
|
default:
|
||||||
|
throw std::logic_error("Unknown input action type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiTree::Node MultiTree::insert()
|
||||||
|
{
|
||||||
|
size_t nextId = size();
|
||||||
|
mChildren.push_back({});
|
||||||
|
mParents.push_back({});
|
||||||
|
return nextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MultiTree::validateTree() const
|
||||||
|
{
|
||||||
|
std::vector<bool> complete(size(), false);
|
||||||
|
traverse([&complete](Node node) { complete[node] = true; });
|
||||||
|
return std::find(complete.begin(), complete.end(), false) == complete.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Function>
|
||||||
|
void MultiTree::traverse(Function callback) const
|
||||||
|
{
|
||||||
|
std::queue<Node> nodeQueue;
|
||||||
|
std::vector<bool> complete(size(), false);
|
||||||
|
for (Node root = 0; root < size(); ++root)
|
||||||
|
{
|
||||||
|
if (!complete[root])
|
||||||
|
nodeQueue.push(root);
|
||||||
|
while (!nodeQueue.empty())
|
||||||
|
{
|
||||||
|
Node node = nodeQueue.back();
|
||||||
|
nodeQueue.pop();
|
||||||
|
|
||||||
|
bool isComplete = true;
|
||||||
|
for (Node parent : mParents[node])
|
||||||
|
isComplete = isComplete && complete[parent];
|
||||||
|
complete[node] = isComplete;
|
||||||
|
if (isComplete)
|
||||||
|
{
|
||||||
|
callback(node);
|
||||||
|
for (Node child : mChildren[node])
|
||||||
|
nodeQueue.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MultiTree::multiEdge(Node target, const std::vector<Node>& source)
|
||||||
|
{
|
||||||
|
mParents[target].reserve(mParents[target].size() + source.size());
|
||||||
|
for (Node s : source)
|
||||||
|
{
|
||||||
|
mParents[target].push_back(s);
|
||||||
|
mChildren[s].push_back(target);
|
||||||
|
}
|
||||||
|
bool validTree = validateTree();
|
||||||
|
if (!validTree)
|
||||||
|
{
|
||||||
|
for (Node s : source)
|
||||||
|
{
|
||||||
|
mParents[target].pop_back();
|
||||||
|
mChildren[s].pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool validateActionValue(sol::object value, Type type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Type::Boolean:
|
||||||
|
return value.get_type() == sol::type::boolean;
|
||||||
|
case Type::Number:
|
||||||
|
return value.get_type() == sol::type::number;
|
||||||
|
case Type::Range:
|
||||||
|
if (value.get_type() != sol::type::number)
|
||||||
|
return false;
|
||||||
|
double d = value.as<double>();
|
||||||
|
return 0.0 <= d && d <= 1.0;
|
||||||
|
}
|
||||||
|
throw std::invalid_argument("Unknown action type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Registry::insert(Info info)
|
||||||
|
{
|
||||||
|
if (mIds.find(info.mKey) != mIds.end())
|
||||||
|
throw std::domain_error(Misc::StringUtils::format("Action key \"%s\" is already in use", info.mKey));
|
||||||
|
if (info.mKey.empty())
|
||||||
|
throw std::domain_error("Action key can't be an empty string");
|
||||||
|
if (info.mL10n.empty())
|
||||||
|
throw std::domain_error("Localization context can't be empty");
|
||||||
|
if (!validateActionValue(info.mDefaultValue, info.mType))
|
||||||
|
throw std::logic_error(Misc::StringUtils::format(
|
||||||
|
"Invalid value: \"%s\" for action \"%s\"", LuaUtil::toString(info.mDefaultValue), info.mKey));
|
||||||
|
Id id = mBindingTree.insert();
|
||||||
|
mKeys.push_back(info.mKey);
|
||||||
|
mIds[std::string(info.mKey)] = id;
|
||||||
|
mInfo.push_back(info);
|
||||||
|
mHandlers.push_back({});
|
||||||
|
mBindings.push_back({});
|
||||||
|
mValues.push_back(info.mDefaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> Registry::nextKey(std::string_view key) const
|
||||||
|
{
|
||||||
|
auto it = mIds.find(key);
|
||||||
|
if (it == mIds.end())
|
||||||
|
return std::nullopt;
|
||||||
|
auto nextId = it->second + 1;
|
||||||
|
if (nextId >= mKeys.size())
|
||||||
|
return std::nullopt;
|
||||||
|
return mKeys.at(nextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Info> Registry::operator[](std::string_view actionKey)
|
||||||
|
{
|
||||||
|
auto iter = mIds.find(actionKey);
|
||||||
|
if (iter == mIds.end())
|
||||||
|
return std::nullopt;
|
||||||
|
return mInfo[iter->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
Registry::Id Registry::safeIdByKey(std::string_view key)
|
||||||
|
{
|
||||||
|
auto iter = mIds.find(key);
|
||||||
|
if (iter == mIds.end())
|
||||||
|
throw std::logic_error(Misc::StringUtils::format("Unknown action key: \"%s\"", key));
|
||||||
|
return iter->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Registry::bind(
|
||||||
|
std::string_view key, const LuaUtil::Callback& callback, const std::vector<std::string_view>& dependencies)
|
||||||
|
{
|
||||||
|
Id id = safeIdByKey(key);
|
||||||
|
std::vector<Id> dependencyIds;
|
||||||
|
dependencyIds.reserve(dependencies.size());
|
||||||
|
for (std::string_view s : dependencies)
|
||||||
|
dependencyIds.push_back(safeIdByKey(s));
|
||||||
|
bool validEdge = mBindingTree.multiEdge(id, dependencyIds);
|
||||||
|
if (validEdge)
|
||||||
|
mBindings[id].push_back(Binding{
|
||||||
|
callback,
|
||||||
|
std::move(dependencyIds),
|
||||||
|
});
|
||||||
|
return validEdge;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object Registry::valueOfType(std::string_view key, Type type)
|
||||||
|
{
|
||||||
|
Id id = safeIdByKey(key);
|
||||||
|
Info info = mInfo[id];
|
||||||
|
if (info.mType != type)
|
||||||
|
throw std::logic_error(
|
||||||
|
Misc::StringUtils::format("Attempt to get value of type \"%s\" from action \"%s\" with type \"%s\"",
|
||||||
|
typeName(type), key, typeName(info.mType)));
|
||||||
|
return mValues[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void Registry::update(double dt)
|
||||||
|
{
|
||||||
|
std::vector<sol::object> dependencyValues;
|
||||||
|
mBindingTree.traverse([this, &dependencyValues, dt](Id node) {
|
||||||
|
sol::main_object newValue = mValues[node];
|
||||||
|
std::vector<Binding>& bindings = mBindings[node];
|
||||||
|
bindings.erase(std::remove_if(bindings.begin(), bindings.end(),
|
||||||
|
[&](const Binding& binding) {
|
||||||
|
if (!binding.mCallback.isValid())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
dependencyValues.clear();
|
||||||
|
for (Id parent : binding.mDependencies)
|
||||||
|
dependencyValues.push_back(mValues[parent]);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newValue = sol::main_object(
|
||||||
|
binding.mCallback.call(dt, newValue, sol::as_args(dependencyValues)));
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
if (!validateActionValue(newValue, mInfo[node].mType))
|
||||||
|
Log(Debug::Error) << Misc::StringUtils::format(
|
||||||
|
"Error due to invalid value of action \"%s\"(\"%s\"): ", mKeys[node],
|
||||||
|
LuaUtil::toString(newValue))
|
||||||
|
<< e.what();
|
||||||
|
else
|
||||||
|
Log(Debug::Error) << "Error in callback: " << e.what();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
bindings.end());
|
||||||
|
|
||||||
|
if (!validateActionValue(newValue, mInfo[node].mType))
|
||||||
|
Log(Debug::Error) << Misc::StringUtils::format(
|
||||||
|
"Invalid value of action \"%s\": %s", mKeys[node], LuaUtil::toString(newValue));
|
||||||
|
if (mValues[node] != newValue)
|
||||||
|
{
|
||||||
|
mValues[node] = sol::object(newValue);
|
||||||
|
std::vector<LuaUtil::Callback>& handlers = mHandlers[node];
|
||||||
|
handlers.erase(std::remove_if(handlers.begin(), handlers.end(),
|
||||||
|
[&](const LuaUtil::Callback& handler) {
|
||||||
|
if (!handler.isValid())
|
||||||
|
return true;
|
||||||
|
handler.tryCall(newValue);
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
handlers.end());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace InputTrigger
|
||||||
|
{
|
||||||
|
Registry::Id Registry::safeIdByKey(std::string_view key)
|
||||||
|
{
|
||||||
|
auto it = mIds.find(key);
|
||||||
|
if (it == mIds.end())
|
||||||
|
throw std::domain_error(Misc::StringUtils::format("Unknown trigger key \"%s\"", key));
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Registry::insert(Info info)
|
||||||
|
{
|
||||||
|
if (mIds.find(info.mKey) != mIds.end())
|
||||||
|
throw std::domain_error(Misc::StringUtils::format("Trigger key \"%s\" is already in use", info.mKey));
|
||||||
|
if (info.mKey.empty())
|
||||||
|
throw std::domain_error("Trigger key can't be an empty string");
|
||||||
|
if (info.mL10n.empty())
|
||||||
|
throw std::domain_error("Localization context can't be empty");
|
||||||
|
Id id = mIds.size();
|
||||||
|
mIds[info.mKey] = id;
|
||||||
|
mInfo.push_back(info);
|
||||||
|
mHandlers.push_back({});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Registry::registerHandler(std::string_view key, const LuaUtil::Callback& callback)
|
||||||
|
{
|
||||||
|
Id id = safeIdByKey(key);
|
||||||
|
mHandlers[id].push_back(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Registry::activate(std::string_view key)
|
||||||
|
{
|
||||||
|
Id id = safeIdByKey(key);
|
||||||
|
std::vector<LuaUtil::Callback>& handlers = mHandlers[id];
|
||||||
|
handlers.erase(std::remove_if(handlers.begin(), handlers.end(),
|
||||||
|
[&](const LuaUtil::Callback& handler) {
|
||||||
|
if (!handler.isValid())
|
||||||
|
return true;
|
||||||
|
handler.tryCall();
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
handlers.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
#ifndef COMPONENTS_LUA_INPUTACTIONS
|
||||||
|
#define COMPONENTS_LUA_INPUTACTIONS
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sol/sol.hpp>
|
||||||
|
|
||||||
|
#include <components/lua/asyncpackage.hpp>
|
||||||
|
#include <components/lua/scriptscontainer.hpp>
|
||||||
|
#include <components/misc/algorithm.hpp>
|
||||||
|
|
||||||
|
namespace LuaUtil::InputAction
|
||||||
|
{
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
Boolean,
|
||||||
|
Number,
|
||||||
|
Range,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Info
|
||||||
|
{
|
||||||
|
std::string mKey;
|
||||||
|
Type mType;
|
||||||
|
std::string mL10n;
|
||||||
|
std::string mName;
|
||||||
|
std::string mDescription;
|
||||||
|
sol::main_object mDefaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MultiTree
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Node = size_t;
|
||||||
|
|
||||||
|
Node insert();
|
||||||
|
bool multiEdge(Node target, const std::vector<Node>& source);
|
||||||
|
size_t size() const { return mParents.size(); }
|
||||||
|
|
||||||
|
template <typename Function> // Function = void(Node)
|
||||||
|
void traverse(Function callback) const;
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
mParents.clear();
|
||||||
|
mChildren.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<Node>> mParents;
|
||||||
|
std::vector<std::vector<Node>> mChildren;
|
||||||
|
|
||||||
|
bool validateTree() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Registry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ConstIterator = std::vector<Info>::const_iterator;
|
||||||
|
void insert(Info info);
|
||||||
|
size_t size() const { return mKeys.size(); }
|
||||||
|
std::optional<std::string> firstKey() const { return mKeys.empty() ? std::nullopt : std::optional(mKeys[0]); }
|
||||||
|
std::optional<std::string> nextKey(std::string_view key) const;
|
||||||
|
std::optional<Info> operator[](std::string_view actionKey);
|
||||||
|
bool bind(
|
||||||
|
std::string_view key, const LuaUtil::Callback& callback, const std::vector<std::string_view>& dependencies);
|
||||||
|
sol::object valueOfType(std::string_view key, Type type);
|
||||||
|
void update(double dt);
|
||||||
|
void registerHandler(std::string_view key, const LuaUtil::Callback& handler)
|
||||||
|
{
|
||||||
|
mHandlers[safeIdByKey(key)].push_back(handler);
|
||||||
|
}
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
mKeys.clear();
|
||||||
|
mIds.clear();
|
||||||
|
mInfo.clear();
|
||||||
|
mHandlers.clear();
|
||||||
|
mBindings.clear();
|
||||||
|
mValues.clear();
|
||||||
|
mBindingTree.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Id = MultiTree::Node;
|
||||||
|
Id safeIdByKey(std::string_view key);
|
||||||
|
struct Binding
|
||||||
|
{
|
||||||
|
LuaUtil::Callback mCallback;
|
||||||
|
std::vector<Id> mDependencies;
|
||||||
|
};
|
||||||
|
std::vector<std::string> mKeys;
|
||||||
|
std::unordered_map<std::string, Id, Misc::StringUtils::StringHash, std::equal_to<>> mIds;
|
||||||
|
std::vector<Info> mInfo;
|
||||||
|
std::vector<std::vector<LuaUtil::Callback>> mHandlers;
|
||||||
|
std::vector<std::vector<Binding>> mBindings;
|
||||||
|
std::vector<sol::object> mValues;
|
||||||
|
MultiTree mBindingTree;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace LuaUtil::InputTrigger
|
||||||
|
{
|
||||||
|
struct Info
|
||||||
|
{
|
||||||
|
std::string mKey;
|
||||||
|
std::string mL10n;
|
||||||
|
std::string mName;
|
||||||
|
std::string mDescription;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Registry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::optional<std::string> firstKey() const
|
||||||
|
{
|
||||||
|
return mIds.empty() ? std::nullopt : std::optional(mIds.begin()->first);
|
||||||
|
}
|
||||||
|
std::optional<std::string> nextKey(std::string_view key) const
|
||||||
|
{
|
||||||
|
auto it = mIds.find(key);
|
||||||
|
if (it == mIds.end() || ++it == mIds.end())
|
||||||
|
return std::nullopt;
|
||||||
|
return it->first;
|
||||||
|
}
|
||||||
|
std::optional<Info> operator[](std::string_view key)
|
||||||
|
{
|
||||||
|
Id id = safeIdByKey(key);
|
||||||
|
return mInfo[id];
|
||||||
|
}
|
||||||
|
void insert(Info info);
|
||||||
|
void registerHandler(std::string_view key, const LuaUtil::Callback& callback);
|
||||||
|
void activate(std::string_view key);
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
mInfo.clear();
|
||||||
|
mHandlers.clear();
|
||||||
|
mIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Id = size_t;
|
||||||
|
Id safeIdByKey(std::string_view key);
|
||||||
|
std::unordered_map<std::string, Id, Misc::StringUtils::StringHash, std::equal_to<>> mIds;
|
||||||
|
std::vector<Info> mInfo;
|
||||||
|
std::vector<std::vector<LuaUtil::Callback>> mHandlers;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // COMPONENTS_LUA_INPUTACTIONS
|
@ -0,0 +1,255 @@
|
|||||||
|
local core = require('openmw.core')
|
||||||
|
local input = require('openmw.input')
|
||||||
|
local util = require('openmw.util')
|
||||||
|
local async = require('openmw.async')
|
||||||
|
local storage = require('openmw.storage')
|
||||||
|
local ui = require('openmw.ui')
|
||||||
|
|
||||||
|
local I = require('openmw.interfaces')
|
||||||
|
|
||||||
|
local actionPressHandlers = {}
|
||||||
|
local function onActionPress(id, handler)
|
||||||
|
actionPressHandlers[id] = actionPressHandlers[id] or {}
|
||||||
|
table.insert(actionPressHandlers[id], handler)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindHold(key, actionId)
|
||||||
|
input.bindAction(key, async:callback(function()
|
||||||
|
return input.isActionPressed(actionId)
|
||||||
|
end), {})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindMovement(key, actionId, axisId, direction)
|
||||||
|
input.bindAction(key, async:callback(function()
|
||||||
|
local actionActive = input.isActionPressed(actionId)
|
||||||
|
local axisActive = input.getAxisValue(axisId) * direction > 0
|
||||||
|
return (actionActive or axisActive) and 1 or 0
|
||||||
|
end), {})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindTrigger(key, actionid)
|
||||||
|
onActionPress(actionid, function()
|
||||||
|
input.activateTrigger(key)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
bindTrigger('AlwaysRun', input.ACTION.AlwaysRun)
|
||||||
|
bindTrigger('ToggleSneak', input.ACTION.Sneak)
|
||||||
|
bindTrigger('ToggleWeapon', input.ACTION.ToggleWeapon)
|
||||||
|
bindTrigger('ToggleSpell', input.ACTION.ToggleSpell)
|
||||||
|
bindTrigger('Jump', input.ACTION.Jump)
|
||||||
|
bindTrigger('AutoMove', input.ACTION.AutoMove)
|
||||||
|
bindTrigger('Inventory', input.ACTION.Inventory)
|
||||||
|
bindTrigger('Journal', input.ACTION.Journal)
|
||||||
|
bindTrigger('QuickKeysMenu', input.ACTION.QuickKeysMenu)
|
||||||
|
|
||||||
|
bindHold('TogglePOV', input.ACTION.TogglePOV)
|
||||||
|
bindHold('Sneak', input.ACTION.Sneak)
|
||||||
|
|
||||||
|
bindHold('Run', input.ACTION.Run)
|
||||||
|
input.bindAction('Run', async:callback(function(_, value)
|
||||||
|
local controllerInput = util.vector2(
|
||||||
|
input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward),
|
||||||
|
input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight)
|
||||||
|
):length2()
|
||||||
|
return value or controllerInput > 0.25
|
||||||
|
end), {})
|
||||||
|
|
||||||
|
input.bindAction('Use', async:callback(function()
|
||||||
|
-- The value "0.6" shouldn't exceed the triggering threshold in BindingsManager::actionValueChanged.
|
||||||
|
-- TODO: Move more logic from BindingsManager to Lua and consider to make this threshold configurable.
|
||||||
|
return input.isActionPressed(input.ACTION.Use) or input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) >= 0.6
|
||||||
|
end), {})
|
||||||
|
|
||||||
|
bindMovement('MoveBackward', input.ACTION.MoveBackward, input.CONTROLLER_AXIS.MoveForwardBackward, 1)
|
||||||
|
bindMovement('MoveForward', input.ACTION.MoveForward, input.CONTROLLER_AXIS.MoveForwardBackward, -1)
|
||||||
|
bindMovement('MoveRight', input.ACTION.MoveRight, input.CONTROLLER_AXIS.MoveLeftRight, 1)
|
||||||
|
bindMovement('MoveLeft', input.ACTION.MoveLeft, input.CONTROLLER_AXIS.MoveLeftRight, -1)
|
||||||
|
|
||||||
|
do
|
||||||
|
local zoomInOut = 0
|
||||||
|
onActionPress(input.ACTION.ZoomIn, function()
|
||||||
|
zoomInOut = zoomInOut + 1
|
||||||
|
end)
|
||||||
|
onActionPress(input.ACTION.ZoomOut, function()
|
||||||
|
zoomInOut = zoomInOut - 1
|
||||||
|
end)
|
||||||
|
input.bindAction('Zoom3rdPerson', async:callback(function(dt, _, togglePOV)
|
||||||
|
local Zoom3rdPerson = zoomInOut * 10
|
||||||
|
if togglePOV then
|
||||||
|
local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft)
|
||||||
|
local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight)
|
||||||
|
local controllerZoom = (triggerRight - triggerLeft) * 100 * dt
|
||||||
|
Zoom3rdPerson = Zoom3rdPerson + controllerZoom
|
||||||
|
end
|
||||||
|
zoomInOut = 0
|
||||||
|
return Zoom3rdPerson
|
||||||
|
end), { 'TogglePOV' })
|
||||||
|
end
|
||||||
|
|
||||||
|
local bindingSection = storage.playerSection('OMWInputBindings')
|
||||||
|
|
||||||
|
local keyboardPresses = {}
|
||||||
|
local keybordHolds = {}
|
||||||
|
local boundActions = {}
|
||||||
|
|
||||||
|
local function bindAction(action)
|
||||||
|
if boundActions[action] then return end
|
||||||
|
boundActions[action] = true
|
||||||
|
input.bindAction(action, async:callback(function()
|
||||||
|
if keybordHolds[action] then
|
||||||
|
for _, binding in pairs(keybordHolds[action]) do
|
||||||
|
if input.isKeyPressed(binding.code) then return true end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end), {})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function registerBinding(binding, id)
|
||||||
|
if not input.actions[binding.key] and not input.triggers[binding.key] then
|
||||||
|
print(string.format('Skipping binding for unknown action or trigger: "%s"', binding.key))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if binding.type == 'keyboardPress' then
|
||||||
|
local bindings = keyboardPresses[binding.code] or {}
|
||||||
|
bindings[id] = binding
|
||||||
|
keyboardPresses[binding.code] = bindings
|
||||||
|
elseif binding.type == 'keyboardHold' then
|
||||||
|
local bindings = keybordHolds[binding.key] or {}
|
||||||
|
bindings[id] = binding
|
||||||
|
keybordHolds[binding.key] = bindings
|
||||||
|
bindAction(binding.key)
|
||||||
|
else
|
||||||
|
error('Unknown binding type "' .. binding.type .. '"')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function clearBinding(id)
|
||||||
|
for _, boundTriggers in pairs(keyboardPresses) do
|
||||||
|
boundTriggers[id] = nil
|
||||||
|
end
|
||||||
|
for _, boundKeys in pairs(keybordHolds) do
|
||||||
|
boundKeys[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function updateBinding(id, binding)
|
||||||
|
bindingSection:set(id, binding)
|
||||||
|
clearBinding(id)
|
||||||
|
if binding ~= nil then
|
||||||
|
registerBinding(binding, id)
|
||||||
|
end
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
local interfaceL10n = core.l10n('interface')
|
||||||
|
|
||||||
|
I.Settings.registerRenderer('inputBinding', function(id, set, arg)
|
||||||
|
if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
|
||||||
|
if not arg.type then error('inputBinding: type argument is required') end
|
||||||
|
if not arg.key then error('inputBinding: key argument is required') end
|
||||||
|
local info = input.actions[arg.key] or input.triggers[arg.key]
|
||||||
|
if not info then return {} end
|
||||||
|
|
||||||
|
local l10n = core.l10n(info.key)
|
||||||
|
|
||||||
|
local name = {
|
||||||
|
template = I.MWUI.templates.textNormal,
|
||||||
|
props = {
|
||||||
|
text = l10n(info.name),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local description = {
|
||||||
|
template = I.MWUI.templates.textNormal,
|
||||||
|
props = {
|
||||||
|
text = l10n(info.description),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local binding = bindingSection:get(id)
|
||||||
|
local label = binding and input.getKeyName(binding.code) or interfaceL10n('None')
|
||||||
|
|
||||||
|
local recorder = {
|
||||||
|
template = I.MWUI.templates.textEditLine,
|
||||||
|
props = {
|
||||||
|
readOnly = true,
|
||||||
|
text = label,
|
||||||
|
},
|
||||||
|
events = {
|
||||||
|
focusGain = async:callback(function()
|
||||||
|
if binding == nil then return end
|
||||||
|
updateBinding(id, nil)
|
||||||
|
set(id)
|
||||||
|
end),
|
||||||
|
keyPress = async:callback(function(key)
|
||||||
|
if binding ~= nil or key.code == input.KEY.Escape then return end
|
||||||
|
|
||||||
|
local newBinding = {
|
||||||
|
code = key.code,
|
||||||
|
type = arg.type,
|
||||||
|
key = arg.key,
|
||||||
|
}
|
||||||
|
updateBinding(id, newBinding)
|
||||||
|
set(id)
|
||||||
|
end),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local row = {
|
||||||
|
type = ui.TYPE.Flex,
|
||||||
|
props = {
|
||||||
|
horizontal = true,
|
||||||
|
},
|
||||||
|
content = ui.content {
|
||||||
|
name,
|
||||||
|
{ props = { size = util.vector2(10, 0) } },
|
||||||
|
recorder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
local column = {
|
||||||
|
type = ui.TYPE.Flex,
|
||||||
|
content = ui.content {
|
||||||
|
row,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return column
|
||||||
|
end)
|
||||||
|
|
||||||
|
local initiated = false
|
||||||
|
|
||||||
|
local function init()
|
||||||
|
for id, binding in pairs(bindingSection:asTable()) do
|
||||||
|
registerBinding(binding, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
engineHandlers = {
|
||||||
|
onFrame = function()
|
||||||
|
if not initiated then
|
||||||
|
initiated = true
|
||||||
|
init()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onInputAction = function(id)
|
||||||
|
if not actionPressHandlers[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, handler in ipairs(actionPressHandlers[id]) do
|
||||||
|
handler()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onKeyPress = function(e)
|
||||||
|
local bindings = keyboardPresses[e.code]
|
||||||
|
if bindings then
|
||||||
|
for _, binding in pairs(bindings) do
|
||||||
|
input.activateTrigger(binding.key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
local input = require('openmw.input')
|
||||||
|
local util = require('openmw.util')
|
||||||
|
local async = require('openmw.async')
|
||||||
|
local storage = require('openmw.storage')
|
||||||
|
local types = require('openmw.types')
|
||||||
|
local self = require('openmw.self')
|
||||||
|
|
||||||
|
local NPC = types.NPC
|
||||||
|
|
||||||
|
local moveActions = {
|
||||||
|
'MoveForward',
|
||||||
|
'MoveBackward',
|
||||||
|
'MoveLeft',
|
||||||
|
'MoveRight'
|
||||||
|
}
|
||||||
|
for _, key in ipairs(moveActions) do
|
||||||
|
local smoothKey = 'Smooth' .. key
|
||||||
|
input.registerAction {
|
||||||
|
key = smoothKey,
|
||||||
|
l10n = 'OMWControls',
|
||||||
|
name = smoothKey .. '_name',
|
||||||
|
description = smoothKey .. '_description',
|
||||||
|
type = input.ACTION_TYPE.Range,
|
||||||
|
defaultValue = 0,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local settings = storage.playerSection('SettingsOMWControls')
|
||||||
|
|
||||||
|
local function shouldAlwaysRun(actor)
|
||||||
|
return actor.controls.sneak or not NPC.isOnGround(actor) or NPC.isSwimming(actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function remapToWalkRun(actor, inputMovement)
|
||||||
|
if shouldAlwaysRun(actor) then
|
||||||
|
return true, inputMovement
|
||||||
|
end
|
||||||
|
local normalizedInput, inputSpeed = inputMovement:normalize()
|
||||||
|
local switchPoint = 0.5
|
||||||
|
if inputSpeed < switchPoint then
|
||||||
|
return false, inputMovement * 2
|
||||||
|
else
|
||||||
|
local matchWalkingSpeed = NPC.getWalkSpeed(actor) / NPC.getRunSpeed(actor)
|
||||||
|
local runSpeedRatio = 2 * (inputSpeed - switchPoint) * (1 - matchWalkingSpeed) + matchWalkingSpeed
|
||||||
|
return true, normalizedInput * math.min(1, runSpeedRatio)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function computeSmoothMovement()
|
||||||
|
local controllerInput = util.vector2(
|
||||||
|
input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward),
|
||||||
|
input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight)
|
||||||
|
)
|
||||||
|
return remapToWalkRun(self, controllerInput)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bindSmoothMove(key, axis, direction)
|
||||||
|
local smoothKey = 'Smooth' .. key
|
||||||
|
input.bindAction(smoothKey, async:callback(function()
|
||||||
|
local _, movement = computeSmoothMovement()
|
||||||
|
return math.max(direction * movement[axis], 0)
|
||||||
|
end), {})
|
||||||
|
input.bindAction(key, async:callback(function(_, standardMovement, smoothMovement)
|
||||||
|
if not settings:get('smoothControllerMovement') then
|
||||||
|
return standardMovement
|
||||||
|
end
|
||||||
|
|
||||||
|
if smoothMovement > 0 then
|
||||||
|
return smoothMovement
|
||||||
|
else
|
||||||
|
return standardMovement
|
||||||
|
end
|
||||||
|
end), { smoothKey })
|
||||||
|
end
|
||||||
|
|
||||||
|
bindSmoothMove('MoveForward', 'x', -1)
|
||||||
|
bindSmoothMove('MoveBackward', 'x', 1)
|
||||||
|
bindSmoothMove('MoveRight', 'y', 1)
|
||||||
|
bindSmoothMove('MoveLeft', 'y', -1)
|
||||||
|
|
||||||
|
input.bindAction('Run', async:callback(function(_, run)
|
||||||
|
if not settings:get('smoothControllerMovement') then
|
||||||
|
return run
|
||||||
|
end
|
||||||
|
local smoothRun, movement = computeSmoothMovement()
|
||||||
|
if movement:length2() > 0 then
|
||||||
|
-- ignore always run
|
||||||
|
return smoothRun ~= settings:get('alwaysRun')
|
||||||
|
else
|
||||||
|
return run
|
||||||
|
end
|
||||||
|
end), {})
|
Loading…
Reference in New Issue