mirror of
https://github.com/OpenMW/openmw.git
synced 2025-04-30 20:11:23 +00:00
Merge branch 'lua_keybinds' into 'master'
Lua Implement mouse input engine handlers, improve inputBinding renderer See merge request OpenMW/openmw!3855
This commit is contained in:
commit
ec1cf46ec7
7 changed files with 210 additions and 65 deletions
|
@ -83,6 +83,12 @@ namespace MWBase
|
||||||
|
|
||||||
struct InputEvent
|
struct InputEvent
|
||||||
{
|
{
|
||||||
|
struct WheelChange
|
||||||
|
{
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
};
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
KeyPressed,
|
KeyPressed,
|
||||||
|
@ -93,8 +99,11 @@ namespace MWBase
|
||||||
TouchPressed,
|
TouchPressed,
|
||||||
TouchReleased,
|
TouchReleased,
|
||||||
TouchMoved,
|
TouchMoved,
|
||||||
|
MouseButtonPressed,
|
||||||
|
MouseButtonReleased,
|
||||||
|
MouseWheel,
|
||||||
} mType;
|
} mType;
|
||||||
std::variant<SDL_Keysym, int, SDLUtil::TouchEvent> mValue;
|
std::variant<SDL_Keysym, int, SDLUtil::TouchEvent, WheelChange> mValue;
|
||||||
};
|
};
|
||||||
virtual void inputEvent(const InputEvent& event) = 0;
|
virtual void inputEvent(const InputEvent& event) = 0;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
#include "../mwbase/environment.hpp"
|
#include "../mwbase/environment.hpp"
|
||||||
#include "../mwbase/inputmanager.hpp"
|
#include "../mwbase/inputmanager.hpp"
|
||||||
|
#include "../mwbase/luamanager.hpp"
|
||||||
#include "../mwbase/windowmanager.hpp"
|
#include "../mwbase/windowmanager.hpp"
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
|
|
||||||
|
@ -119,15 +120,22 @@ namespace MWInput
|
||||||
mBindingsManager->setPlayerControlsEnabled(!guiMode);
|
mBindingsManager->setPlayerControlsEnabled(!guiMode);
|
||||||
mBindingsManager->mouseReleased(arg, id);
|
mBindingsManager->mouseReleased(arg, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MWBase::Environment::get().getLuaManager()->inputEvent(
|
||||||
|
{ MWBase::LuaManager::InputEvent::MouseButtonReleased, arg.button });
|
||||||
}
|
}
|
||||||
|
|
||||||
void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg)
|
void MouseManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg)
|
||||||
{
|
{
|
||||||
MWBase::InputManager* input = MWBase::Environment::get().getInputManager();
|
MWBase::InputManager* input = MWBase::Environment::get().getInputManager();
|
||||||
if (mBindingsManager->isDetectingBindingState() || !input->controlsDisabled())
|
if (mBindingsManager->isDetectingBindingState() || !input->controlsDisabled())
|
||||||
|
{
|
||||||
mBindingsManager->mouseWheelMoved(arg);
|
mBindingsManager->mouseWheelMoved(arg);
|
||||||
|
}
|
||||||
|
|
||||||
input->setJoystickLastUsed(false);
|
input->setJoystickLastUsed(false);
|
||||||
|
MWBase::Environment::get().getLuaManager()->inputEvent({ MWBase::LuaManager::InputEvent::MouseWheel,
|
||||||
|
MWBase::LuaManager::InputEvent::WheelChange{ arg.x, arg.y } });
|
||||||
}
|
}
|
||||||
|
|
||||||
void MouseManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 id)
|
void MouseManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 id)
|
||||||
|
@ -161,7 +169,11 @@ namespace MWInput
|
||||||
const MWGui::SettingsWindow* settingsWindow
|
const MWGui::SettingsWindow* settingsWindow
|
||||||
= MWBase::Environment::get().getWindowManager()->getSettingsWindow();
|
= MWBase::Environment::get().getWindowManager()->getSettingsWindow();
|
||||||
if ((!settingsWindow || !settingsWindow->isVisible()) && !input->controlsDisabled())
|
if ((!settingsWindow || !settingsWindow->isVisible()) && !input->controlsDisabled())
|
||||||
|
{
|
||||||
mBindingsManager->mousePressed(arg, id);
|
mBindingsManager->mousePressed(arg, id);
|
||||||
|
}
|
||||||
|
MWBase::Environment::get().getLuaManager()->inputEvent(
|
||||||
|
{ MWBase::LuaManager::InputEvent::MouseButtonPressed, arg.button });
|
||||||
}
|
}
|
||||||
|
|
||||||
void MouseManager::updateCursorMode()
|
void MouseManager::updateCursorMode()
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace MWLua
|
||||||
{
|
{
|
||||||
mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers,
|
mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers,
|
||||||
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed,
|
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed,
|
||||||
&mTouchpadReleased, &mTouchpadMoved });
|
&mTouchpadReleased, &mTouchpadMoved, &mMouseButtonPress, &mMouseButtonRelease, &mMouseWheel });
|
||||||
}
|
}
|
||||||
|
|
||||||
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
|
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
|
||||||
|
@ -53,6 +53,16 @@ namespace MWLua
|
||||||
case InputEvent::TouchMoved:
|
case InputEvent::TouchMoved:
|
||||||
mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get<SDLUtil::TouchEvent>(event.mValue));
|
mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get<SDLUtil::TouchEvent>(event.mValue));
|
||||||
break;
|
break;
|
||||||
|
case InputEvent::MouseButtonPressed:
|
||||||
|
mScriptsContainer->callEngineHandlers(mMouseButtonPress, std::get<int>(event.mValue));
|
||||||
|
break;
|
||||||
|
case InputEvent::MouseButtonReleased:
|
||||||
|
mScriptsContainer->callEngineHandlers(mMouseButtonRelease, std::get<int>(event.mValue));
|
||||||
|
break;
|
||||||
|
case InputEvent::MouseWheel:
|
||||||
|
auto wheelEvent = std::get<MWBase::LuaManager::InputEvent::WheelChange>(event.mValue);
|
||||||
|
mScriptsContainer->callEngineHandlers(mMouseWheel, wheelEvent.y, wheelEvent.x);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +76,9 @@ namespace MWLua
|
||||||
typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" };
|
typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" };
|
||||||
typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" };
|
typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" };
|
||||||
typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" };
|
typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" };
|
||||||
|
typename Container::EngineHandlerList mMouseButtonPress{ "onMouseButtonPress" };
|
||||||
|
typename Container::EngineHandlerList mMouseButtonRelease{ "onMouseButtonRelease" };
|
||||||
|
typename Container::EngineHandlerList mMouseWheel{ "onMouseWheel" };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,15 @@ Engine handler is a function defined by a script, that can be called by the engi
|
||||||
* - onTouchMove(touchEvent)
|
* - onTouchMove(touchEvent)
|
||||||
- | A finger moved on a touch device.
|
- | A finger moved on a touch device.
|
||||||
| `Touch event <openmw_input.html##(TouchEvent)>`_.
|
| `Touch event <openmw_input.html##(TouchEvent)>`_.
|
||||||
|
* - onMouseButtonPress(button)
|
||||||
|
- | A mouse button was pressed
|
||||||
|
| Button id
|
||||||
|
* - onMouseButtonRelease(button)
|
||||||
|
- | A mouse button was released
|
||||||
|
| Button id
|
||||||
|
* - onMouseWheel(vertical, horizontal)
|
||||||
|
- | Mouse wheel was scrolled
|
||||||
|
| vertical and horizontal mouse wheel change
|
||||||
* - | onConsoleCommand(
|
* - | onConsoleCommand(
|
||||||
| mode, command, selectedObject)
|
| mode, command, selectedObject)
|
||||||
- | User entered `command` in in-game console. Called if either
|
- | User entered `command` in in-game console. Called if either
|
||||||
|
|
|
@ -143,10 +143,9 @@ Table with the following fields:
|
||||||
* - name
|
* - name
|
||||||
- type (default)
|
- type (default)
|
||||||
- description
|
- description
|
||||||
* - type
|
|
||||||
- 'keyboardPress', 'keyboardHold'
|
|
||||||
- The type of input that's allowed to be bound
|
|
||||||
* - key
|
* - key
|
||||||
- #string
|
- #string
|
||||||
- Key of the action or trigger to which the input is bound
|
- Key of the action or trigger to which the input is bound
|
||||||
|
* - type
|
||||||
|
- 'action', 'trigger'
|
||||||
|
- Type of the key
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
local core = require('openmw.core')
|
|
||||||
local input = require('openmw.input')
|
local input = require('openmw.input')
|
||||||
local util = require('openmw.util')
|
local util = require('openmw.util')
|
||||||
local async = require('openmw.async')
|
local async = require('openmw.async')
|
||||||
local storage = require('openmw.storage')
|
local storage = require('openmw.storage')
|
||||||
local ui = require('openmw.ui')
|
|
||||||
|
|
||||||
local I = require('openmw.interfaces')
|
|
||||||
|
|
||||||
local actionPressHandlers = {}
|
local actionPressHandlers = {}
|
||||||
local function onActionPress(id, handler)
|
local function onActionPress(id, handler)
|
||||||
|
@ -89,48 +85,87 @@ end
|
||||||
|
|
||||||
local bindingSection = storage.playerSection('OMWInputBindings')
|
local bindingSection = storage.playerSection('OMWInputBindings')
|
||||||
|
|
||||||
local keyboardPresses = {}
|
local devices = {
|
||||||
local keybordHolds = {}
|
keyboard = true,
|
||||||
local boundActions = {}
|
mouse = true,
|
||||||
|
controller = true
|
||||||
|
}
|
||||||
|
|
||||||
local function bindAction(action)
|
local function invalidBinding(binding)
|
||||||
if boundActions[action] then return end
|
if not binding.key then
|
||||||
boundActions[action] = true
|
return 'has no key'
|
||||||
input.bindAction(action, async:callback(function()
|
elseif binding.type ~= 'action' and binding.type ~= 'trigger' then
|
||||||
if keybordHolds[action] then
|
return string.format('has invalid type', binding.type)
|
||||||
for _, binding in pairs(keybordHolds[action]) do
|
elseif binding.type == 'action' and not input.actions[binding.key] then
|
||||||
if input.isKeyPressed(binding.code) then return true end
|
return string.format("action %s doesn't exist", binding.key)
|
||||||
|
elseif binding.type == 'trigger' and not input.triggers[binding.key] then
|
||||||
|
return string.format("trigger %s doesn't exist", binding.key)
|
||||||
|
elseif not binding.device or not devices[binding.device] then
|
||||||
|
return string.format("invalid device %s", binding.device)
|
||||||
|
elseif not binding.button then
|
||||||
|
return 'has no button'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local boundActions = {}
|
||||||
|
local actionBindings = {}
|
||||||
|
|
||||||
|
local function bindAction(binding, id)
|
||||||
|
local action = binding.key
|
||||||
|
actionBindings[action] = actionBindings[action] or {}
|
||||||
|
actionBindings[action][id] = binding
|
||||||
|
if not boundActions[action] then
|
||||||
|
boundActions[binding.key] = true
|
||||||
|
input.bindAction(action, async:callback(function()
|
||||||
|
for _, binding in pairs(actionBindings[action] or {}) do
|
||||||
|
if binding.device == 'keyboard' then
|
||||||
|
if input.isKeyPressed(binding.button) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
elseif binding.device == 'mouse' then
|
||||||
|
if input.isMouseButtonPressed(binding.button) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
elseif binding.device == 'controller' then
|
||||||
|
if input.isControllerButtonPressed(binding.button) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
return false
|
||||||
return false
|
end), {})
|
||||||
end), {})
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local triggerBindings = {}
|
||||||
|
for device in pairs(devices) do triggerBindings[device] = {} end
|
||||||
|
|
||||||
|
local function bindTrigger(binding, id)
|
||||||
|
local deviceBindings = triggerBindings[binding.device]
|
||||||
|
deviceBindings[binding.button] = deviceBindings[binding.button] or {}
|
||||||
|
deviceBindings[binding.button][id] = binding
|
||||||
end
|
end
|
||||||
|
|
||||||
local function registerBinding(binding, id)
|
local function registerBinding(binding, id)
|
||||||
if not input.actions[binding.key] and not input.triggers[binding.key] then
|
local invalid = invalidBinding(binding)
|
||||||
print(string.format('Skipping binding for unknown action or trigger: "%s"', binding.key))
|
if invalid then
|
||||||
return
|
print(string.format('Skipping invalid binding %s: %s', id, invalid))
|
||||||
end
|
elseif binding.type == 'action' then
|
||||||
if binding.type == 'keyboardPress' then
|
bindAction(binding, id)
|
||||||
local bindings = keyboardPresses[binding.code] or {}
|
elseif binding.type == 'trigger' then
|
||||||
bindings[id] = binding
|
bindTrigger(binding, id)
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
function clearBinding(id)
|
function clearBinding(id)
|
||||||
for _, boundTriggers in pairs(keyboardPresses) do
|
for _, deviceBindings in pairs(triggerBindings) do
|
||||||
boundTriggers[id] = nil
|
for _, buttonBindings in pairs(deviceBindings) do
|
||||||
|
buttonBindings[id] = nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
for _, boundKeys in pairs(keybordHolds) do
|
|
||||||
boundKeys[id] = nil
|
for _, bindings in pairs(actionBindings) do
|
||||||
|
bindings[id] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -170,11 +205,24 @@ return {
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
onKeyPress = function(e)
|
onKeyPress = function(e)
|
||||||
local bindings = keyboardPresses[e.code]
|
local buttonTriggers = triggerBindings.keyboard[e.code]
|
||||||
if bindings then
|
if not buttonTriggers then return end
|
||||||
for _, binding in pairs(bindings) do
|
for _, binding in pairs(buttonTriggers) do
|
||||||
input.activateTrigger(binding.key)
|
input.activateTrigger(binding.key)
|
||||||
end
|
end
|
||||||
|
end,
|
||||||
|
onMouseButtonPress = function(button)
|
||||||
|
local buttonTriggers = triggerBindings.mouse[button]
|
||||||
|
if not buttonTriggers then return end
|
||||||
|
for _, binding in pairs(buttonTriggers) do
|
||||||
|
input.activateTrigger(binding.key)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
onControllerButtonPress = function(id)
|
||||||
|
local buttonTriggers = triggerBindings.controller[id]
|
||||||
|
if not buttonTriggers then return end
|
||||||
|
for _, binding in pairs(buttonTriggers) do
|
||||||
|
input.activateTrigger(binding.key)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,14 +44,63 @@ local bindingSection = storage.playerSection('OMWInputBindings')
|
||||||
|
|
||||||
local recording = nil
|
local recording = nil
|
||||||
|
|
||||||
|
local mouseButtonNames = {
|
||||||
|
[1] = 'Left',
|
||||||
|
[2] = 'Middle',
|
||||||
|
[3] = 'Right',
|
||||||
|
[4] = '4',
|
||||||
|
[5] = '5',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- TODO: support different controllers, use icons to render controller buttons
|
||||||
|
local controllerButtonNames = {
|
||||||
|
[-1] = 'Invalid',
|
||||||
|
[input.CONTROLLER_BUTTON.A] = "A",
|
||||||
|
[input.CONTROLLER_BUTTON.B] = "B",
|
||||||
|
[input.CONTROLLER_BUTTON.X] = "X",
|
||||||
|
[input.CONTROLLER_BUTTON.Y] = "Y",
|
||||||
|
[input.CONTROLLER_BUTTON.Back] = "Back",
|
||||||
|
[input.CONTROLLER_BUTTON.Guide] = "Guide",
|
||||||
|
[input.CONTROLLER_BUTTON.Start] = "Start",
|
||||||
|
[input.CONTROLLER_BUTTON.LeftStick] = "Left Stick",
|
||||||
|
[input.CONTROLLER_BUTTON.RightStick] = "Right Stick",
|
||||||
|
[input.CONTROLLER_BUTTON.LeftShoulder] = "LB",
|
||||||
|
[input.CONTROLLER_BUTTON.RightShoulder] = "RB",
|
||||||
|
[input.CONTROLLER_BUTTON.DPadUp] = "D-pad Up",
|
||||||
|
[input.CONTROLLER_BUTTON.DPadDown] = "D-pad Down",
|
||||||
|
[input.CONTROLLER_BUTTON.DPadLeft] = "D-pad Left",
|
||||||
|
[input.CONTROLLER_BUTTON.DPadRight] = "D-pad Right",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function bindingLabel(recording, binding)
|
||||||
|
if recording then
|
||||||
|
return interfaceL10n('N/A')
|
||||||
|
elseif not binding or not binding.button then
|
||||||
|
return interfaceL10n('None')
|
||||||
|
elseif binding.device == 'keyboard' then
|
||||||
|
return input.getKeyName(binding.button)
|
||||||
|
elseif binding.device == 'mouse' then
|
||||||
|
return string.format('Mouse %s', mouseButtonNames[binding.button] or 'Unknown')
|
||||||
|
elseif binding.device == 'controller' then
|
||||||
|
return string.format('Controller %s', controllerButtonNames[binding.button] or 'Unknown')
|
||||||
|
else
|
||||||
|
return 'Unknown'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local inputTypes = {
|
||||||
|
action = input.actions,
|
||||||
|
trigger = input.triggers,
|
||||||
|
}
|
||||||
|
|
||||||
I.Settings.registerRenderer('inputBinding', function(id, set, arg)
|
I.Settings.registerRenderer('inputBinding', function(id, set, arg)
|
||||||
if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
|
if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
|
||||||
if not arg then error('inputBinding: argument with "key" and "type" is required') end
|
if not arg then error('inputBinding: argument with "key" and "type" is required') end
|
||||||
if not arg.type then error('inputBinding: type argument is required') end
|
if not arg.type then error('inputBinding: type argument is required') end
|
||||||
|
if not inputTypes[arg.type] then error('inputBinding: type must be "action" or "trigger"') end
|
||||||
if not arg.key then error('inputBinding: key 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]
|
local info = inputTypes[arg.type][arg.key]
|
||||||
if not info then return {} end
|
if not info then print(string.format('inputBinding: %s %s not found', arg.type, arg.key)) return end
|
||||||
|
|
||||||
local l10n = core.l10n(info.key)
|
local l10n = core.l10n(info.key)
|
||||||
|
|
||||||
|
@ -70,9 +119,7 @@ I.Settings.registerRenderer('inputBinding', function(id, set, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
local binding = bindingSection:get(id)
|
local binding = bindingSection:get(id)
|
||||||
local label = interfaceL10n('None')
|
local label = bindingLabel(recording and recording.id == id, binding)
|
||||||
if binding then label = input.getKeyName(binding.code) end
|
|
||||||
if recording and recording.id == id then label = interfaceL10n('N/A') end
|
|
||||||
|
|
||||||
local recorder = {
|
local recorder = {
|
||||||
template = I.MWUI.templates.textNormal,
|
template = I.MWUI.templates.textNormal,
|
||||||
|
@ -115,22 +162,30 @@ I.Settings.registerRenderer('inputBinding', function(id, set, arg)
|
||||||
return column
|
return column
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local function bindButton(device, button)
|
||||||
|
if recording == nil then return end
|
||||||
|
local binding = {
|
||||||
|
device = device,
|
||||||
|
button = button,
|
||||||
|
type = recording.arg.type,
|
||||||
|
key = recording.arg.key,
|
||||||
|
}
|
||||||
|
bindingSection:set(recording.id, binding)
|
||||||
|
local refresh = recording.refresh
|
||||||
|
recording = nil
|
||||||
|
refresh()
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
engineHandlers = {
|
engineHandlers = {
|
||||||
onKeyPress = function(key)
|
onKeyPress = function(key)
|
||||||
if recording == nil then return end
|
bindButton(key.code ~= input.KEY.Escape and 'keyboard' or nil, key.code)
|
||||||
local binding = {
|
end,
|
||||||
code = key.code,
|
onMouseButtonPress = function(button)
|
||||||
type = recording.arg.type,
|
bindButton('mouse', button)
|
||||||
key = recording.arg.key,
|
end,
|
||||||
}
|
onControllerButtonPress = function(id)
|
||||||
if key.code == input.KEY.Escape then -- TODO: prevent settings modal from closing
|
bindButton('controller', id)
|
||||||
binding.code = nil
|
|
||||||
end
|
|
||||||
bindingSection:set(recording.id, binding)
|
|
||||||
local refresh = recording.refresh
|
|
||||||
recording = nil
|
|
||||||
refresh()
|
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue