Merge branch 'lua_actions_electric_boogaloo' into 'master'

Lua actions take 3

See merge request OpenMW/openmw!2628
macos_ci_fix
psi29a 4 months ago
commit e9f3e5c6d1

@ -19,6 +19,7 @@ apps/openmw_test_suite/lua/test_serialization.cpp
apps/openmw_test_suite/lua/test_storage.cpp
apps/openmw_test_suite/lua/test_ui_content.cpp
apps/openmw_test_suite/lua/test_utilpackage.cpp
apps/openmw_test_suite/lua/test_inputactions.cpp
apps/openmw_test_suite/misc/test_endianness.cpp
apps/openmw_test_suite/misc/test_resourcehelpers.cpp
apps/openmw_test_suite/misc/test_stringops.cpp

@ -29,6 +29,14 @@ namespace ESM
struct LuaScripts;
}
namespace LuaUtil
{
namespace InputAction
{
class Registry;
}
}
namespace MWBase
{
// \brief LuaManager is the central interface through which the engine invokes lua scripts.

@ -4,12 +4,14 @@
#include <SDL_gamecontroller.h>
#include <SDL_mouse.h>
#include <components/lua/inputactions.hpp>
#include <components/lua/luastate.hpp>
#include <components/sdlutil/events.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwinput/actions.hpp"
#include "luamanagerimp.hpp"
namespace sol
{
@ -17,6 +19,16 @@ namespace sol
struct is_automagical<SDL_Keysym> : std::false_type
{
};
template <>
struct is_automagical<LuaUtil::InputAction::Info> : std::false_type
{
};
template <>
struct is_automagical<LuaUtil::InputAction::Registry> : std::false_type
{
};
}
namespace MWLua
@ -46,9 +58,121 @@ namespace MWLua
touchpadEvent["pressure"]
= sol::readonly_property([](const SDLUtil::TouchEvent& e) -> float { return e.mPressure; });
auto inputActions = context.mLua->sol().new_usertype<LuaUtil::InputAction::Registry>("InputActions");
inputActions[sol::meta_function::index]
= [](LuaUtil::InputAction::Registry& registry, std::string_view key) { return registry[key]; };
{
auto pairs = [](LuaUtil::InputAction::Registry& registry) {
auto next = [](LuaUtil::InputAction::Registry& registry, std::string_view key)
-> sol::optional<std::tuple<std::string_view, LuaUtil::InputAction::Info>> {
std::optional<std::string_view> nextKey(registry.nextKey(key));
if (!nextKey.has_value())
return sol::nullopt;
else
return std::make_tuple(*nextKey, registry[*nextKey].value());
};
return std::make_tuple(next, registry, registry.firstKey());
};
inputActions[sol::meta_function::pairs] = pairs;
}
auto actionInfo = context.mLua->sol().new_usertype<LuaUtil::InputAction::Info>("ActionInfo", "key",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mKey; }), "name",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mName; }), "description",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mDescription; }), "type",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mType; }), "l10n",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mL10n; }), "defaultValue",
sol::property([](const LuaUtil::InputAction::Info& info) { return info.mDefaultValue; }));
auto inputTriggers = context.mLua->sol().new_usertype<LuaUtil::InputTrigger::Registry>("InputTriggers");
inputTriggers[sol::meta_function::index]
= [](LuaUtil::InputTrigger::Registry& registry, std::string_view key) { return registry[key]; };
{
auto pairs = [](LuaUtil::InputTrigger::Registry& registry) {
auto next = [](LuaUtil::InputTrigger::Registry& registry, std::string_view key)
-> sol::optional<std::tuple<std::string_view, LuaUtil::InputTrigger::Info>> {
std::optional<std::string_view> nextKey(registry.nextKey(key));
if (!nextKey.has_value())
return sol::nullopt;
else
return std::make_tuple(*nextKey, registry[*nextKey].value());
};
return std::make_tuple(next, registry, registry.firstKey());
};
inputTriggers[sol::meta_function::pairs] = pairs;
}
auto triggerInfo = context.mLua->sol().new_usertype<LuaUtil::InputTrigger::Info>("TriggerInfo", "key",
sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mKey; }), "name",
sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mName; }), "description",
sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mDescription; }), "l10n",
sol::property([](const LuaUtil::InputTrigger::Info& info) { return info.mL10n; }));
MWBase::InputManager* input = MWBase::Environment::get().getInputManager();
sol::table api(context.mLua->sol(), sol::create);
api["ACTION_TYPE"]
= LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, LuaUtil::InputAction::Type>({
{ "Boolean", LuaUtil::InputAction::Type::Boolean },
{ "Number", LuaUtil::InputAction::Type::Number },
{ "Range", LuaUtil::InputAction::Type::Range },
}));
api["actions"] = std::ref(context.mLuaManager->inputActions());
api["registerAction"] = [manager = context.mLuaManager](sol::table options) {
LuaUtil::InputAction::Info parsedOptions;
parsedOptions.mKey = options["key"].get<std::string_view>();
parsedOptions.mType = options["type"].get<LuaUtil::InputAction::Type>();
parsedOptions.mL10n = options["l10n"].get<std::string_view>();
parsedOptions.mName = options["name"].get<std::string_view>();
parsedOptions.mDescription = options["description"].get<std::string_view>();
parsedOptions.mDefaultValue = options["defaultValue"].get<sol::main_object>();
manager->inputActions().insert(parsedOptions);
};
api["bindAction"] = [manager = context.mLuaManager](
std::string_view key, const sol::table& callback, sol::table dependencies) {
std::vector<std::string_view> parsedDependencies;
parsedDependencies.reserve(dependencies.size());
for (size_t i = 1; i <= dependencies.size(); ++i)
{
sol::object dependency = dependencies[i];
if (!dependency.is<std::string_view>())
throw std::domain_error("The dependencies argument must be a list of Action keys");
parsedDependencies.push_back(dependency.as<std::string_view>());
}
if (!manager->inputActions().bind(key, LuaUtil::Callback::fromLua(callback), parsedDependencies))
throw std::domain_error("Cyclic action binding");
};
api["registerActionHandler"]
= [manager = context.mLuaManager](std::string_view key, const sol::table& callback) {
manager->inputActions().registerHandler(key, LuaUtil::Callback::fromLua(callback));
};
api["getBooleanActionValue"] = [manager = context.mLuaManager](std::string_view key) {
return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Boolean);
};
api["getNumberActionValue"] = [manager = context.mLuaManager](std::string_view key) {
return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Number);
};
api["getRangeActionValue"] = [manager = context.mLuaManager](std::string_view key) {
return manager->inputActions().valueOfType(key, LuaUtil::InputAction::Type::Range);
};
api["triggers"] = std::ref(context.mLuaManager->inputTriggers());
api["registerTrigger"] = [manager = context.mLuaManager](sol::table options) {
LuaUtil::InputTrigger::Info parsedOptions;
parsedOptions.mKey = options["key"].get<std::string_view>();
parsedOptions.mL10n = options["l10n"].get<std::string_view>();
parsedOptions.mName = options["name"].get<std::string_view>();
parsedOptions.mDescription = options["description"].get<std::string_view>();
manager->inputTriggers().insert(parsedOptions);
};
api["registerTriggerHandler"]
= [manager = context.mLuaManager](std::string_view key, const sol::table& callback) {
manager->inputTriggers().registerHandler(key, LuaUtil::Callback::fromLua(callback));
};
api["activateTrigger"]
= [manager = context.mLuaManager](std::string_view key) { manager->inputTriggers().activate(key); };
api["isIdle"] = [input]() { return input->isIdle(); };
api["isActionPressed"] = [input](int action) { return input->actionIsActive(action); };
api["isKeyPressed"] = [](SDL_Scancode code) -> bool {

@ -225,10 +225,12 @@ namespace MWLua
playerScripts->processInputEvent(event);
}
mInputEvents.clear();
double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()
? 0.0
: MWBase::Environment::get().getFrameDuration();
mInputActions.update(frameDuration);
if (playerScripts)
playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()
? 0.0
: MWBase::Environment::get().getFrameDuration());
playerScripts->onFrame(frameDuration);
mProcessingInputEvents = false;
for (const std::string& message : mUIMessages)
@ -291,6 +293,8 @@ namespace MWLua
}
mGlobalStorage.clearTemporaryAndRemoveCallbacks();
mPlayerStorage.clearTemporaryAndRemoveCallbacks();
mInputActions.clear();
mInputTriggers.clear();
for (int i = 0; i < 5; ++i)
lua_gc(mLua.sol(), LUA_GCCOLLECT, 0);
}
@ -520,6 +524,8 @@ namespace MWLua
MWBase::Environment::get().getL10nManager()->dropCache();
mUiResourceManager.clear();
mLua.dropScriptCache();
mInputActions.clear();
mInputTriggers.clear();
initConfiguration();
{ // Reload global scripts

@ -6,6 +6,7 @@
#include <osg/Stats>
#include <set>
#include <components/lua/inputactions.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/storage.hpp>
#include <components/lua_ui/resources.hpp>
@ -144,6 +145,9 @@ namespace MWLua
void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
std::string formatResourceUsageStats() const override;
LuaUtil::InputAction::Registry& inputActions() { return mInputActions; }
LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; }
private:
void initConfiguration();
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr,
@ -206,6 +210,9 @@ namespace MWLua
LuaUtil::LuaStorage mGlobalStorage{ mLua.sol() };
LuaUtil::LuaStorage mPlayerStorage{ mLua.sol() };
LuaUtil::InputAction::Registry mInputActions;
LuaUtil::InputTrigger::Registry mInputTriggers;
};
}

