mirror of https://github.com/OpenMW/openmw.git
Merge branch 'lua_actions_electric_boogaloo' into 'master'
Lua actions take 3 See merge request OpenMW/openmw!2628macos_ci_fix
commit
e9f3e5c6d1
@ -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