mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-16 16:29:55 +00:00
Merge branch 'lua_actions_electric_boogaloo' into 'master'
Lua actions take 3 See merge request OpenMW/openmw!2628
This commit is contained in:
commit
e9f3e5c6d1
25 changed files with 1490 additions and 201 deletions
|
@ -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();
|
||||
if (playerScripts)
|
||||
playerScripts->onFrame(MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()
|
||||
double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()
|
||||
? 0.0
|
||||
: MWBase::Environment::get().getFrameDuration());
|
||||
: MWBase::Environment::get().getFrameDuration();
|
||||
mInputActions.update(frameDuration);
|
||||
if (playerScripts)
|
||||
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
|
||||
|
||||
|
|
65
apps/openmw_test_suite/lua/test_inputactions.cpp
Normal file
65
apps/openmw_test_suite/lua/test_inputactions.cpp
Normal file
|
@ -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
|
||||
|
|
288
components/lua/inputactions.cpp
Normal file
288
components/lua/inputactions.cpp
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
153
components/lua/inputactions.hpp
Normal file
153
components/lua/inputactions.hpp
Normal file
|
@ -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
|
||||
|
@ -128,7 +146,8 @@ local function zoom(delta)
|
|||
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,17 +156,6 @@ 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)
|
||||
|
@ -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
|
||||
|
|
255
files/data/scripts/omw/input/actionbindings.lua
Normal file
255
files/data/scripts/omw/input/actionbindings.lua
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
92
files/data/scripts/omw/input/smoothmovement.lua
Normal file
92
files/data/scripts/omw/input/smoothmovement.lua
Normal file
|
@ -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,7 +16,7 @@ local function boolSetting(key, default)
|
|||
key = key,
|
||||
renderer = 'checkbox',
|
||||
name = key,
|
||||
description = key..'Description',
|
||||
description = key .. 'Description',
|
||||
default = default,
|
||||
}
|
||||
end
|
||||
|
@ -36,95 +36,68 @@ 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
|
||||
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
|
||||
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')
|
||||
end
|
||||
if self.controls.movement ~= 0 or not Actor.canMove(self) then
|
||||
autoMove = false
|
||||
elseif autoMove then
|
||||
self.controls.movement = 1
|
||||
end
|
||||
self.controls.jump = attemptJump and Player.getControlSwitch(self, Player.CONTROL_SWITCH.Jumping)
|
||||
if not settings:get('toggleSneak') then
|
||||
self.controls.sneak = input.isActionPressed(input.ACTION.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 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 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
|
||||
end
|
||||
end
|
||||
if controlsAllowed and not combatControlsOverridden then
|
||||
processAttacking()
|
||||
end
|
||||
attemptJump = false
|
||||
startAttack = false
|
||||
end
|
||||
|
||||
local function checkNotWerewolf()
|
||||
if Player.isWerewolf(self) then
|
||||
ui.showMessage(core.getGMST('sWerewolfRefusal'))
|
||||
|
@ -139,50 +112,69 @@ local function isJournalAllowed()
|
|||
return I.UI.getWindowsForMode(I.UI.MODE.Interface)[I.UI.WINDOW.Magic]
|
||||
end
|
||||
|
||||
local function onInputAction(action)
|
||||
if not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) then
|
||||
return
|
||||
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
|
||||
movement = 1
|
||||
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
|
||||
end
|
||||
self.controls.movement = movement
|
||||
self.controls.sideMovement = sideMovement
|
||||
self.controls.run = run
|
||||
|
||||
if core.isWorldPaused() or I.UI.getMode() then
|
||||
return
|
||||
if not settings:get('toggleSneak') then
|
||||
self.controls.sneak = input.getBooleanActionValue('Sneak')
|
||||
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
|
||||
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
|
||||
elseif action == input.ACTION.ToggleSpell and not combatControlsOverridden then
|
||||
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
|
||||
|
||||
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
|
||||
|
@ -190,17 +182,93 @@ local function onInputAction(action)
|
|||
Actor.setStance(self, Actor.STANCE.Spell)
|
||||
end
|
||||
end
|
||||
elseif action == input.ACTION.ToggleWeapon and not combatControlsOverridden then
|
||||
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
|
||||
end))
|
||||
|
||||
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
|
||||
self.controls.use = input.getBooleanActionValue('Use') and 1 or 0
|
||||
end
|
||||
startUse = false
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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 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))
|
||||
|
||||
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…
Reference in a new issue