@ -28,6 +28,7 @@ file(GLOB UNITTEST_SRC_FILES
lua/test_l10n.cpp
lua/test_storage.cpp
lua/test_async.cpp
lua/test_inputactions.cpp
lua/test_ui_content.cpp

@ -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>());
}
}

@ -44,7 +44,7 @@ list (APPEND COMPONENT_FILES "${OpenMW_BINARY_DIR}/${VERSION_CPP_FILE}")
add_component_dir (lua
luastate scriptscontainer asyncpackage utilpackage serialization configuration l10n storage utf8
shapes/box
shapes/box inputactions
)
add_component_dir (l10n

@ -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

@ -99,6 +99,13 @@ namespace Misc::StringUtils
bool operator()(std::string_view left, std::string_view right) const { return ciLess(left, right); }
};
struct StringHash
{
using is_transparent = void;
[[nodiscard]] size_t operator()(std::string_view sv) const { return std::hash<std::string_view>{}(sv); }
[[nodiscard]] size_t operator()(const std::string& s) const { return std::hash<std::string>{}(s); }
};
/** @brief Replaces all occurrences of a string in another string.
*
* @param str The string to operate on.

@ -109,7 +109,8 @@ Engine handler is a function defined by a script, that can be called by the engi
| Usage example:
| ``if id == input.CONTROLLER_BUTTON.LeftStick then ...``
* - onInputAction(id)
- | `Game control <openmw_input.html##(ACTION)>`_ is pressed.
- | (DEPRECATED, use `registerActionHandler <openmw_input.html##(registerActionHandler)>`_)
| `Game control <openmw_input.html##(ACTION)>`_ is pressed.
| Usage example:
| ``if id == input.ACTION.ToggleWeapon then ...``
* - onTouchPress(touchEvent)

@ -126,3 +126,27 @@ Table with the following optional fields:
* - disabled
- bool (false)
- Disables changing the setting from the UI
inputBinding
-----
Allows the user to bind inputs to an action or trigger
**Argument**
Table with the following fields:
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type (default)
- description
* - type
- 'keyboardPress', 'keyboardHold'
- The type of input that's allowed to be bound
* - key
- #string
- Key of the action or trigger to which the input is bound

@ -92,6 +92,8 @@ set(BUILTIN_DATA_FILES
scripts/omw/ui.lua
scripts/omw/usehandlers.lua
scripts/omw/worldeventhandlers.lua
scripts/omw/input/actionbindings.lua
scripts/omw/input/smoothmovement.lua
shaders/adjustments.omwfx
shaders/bloomlinear.omwfx

@ -13,6 +13,8 @@ GLOBAL: scripts/omw/worldeventhandlers.lua
PLAYER: scripts/omw/mechanics/playercontroller.lua
PLAYER: scripts/omw/playercontrols.lua
PLAYER: scripts/omw/camera/camera.lua
PLAYER: scripts/omw/input/actionbindings.lua
PLAYER: scripts/omw/input/smoothmovement.lua
NPC,CREATURE: scripts/omw/ai.lua
# User interface

@ -10,7 +10,76 @@ alwaysRunDescription: |
toggleSneak: "Toggle sneak"
toggleSneakDescription: |
This setting causes the behavior of the sneak key (bound to Ctrl by default)
to toggle sneaking on and off rather than requiring the key to be held down while sneaking.
This setting causes the sneak key (bound to Ctrl by default) to toggle sneaking on and off
rather than requiring the key to be held down while sneaking.
Players that spend significant time sneaking may find the character easier to control with this option enabled.
smoothControllerMovement: "Smooth controller movement"
smoothControllerMovementDescription: |
Enables smooth movement with controller stick, with no abrupt switch from walking to running.
TogglePOV_name: "Toggle POV"
TogglePOV_description: "Toggle between first and third person view. Hold to enter preview mode."
Zoom3rdPerson_name: "Zoom In/Out"
Zoom3rdPerson_description: "Moves the camera closer / further away when in third person view."
MoveForward_name: "Move Forward"
MoveForward_description: "Can cancel out with Move Backward"
MoveBackward_name: "Move Backward"
MoveBackward_description: "Can cancel out with Move Forward"
MoveLeft_name: "Move Left"
MoveLeft_description: "Can cancel out with Move Right"
MoveRight_name: "Move Right"
MoveRight_description: "Can cancel out with Move Left"
Use_name: "Use"
Use_description: "Attack with a weapon or cast a spell depending on current stance"
Run_name: "Run"
Run_description: "Hold to run/walk, depending on the Always Run setting"
AlwaysRun_name: "Always Run"
AlwaysRun_description: "Toggle the Always Run setting"
Jump_name: "Jump"
Jump_description: "Jump whenever you are on the ground"
AutoMove_name: "Auto Run"
AutoMove_description: "Toggle continous forward movement"
Sneak_name: "Sneak"
Sneak_description: "Hold to sneak, if the Toggle Sneak setting is off"
ToggleSneak_name: "Toggle Sneak"
ToggleSneak_description: "Toggle sneak, if the Toggle Sneak setting is on"
ToggleWeapon_name: "Ready Weapon"
ToggleWeapon_description: "Enter or leave the weapon stance"
ToggleSpell_name: "Ready Magic"
ToggleSpell_description: "Enter or leave the magic stance"
Inventory_name: "Inventory"
Inventory_description: "Open the inventory"
Journal_name: "Journal"
Journal_description: "Open the journal"
QuickKeysMenu_name: "QuickKeysMenu"
QuickKeysMenu_description: "Open the quick keys menu"
SmoothMoveForward_name: "Smooth Move Forward"
SmoothMoveForward_description: "Forward movement adjusted for smooth Walk-Run transitions"
SmoothMoveBackward_name: "Smooth Move Backward"
SmoothMoveBackward_description: "Backward movement adjusted for smooth Walk-Run transitions"
SmoothMoveLeft_name: "Smooth Move Left"
SmoothMoveLeft_description: "Left movement adjusted for smooth Walk-Run transitions"
SkmoothMoveRight_name: "SmoothMove Right"
SkmoothMoveRight_description: "Right movement adjusted for smooth Walk-Run transitions"

@ -14,3 +14,6 @@ toggleSneakDescription: |
Une simple pression de la touche associée (Ctrl par défaut) active le mode discrétion, une seconde pression désactive le mode discrétion.\n\n
Il n'est plus nécessaire de maintenir une touche appuyée pour que le mode discrétion soit actif.\n\n
Certains joueurs ayant une utilisation intensive du mode discrétion considèrent qu'il est plus aisé de contrôler leur personnage ainsi.
# smoothControllerMovement
# smoothControllerMovementDescription

@ -14,3 +14,5 @@ toggleSneakDescription: |
чтобы красться, её достаточно нажать единожды для переключения положения, а не зажимать.
Игрокам, которые много времени крадутся, может быть проще управлять персонажем, когда опция включена.
# smoothControllerMovement
# smoothControllerMovementDescription

@ -13,3 +13,6 @@ toggleSneakDescription: |
Denna inställningen gör att smygknappen (förinställt till ctrl) slår smygning på eller av vid ett knapptryck
istället för att att kräva att knappen hålls nedtryckt för att smyga.
Spelare som spenderar mycket tid med att smyga lär ha lättare att kontrollera rollfiguren med denna funktion aktiverad.
# smoothControllerMovement
# smoothControllerMovementDescription

@ -10,6 +10,24 @@ local I = require('openmw.interfaces')
local Actor = require('openmw.types').Actor
local Player = require('openmw.types').Player
input.registerAction {
key = 'TogglePOV',
l10n = 'OMWControls',
name = 'TogglePOV_name',
description = 'TogglePOV_description',
type = input.ACTION_TYPE.Boolean,
defaultValue = false,
}
input.registerAction {
key = 'Zoom3rdPerson',
l10n = 'OMWControls',
name = 'Zoom3rdPerson_name',
description = 'Zoom3rdPerson_description',
type = input.ACTION_TYPE.Number,
defaultValue = 0,
}
local settings = require('scripts.omw.camera.settings').thirdPerson
local head_bobbing = require('scripts.omw.camera.head_bobbing')
local third_person = require('scripts.omw.camera.third_person')
@ -63,7 +81,7 @@ local previewTimer = 0
local function updatePOV(dt)
local switchLimit = 0.25
if input.isActionPressed(input.ACTION.TogglePOV) and Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) then
if input.getBooleanActionValue('TogglePOV') and Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) then
previewTimer = previewTimer + dt
if primaryMode == MODE.ThirdPerson or previewTimer >= switchLimit then
third_person.standingPreview = false
@ -117,18 +135,19 @@ local maxDistance = 800
local function zoom(delta)
if not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or
camera.getMode() == MODE.Static or next(noZoom) then
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or
camera.getMode() == MODE.Static or next(noZoom) then
return
end
if camera.getMode() ~= MODE.FirstPerson then
local obstacleDelta = third_person.preferredDistance - camera.getThirdPersonDistance()
if delta > 0 and third_person.baseDistance == minDistance and
(camera.getMode() ~= MODE.Preview or third_person.standingPreview) and not next(noModeControl) then
(camera.getMode() ~= MODE.Preview or third_person.standingPreview) and not next(noModeControl) then
primaryMode = MODE.FirstPerson
camera.setMode(primaryMode)
elseif delta > 0 or obstacleDelta < -delta then
third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance, maxDistance)
third_person.baseDistance = util.clamp(third_person.baseDistance - delta - obstacleDelta, minDistance,
maxDistance)
end
elseif delta < 0 and not next(noModeControl) then
primaryMode = MODE.ThirdPerson
@ -137,21 +156,10 @@ local function zoom(delta)
end
end
local function applyControllerZoom(dt)
if input.isActionPressed(input.ACTION.TogglePOV) then
local triggerLeft = input.getAxisValue(input.CONTROLLER_AXIS.TriggerLeft)
local triggerRight = input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight)
local controllerZoom = (triggerRight - triggerLeft) * 100 * dt
if controllerZoom ~= 0 then
zoom(controllerZoom)
end
end
end
local function updateStandingPreview()
local mode = camera.getMode()
if not previewIfStandStill or next(noStandingPreview)
or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then
or mode == MODE.FirstPerson or mode == MODE.Static or mode == MODE.Vanity then
third_person.standingPreview = false
return
end
@ -184,7 +192,7 @@ local function updateIdleTimer(dt)
if not input.isIdle() then
idleTimer = 0
elseif self.controls.movement ~= 0 or self.controls.sideMovement ~= 0 or self.controls.jump or self.controls.use ~= 0 then
idleTimer = 0 -- also reset the timer in case of a scripted movement
idleTimer = 0 -- also reset the timer in case of a scripted movement
else
idleTimer = idleTimer + dt
end
@ -205,7 +213,14 @@ local function onFrame(dt)
updateStandingPreview()
updateCrosshair()
end
applyControllerZoom(dt)
do
local Zoom3rdPerson = input.getNumberActionValue('Zoom3rdPerson')
if Zoom3rdPerson ~= 0 then
zoom(Zoom3rdPerson)
end
end
third_person.update(dt, smoothedSpeed)
if not next(noHeadBobbing) then head_bobbing.update(dt, smoothedSpeed) end
if slowViewChange then
@ -312,15 +327,6 @@ return {
engineHandlers = {
onUpdate = onUpdate,
onFrame = onFrame,
onInputAction = function(action)
if core.isWorldPaused() or I.UI.getMode() then return end
if action == input.ACTION.ZoomIn then
zoom(10)
elseif action == input.ACTION.ZoomOut then
zoom(-10)
end
move360.onInputAction(action)
end,
onTeleported = function()
camera.instantTransition()
end,
@ -329,7 +335,7 @@ return {
if data and data.distance then third_person.baseDistance = data.distance end
end,
onSave = function()
return {version = 0, distance = third_person.baseDistance}
return { version = 0, distance = third_person.baseDistance }
end,
},
}

@ -30,6 +30,26 @@ local function turnOff()
end
end
local function processZoom3rdPerson()
if
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or
input.getBooleanActionValue('TogglePOV') or
not I.Camera.isModeControlEnabled()
then
return
end
local Zoom3rdPerson = input.getNumberActionValue('Zoom3rdPerson')
if Zoom3rdPerson > 0 and camera.getMode() == MODE.Preview
and I.Camera.getBaseThirdPersonDistance() == 30 then
self.controls.yawChange = camera.getYaw() - self.rotation:getYaw()
camera.setMode(MODE.FirstPerson)
elseif Zoom3rdPerson < 0 and camera.getMode() == MODE.FirstPerson then
camera.setMode(MODE.Preview)
I.Camera.setBaseThirdPersonDistance(30)
end
end
function M.onFrame(dt)
if core.isWorldPaused() then return end
local newActive = M.enabled and Actor.getStance(self) == Actor.STANCE.Nothing
@ -39,9 +59,10 @@ function M.onFrame(dt)
turnOff()
end
if not active then return end
processZoom3rdPerson()
if camera.getMode() == MODE.Static then return end
if camera.getMode() == MODE.ThirdPerson then camera.setMode(MODE.Preview) end
if camera.getMode() == MODE.Preview and not input.isActionPressed(input.ACTION.TogglePOV) then
if camera.getMode() == MODE.Preview and not input.getBooleanActionValue('TogglePOV') then
camera.showCrosshair(camera.getFocalPreferredOffset():length() > 5)
local move = util.vector2(self.controls.sideMovement, self.controls.movement)
local yawDelta = camera.getYaw() - self.rotation:getYaw()
@ -59,22 +80,4 @@ function M.onFrame(dt)
end
end
function M.onInputAction(action)
if not active or core.isWorldPaused() or
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or
input.isActionPressed(input.ACTION.TogglePOV) or
not I.Camera.isModeControlEnabled() then
return
end
if action == input.ACTION.ZoomIn and camera.getMode() == MODE.Preview
and I.Camera.getBaseThirdPersonDistance() == 30 then
self.controls.yawChange = camera.getYaw() - self.rotation:getYaw()
camera.setMode(MODE.FirstPerson)
elseif action == input.ACTION.ZoomOut and camera.getMode() == MODE.FirstPerson then
camera.setMode(MODE.Preview)
I.Camera.setBaseThirdPersonDistance(30)
end
end
return M

@ -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), {})

@ -1,12 +1,12 @@
local core = require('openmw.core')
local input = require('openmw.input')
local self = require('openmw.self')
local util = require('openmw.util')
local storage = require('openmw.storage')
local ui = require('openmw.ui')
local async = require('openmw.async')
local Actor = require('openmw.types').Actor
local Player = require('openmw.types').Player
local storage = require('openmw.storage')
local I = require('openmw.interfaces')
local settingsGroup = 'SettingsOMWControls'
@ -16,16 +16,16 @@ local function boolSetting(key, default)
key = key,
renderer = 'checkbox',
name = key,
description = key..'Description',
description = key .. 'Description',
default = default,
}
end
I.Settings.registerPage({
key = 'OMWControls',
l10n = 'OMWControls',
name = 'ControlsPage',
description = 'ControlsPageDescription',
key = 'OMWControls',
l10n = 'OMWControls',
name = 'ControlsPage',
description = 'ControlsPageDescription',
})
I.Settings.registerGroup({
@ -36,171 +36,239 @@ I.Settings.registerGroup({
permanentStorage = true,
settings = {
boolSetting('alwaysRun', false),
boolSetting('toggleSneak', false),
boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI
boolSetting('smoothControllerMovement', true),
},
})
local settings = storage.playerSection(settingsGroup)
local settings = storage.playerSection('SettingsOMWControls')
local attemptJump = false
local startAttack = false
local autoMove = false
local movementControlsOverridden = false
local combatControlsOverridden = false
local uiControlsOverridden = false
do
local rangeActions = {
'MoveForward',
'MoveBackward',
'MoveLeft',
'MoveRight'
}
for _, key in ipairs(rangeActions) do
input.registerAction {
key = key,
l10n = 'OMWControls',
name = key .. '_name',
description = key .. '_description',
type = input.ACTION_TYPE.Range,
defaultValue = 0,
}
end
local function processMovement()
local controllerMovement = -input.getAxisValue(input.CONTROLLER_AXIS.MoveForwardBackward)
local controllerSideMovement = input.getAxisValue(input.CONTROLLER_AXIS.MoveLeftRight)
if controllerMovement ~= 0 or controllerSideMovement ~= 0 then
-- controller movement
if util.vector2(controllerMovement, controllerSideMovement):length2() < 0.25
and not self.controls.sneak and Actor.isOnGround(self) and not Actor.isSwimming(self) then
self.controls.run = false
self.controls.movement = controllerMovement * 2
self.controls.sideMovement = controllerSideMovement * 2
else
self.controls.run = true
self.controls.movement = controllerMovement
self.controls.sideMovement = controllerSideMovement
end
local booleanActions = {
'Use',
'Run',
'Sneak',
}
for _, key in ipairs(booleanActions) do
input.registerAction {
key = key,
l10n = 'OMWControls',
name = key .. '_name',
description = key .. '_description',
type = input.ACTION_TYPE.Boolean,
defaultValue = false,
}
end
local triggers = {
'Jump',
'AutoMove',
'ToggleWeapon',
'ToggleSpell',
'AlwaysRun',
'ToggleSneak',
'Inventory',
'Journal',
'QuickKeysMenu',
}
for _, key in ipairs(triggers) do
input.registerTrigger {
key = key,
l10n = 'OMWControls',
name = key .. '_name',
description = key .. '_description',
}
end
end
local function checkNotWerewolf()
if Player.isWerewolf(self) then
ui.showMessage(core.getGMST('sWerewolfRefusal'))
return false
else
-- keyboard movement
self.controls.movement = 0
self.controls.sideMovement = 0
if input.isActionPressed(input.ACTION.MoveLeft) then
self.controls.sideMovement = self.controls.sideMovement - 1
end
if input.isActionPressed(input.ACTION.MoveRight) then
self.controls.sideMovement = self.controls.sideMovement + 1
end
if input.isActionPressed(input.ACTION.MoveBackward) then
self.controls.movement = self.controls.movement - 1
end
if input.isActionPressed(input.ACTION.MoveForward) then
self.controls.movement = self.controls.movement + 1
end
self.controls.run = input.isActionPressed(input.ACTION.Run) ~= settings:get('alwaysRun')
return true
end
if self.controls.movement ~= 0 or not Actor.canMove(self) then
end
local function isJournalAllowed()
-- During chargen journal is not allowed until magic window is allowed
return I.UI.getWindowsForMode(I.UI.MODE.Interface)[I.UI.WINDOW.Magic]
end
local movementControlsOverridden = false
local autoMove = false
local function processMovement()
local movement = input.getRangeActionValue('MoveForward') - input.getRangeActionValue('MoveBackward')
local sideMovement = input.getRangeActionValue('MoveRight') - input.getRangeActionValue('MoveLeft')
local run = input.getBooleanActionValue('Run') ~= settings:get('alwaysRun')
if movement ~= 0 or not Actor.canMove(self) then
autoMove = false
elseif autoMove then
self.controls.movement = 1
movement = 1
end
self.controls.jump = attemptJump and Player.getControlSwitch(self, Player.CONTROL_SWITCH.Jumping)
self.controls.movement = movement
self.controls.sideMovement = sideMovement
self.controls.run = run
if not settings:get('toggleSneak') then
self.controls.sneak = input.isActionPressed(input.ACTION.Sneak)
self.controls.sneak = input.getBooleanActionValue('Sneak')
end
end
local function processAttacking()
if startAttack then
self.controls.use = 1
elseif Actor.stance(self) == Actor.STANCE.Spell then
self.controls.use = 0
elseif input.getAxisValue(input.CONTROLLER_AXIS.TriggerRight) < 0.6
and not input.isActionPressed(input.ACTION.Use) then
-- 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.
self.controls.use = 0
local function controlsAllowed()
return not core.isWorldPaused()
and Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls)
and not I.UI.getMode()
end
local function movementAllowed()
return controlsAllowed() and not movementControlsOverridden
end
input.registerTriggerHandler('Jump', async:callback(function()
if not movementAllowed() then return end
self.controls.jump = Player.getControlSwitch(self, Player.CONTROL_SWITCH.Jumping)
end))
input.registerTriggerHandler('ToggleSneak', async:callback(function()
if not movementAllowed() then return end
if settings:get('toggleSneak') then
self.controls.sneak = not self.controls.sneak
end
end))
input.registerTriggerHandler('AlwaysRun', async:callback(function()
if not movementAllowed() then return end
settings:set('alwaysRun', not settings:get('alwaysRun'))
end))
input.registerTriggerHandler('AutoMove', async:callback(function()
if not movementAllowed() then return end
autoMove = not autoMove
end))
local combatControlsOverridden = false
local function combatAllowed()
return controlsAllowed() and not combatControlsOverridden
end
local function onFrame(dt)
local controlsAllowed = Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls)
and not core.isWorldPaused() and not I.UI.getMode()
if not movementControlsOverridden then
if controlsAllowed then
processMovement()
else
self.controls.movement = 0
self.controls.sideMovement = 0
self.controls.jump = false
input.registerTriggerHandler('ToggleSpell', async:callback(function()
if not combatAllowed() then return end
if Actor.stance(self) == Actor.STANCE.Spell then
Actor.setStance(self, Actor.STANCE.Nothing)
elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Magic) then
if checkNotWerewolf() then
Actor.setStance(self, Actor.STANCE.Spell)
end
end
if controlsAllowed and not combatControlsOverridden then
processAttacking()
end))
input.registerTriggerHandler('ToggleWeapon', async:callback(function()
if not combatAllowed() then return end
if Actor.stance(self) == Actor.STANCE.Weapon then
Actor.setStance(self, Actor.STANCE.Nothing)
elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Fighting) then
Actor.setStance(self, Actor.STANCE.Weapon)
end
attemptJump = false
startAttack = false
end
end))
local function checkNotWerewolf()
if Player.isWerewolf(self) then
ui.showMessage(core.getGMST('sWerewolfRefusal'))
return false
local startUse = false
input.registerActionHandler('Use', async:callback(function(value)
if value then startUse = true end
end))
local function processAttacking()
if Actor.stance(self) == Actor.STANCE.Spell then
self.controls.use = startUse and 1 or 0
else
return true
self.controls.use = input.getBooleanActionValue('Use') and 1 or 0
end
startUse = false
end
local function isJournalAllowed()
-- During chargen journal is not allowed until magic window is allowed
return I.UI.getWindowsForMode(I.UI.MODE.Interface)[I.UI.WINDOW.Magic]
local uiControlsOverridden = false
input.registerTriggerHandler('ToggleWeapon', async:callback(function()
if not combatAllowed() then return end
if Actor.stance(self) == Actor.STANCE.Weapon then
Actor.setStance(self, Actor.STANCE.Nothing)
elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Fighting) then
Actor.setStance(self, Actor.STANCE.Weapon)
end
end))
local function uiAllowed()
return Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) and not uiControlsOverridden
end
local function onInputAction(action)
if not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) then
return
end
if not uiControlsOverridden then
if action == input.ACTION.Inventory then
if I.UI.getMode() == nil then
I.UI.setMode(I.UI.MODE.Interface)
elseif I.UI.getMode() == I.UI.MODE.Interface or I.UI.getMode() == I.UI.MODE.Container then
I.UI.removeMode(I.UI.getMode())
end
elseif action == input.ACTION.Journal then
if I.UI.getMode() == I.UI.MODE.Journal then
I.UI.removeMode(I.UI.MODE.Journal)
elseif isJournalAllowed() then
I.UI.addMode(I.UI.MODE.Journal)
end
elseif action == input.ACTION.QuickKeysMenu then
if I.UI.getMode() == I.UI.MODE.QuickKeysMenu then
I.UI.removeMode(I.UI.MODE.QuickKeysMenu)
elseif checkNotWerewolf() and Player.isCharGenFinished(self) then
I.UI.addMode(I.UI.MODE.QuickKeysMenu)
end
end
input.registerTriggerHandler('Inventory', async:callback(function()
if not uiAllowed() then return end
if I.UI.getMode() == nil then
I.UI.setMode(I.UI.MODE.Interface)
elseif I.UI.getMode() == I.UI.MODE.Interface or I.UI.getMode() == I.UI.MODE.Container then
I.UI.removeMode(I.UI.getMode())
end
end))
input.registerTriggerHandler('Journal', async:callback(function()
if not uiAllowed() then return end
if core.isWorldPaused() or I.UI.getMode() then
return
if I.UI.getMode() == I.UI.MODE.Journal then
I.UI.removeMode(I.UI.MODE.Journal)
elseif isJournalAllowed() then
I.UI.addMode(I.UI.MODE.Journal)
end
end))
if action == input.ACTION.Jump then
attemptJump = true
elseif action == input.ACTION.Use then
startAttack = Actor.stance(self) ~= Actor.STANCE.Nothing
elseif action == input.ACTION.AutoMove and not movementControlsOverridden then
autoMove = not autoMove
elseif action == input.ACTION.AlwaysRun and not movementControlsOverridden then
settings:set('alwaysRun', not settings:get('alwaysRun'))
elseif action == input.ACTION.Sneak and not movementControlsOverridden then
if settings:get('toggleSneak') then
self.controls.sneak = not self.controls.sneak
end
elseif action == input.ACTION.ToggleSpell and not combatControlsOverridden then
if Actor.stance(self) == Actor.STANCE.Spell then
Actor.setStance(self, Actor.STANCE.Nothing)
elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Magic) then
if checkNotWerewolf() then
Actor.setStance(self, Actor.STANCE.Spell)
end
end
elseif action == input.ACTION.ToggleWeapon and not combatControlsOverridden then
if Actor.stance(self) == Actor.STANCE.Weapon then
Actor.setStance(self, Actor.STANCE.Nothing)
elseif Player.getControlSwitch(self, Player.CONTROL_SWITCH.Fighting) then
Actor.setStance(self, Actor.STANCE.Weapon)
end
input.registerTriggerHandler('QuickKeysMenu', async:callback(function()
if not uiAllowed() then return end
if I.UI.getMode() == I.UI.MODE.QuickKeysMenu then
I.UI.removeMode(I.UI.MODE.QuickKeysMenu)
elseif checkNotWerewolf() and Player.isCharGenFinished(self) then
I.UI.addMode(I.UI.MODE.QuickKeysMenu)
end
end))
local function onFrame(_)
if movementAllowed() then
processMovement()
elseif not movementControlsOverridden then
self.controls.movement = 0
self.controls.sideMovement = 0
self.controls.jump = false
end
if combatAllowed() then
processAttacking()
end
end
local function onSave()
return {sneaking = self.controls.sneak}
return {
sneaking = self.controls.sneak
}
end
local function onLoad(data)
@ -211,7 +279,6 @@ end
return {
engineHandlers = {
onFrame = onFrame,
onInputAction = onInputAction,
onSave = onSave,
onLoad = onLoad,
},
@ -242,4 +309,3 @@ return {
overrideUiControls = function(v) uiControlsOverridden = v end,
}
}

@ -11,8 +11,7 @@
-- @return #boolean
---
-- Is a specific control currently pressed.
-- Input bindings can be changed ingame using Options/Controls menu.
-- (DEPRECATED, use getBooleanActionValue) Input bindings can be changed ingame using Options/Controls menu.
-- @function [parent=#input] isActionPressed
-- @param #number actionId One of @{openmw.input#ACTION}
-- @return #boolean
@ -108,6 +107,7 @@
-- @field [parent=#input] #CONTROL_SWITCH CONTROL_SWITCH
---
-- (DEPRECATED, use actions with matching keys)
-- @type ACTION
-- @field [parent=#ACTION] #number GameMenu
-- @field [parent=#ACTION] #number Screenshot
@ -153,7 +153,7 @@
-- @field [parent=#ACTION] #number TogglePostProcessorHUD
---
-- Values that can be used with isActionPressed.
-- (DEPRECATED, use getBooleanActionValue) Values that can be used with isActionPressed.
-- @field [parent=#input] #ACTION ACTION
---
@ -187,10 +187,10 @@
-- @field [parent=#CONTROLLER_AXIS] #number RightY Right stick vertical axis (from -1 to 1)
-- @field [parent=#CONTROLLER_AXIS] #number TriggerLeft Left trigger (from 0 to 1)
-- @field [parent=#CONTROLLER_AXIS] #number TriggerRight Right trigger (from 0 to 1)
-- @field [parent=#CONTROLLER_AXIS] #number LookUpDown View direction vertical axis (RightY by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number LookLeftRight View direction horizontal axis (RightX by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number MoveForwardBackward Movement forward/backward (LeftY by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number MoveLeftRight Side movement (LeftX by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number LookUpDown (DEPRECATED, use the LookUpDown action) View direction vertical axis (RightY by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number LookLeftRight (DEPRECATED, use the LookLeftRight action) View direction horizontal axis (RightX by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number MoveForwardBackward (DEPRECATED, use the MoveForwardBackward action) Movement forward/backward (LeftY by default, can be mapped to another axis in Options/Controls menu)
-- @field [parent=#CONTROLLER_AXIS] #number MoveLeftRight (DEPRECATED, use the MoveLeftRight action) Side movement (LeftX by default, can be mapped to another axis in Options/Controls menu)
---
-- Values that can be used with getAxisValue.
@ -327,4 +327,105 @@
-- @field [parent=#TouchEvent] openmw.util#Vector2 position Relative position on the touch device (0 to 1 from top left corner),
-- @field [parent=#TouchEvent] #number pressure Pressure of the finger.
---
-- @type ActionType
---
-- @type ACTION_TYPE
-- @field #ActionType Boolean Input action with value of true or false
-- @field #ActionType Number Input action with a numeric value
-- @field #ActionType Range Input action with a numeric value between 0 and 1 (inclusive)
---
-- Values that can be used in registerAction
-- @field [parent=#input] #ACTION_TYPE ACTION_TYPE
---
-- @type ActionInfo
-- @field [parent=#Actioninfo] #string key
-- @field [parent=#Actioninfo] #ActionType type
-- @field [parent=#Actioninfo] #string l10n Localization context containing the name and description keys
-- @field [parent=#Actioninfo] #string name Localization key of the action's name
-- @field [parent=#Actioninfo] #string description Localization key of the action's description
-- @field [parent=#Actioninfo] defaultValue initial value of the action
---
-- Map of all currently registered actions
-- @field [parent=#input] #map<#string,#ActionInfo> actions
---
-- Registers a new input action. The key must be unique
-- @function [parent=#input] registerAction
-- @param #ActionInfo info
---
-- Provides a function computing the value of given input action.
-- The callback is called once a frame, after the values of dependency actions are resolved.
-- Throws an error if a cyclic action dependency is detected.
-- @function [parent=#input] bindAction
-- @param #string key
-- @param openmw.async#Callback callback returning the new value of the action, and taking as arguments:
-- frame time in seconds,
-- value of the function,
-- value of the first dependency action,
-- ...
-- @param #list<#string> dependencies
-- @usage
-- input.bindAction('Activate', async:callback(function(dt, use, sneak, run)
-- -- while sneaking, only activate things while holding the run binding
-- return use and (run or not sneak)
-- end), { 'Sneak', 'Run' })
---
-- Registers a function to be called whenever the action's value changes
-- @function [parent=#input] registerActionHandler
-- @param #string key
-- @param openmw.async#Callback callback takes the new action value as the only argument
---
-- Returns the value of a Boolean action
-- @function [parent=#input] getBooleanActionValue
-- @param #string key
-- @return #boolean
---
-- Returns the value of a Number action
-- @function [parent=#input] getNumberActionValue
-- @param #string key
-- @return #number
---
-- Returns the value of a Range action
-- @function [parent=#input] getRangeActionValue
-- @param #string key
-- @return #number
---
-- @type TriggerInfo
-- @field [parent=#Actioninfo] #string key
-- @field [parent=#Actioninfo] #string l10n Localization context containing the name and description keys
-- @field [parent=#Actioninfo] #string name Localization key of the trigger's name
-- @field [parent=#Actioninfo] #string description Localization key of the trigger's description
---
-- Map of all currently registered triggers
-- @field [parent=#input] #map<#string,#TriggerInfo> triggers
---
-- Registers a new input trigger. The key must be unique
-- @function [parent=#input] registerTrigger
-- @param #TriggerInfo info
---
-- Registers a function to be called whenever the trigger activates
-- @function [parent=#input] registerTriggerHandler
-- @param #string key
-- @param openmw.async#Callback callback takes the new action value as the only argument
---
-- Activates the trigger with the given key
-- @function [parent=#input] activateTrigger
-- @param #string key
return nil

Loading…
Cancel
Save