otclient-redemption/modules/modulelib/controller.lua

590 lines
15 KiB
Lua
Raw Permalink Normal View History

2023-05-12 21:20:34 -03:00
local TypeEvent = {
MODULE_INIT = 1,
GAME_INIT = 2
}
local function unregisterModalHandle(self, handle)
if not self.modalHandles or not handle then
return false
end
-- Use the stored event type to go directly to the right bucket (O(n) instead of O(n*types))
local handles = self.modalHandles[handle.controllerEventType]
if handles and table.removevalue(handles, handle) then
return true
end
return false
end
local function registerModalHandle(self, handle)
if not handle then
return nil
end
local eventType = handle.controllerEventType
if not self.modalHandles[eventType] then
self.modalHandles[eventType] = {}
end
table.insert(self.modalHandles[eventType], handle)
return handle
end
local function destroyHandle(self, handle)
if handle.htmlId then
local htmlId = handle.htmlId
handle.htmlId = nil
handle.ui = nil
g_html.destroy(htmlId)
return
end
if handle.ui and not handle.ui:isDestroyed() then
handle.ui:destroy()
end
handle.ui = nil
end
local function destroyModalHandles(self, eventType)
if not self.modalHandles then
return
end
local handles = self.modalHandles[eventType]
if not handles then
return
end
for i = #handles, 1, -1 do
destroyHandle(self, handles[i])
end
self.modalHandles[eventType] = nil
end
local function destroyUIEvents(self)
if not self.uiEvents then
return
end
for _, event in pairs(self.uiEvents) do
event:destroy()
end
self.uiEvents = {}
WidgetWatch.update() -- Prevent warnings from stale widget references in the watch list.
end
2023-05-12 21:20:34 -03:00
local function onGameStart(self)
if self.__onGameStart ~= nil then
self.currentTypeEvent = TypeEvent.GAME_INIT
addEvent(function()
self:__onGameStart()
local eventList = self.events[TypeEvent.GAME_INIT]
if eventList ~= nil then
for _, event in pairs(eventList) do
event:connect()
end
end
end)
2023-05-12 21:20:34 -03:00
end
end
local function onGameEnd(self)
if self.__onGameEnd ~= nil then
self:__onGameEnd()
end
destroyModalHandles(self, TypeEvent.GAME_INIT)
2023-05-12 21:20:34 -03:00
local eventList = self.events[TypeEvent.GAME_INIT]
if eventList ~= nil then
for _, event in pairs(eventList) do
2024-08-29 18:07:23 -03:00
event:destroy()
2023-05-12 21:20:34 -03:00
end
self.events[TypeEvent.GAME_INIT] = nil
2023-05-12 21:20:34 -03:00
end
local scheduledEventsList = self.scheduledEvents and self.scheduledEvents[TypeEvent.GAME_INIT]
if scheduledEventsList then
for _, eventId in pairs(scheduledEventsList) do
removeEvent(eventId)
end
if self.scheduledEvents then
self.scheduledEvents[TypeEvent.GAME_INIT] = nil
end
end
2024-07-16 21:36:58 -03:00
if self.dataUI ~= nil and self.dataUI.onGameStart and self.ui then
2024-08-29 18:07:23 -03:00
self:destroyUI()
2023-05-12 21:20:34 -03:00
end
2024-08-29 18:07:23 -03:00
table.remove_if(self.keyboardEvents, function(i, event)
local destroy = event.controllerEventType == TypeEvent.GAME_INIT
if destroy then
g_keyboard['unbind' .. event.name](event.args[1], event.args[2], event.args[3])
end
return destroy
end)
2023-05-12 21:20:34 -03:00
end
Controller = {
2023-05-12 21:20:34 -03:00
ui = nil,
2024-07-16 21:36:58 -03:00
name = nil,
2023-05-12 21:20:34 -03:00
attrs = nil,
extendedOpcodes = nil,
2024-07-03 22:27:40 -03:00
opcodes = nil,
2024-07-16 21:36:58 -03:00
events = nil,
uiEvents = nil,
2025-09-28 14:46:24 -03:00
htmlId = nil,
2024-07-16 21:36:58 -03:00
keyboardAnchor = nil,
scheduledEvents = nil,
keyboardEvents = nil,
modalHandles = nil
}
2025-09-28 14:46:24 -03:00
if not G_CONTROLLER_CALLED then
G_CONTROLLER_CALLED = {}
end
function Controller:new()
local module = g_modules.getCurrentModule()
2021-07-19 19:24:59 -03:00
local obj = {
name = module and module:getName() or nil,
2023-05-12 21:20:34 -03:00
currentTypeEvent = TypeEvent.MODULE_INIT,
2021-07-19 19:24:59 -03:00
events = {},
uiEvents = {},
scheduledEvents = {},
2021-07-19 19:24:59 -03:00
keyboardEvents = {},
modalHandles = {},
2023-05-12 21:20:34 -03:00
attrs = {},
extendedOpcodes = {},
opcodes = {},
2021-07-19 19:24:59 -03:00
}
setmetatable(obj, self)
self.__index = self
2025-09-28 14:46:24 -03:00
if obj.name then
G_CONTROLLER_CALLED[obj.name] = obj
end
2021-07-19 19:24:59 -03:00
return obj
end
function Controller:init()
2024-07-12 14:31:39 -03:00
if self.dataUI ~= nil then
self:loadUI()
2023-05-12 21:20:34 -03:00
end
if self.onInit then
2023-05-12 21:20:34 -03:00
self.currentTypeEvent = TypeEvent.MODULE_INIT
self:onInit()
end
2021-07-19 19:24:59 -03:00
2023-05-12 21:20:34 -03:00
self.__onGameStart = self.onGameStart
self.onGameStart = function()
onGameStart(self)
2021-07-19 19:24:59 -03:00
end
2023-05-12 21:20:34 -03:00
connect(g_game, {
onGameStart = self.onGameStart
})
if g_game.isOnline() then
self:onGameStart()
end
self.__onGameEnd = self.onGameEnd
self.onGameEnd = function()
onGameEnd(self)
end
connect(g_game, {
onGameEnd = self.onGameEnd
})
2023-05-12 21:20:34 -03:00
local eventList = self.events[TypeEvent.MODULE_INIT]
if eventList then
for _, event in pairs(eventList) do
event:connect()
2023-05-12 21:20:34 -03:00
end
end
end
2024-07-16 21:36:58 -03:00
function Controller:loadHtml(path, parent)
local suffix = ".html"
if path:sub(- #suffix) ~= suffix then
path = path .. suffix
2024-07-16 21:36:58 -03:00
end
2024-07-16 21:36:58 -03:00
self:setUI(path, parent)
2025-09-28 14:46:24 -03:00
self.htmlId = g_html.load(self.name, path, g_ui.getRootWidget())
self.ui = g_html.getRootWidget(self.htmlId);
end
function Controller:unloadHtml()
if self.htmlId == nil then
error('HTML unload operation failed: no HTML was previously loaded.')
return
end
self:destroyUI()
2024-07-16 21:36:58 -03:00
end
2024-08-29 18:07:23 -03:00
function Controller:destroyUI()
2025-09-28 14:46:24 -03:00
if self.htmlId ~= nil then
local htmlId = self.htmlId
self.htmlId = nil
g_html.destroy(htmlId)
2025-09-28 14:46:24 -03:00
self.ui = nil
2024-08-29 18:07:23 -03:00
end
if self.ui then
self.ui:destroy()
self.ui = nil
end
destroyUIEvents(self)
2024-08-29 18:07:23 -03:00
end
2025-09-28 14:46:24 -03:00
function Controller:findWidget(query)
return self.ui and self.ui:querySelector(query:trim())
2024-07-16 21:36:58 -03:00
end
function Controller:findWidgets(query)
2025-09-28 14:46:24 -03:00
return self.ui and self.ui:querySelectorAll(query:trim())
2024-07-16 21:36:58 -03:00
end
2025-09-28 14:46:24 -03:00
function Controller:createWidgetFromHTML(html, parent)
local widget = g_html.createWidgetFromHTML(html, parent, self.htmlId)
return widget
2024-07-16 21:36:58 -03:00
end
-- Opens a modal on demand without affecting self.ui / self.htmlId.
-- Supported modes:
-- 'otui' (default): source is a widget style name
-- 'file': source is an OTUI file path relative to the module
-- 'html': source is an HTML path relative to the module
-- Returns a handle that is auto-destroyed on terminate (or onGameEnd if opened during onGameStart).
function Controller:openModal(source, options)
options = options or {}
local mode = options.mode or 'otui'
local parent = options.parent
if mode == 'html' then
local suffix = ".html"
local relativePath = source
if relativePath:sub(-#suffix) ~= suffix then
relativePath = relativePath .. suffix
end
local htmlId = g_html.load(self.name, relativePath, parent or g_ui.getRootWidget())
if not htmlId or htmlId == 0 then
perror('openModal: failed to load HTML "' .. tostring(relativePath) .. '" in ' .. tostring(self.name))
return nil
end
return registerModalHandle(self, {
htmlId = htmlId,
ui = g_html.getRootWidget(htmlId),
controllerEventType = self.currentTypeEvent
})
end
local ui
if mode == 'file' then
ui = g_ui.loadUI('/' .. self.name .. '/' .. source, parent or rootWidget)
if not ui then
perror('openModal: failed to load OTUI file "' .. tostring(source) .. '" in ' .. tostring(self.name))
return nil
end
else
ui = g_ui.createWidget(source, parent or rootWidget)
if not ui then
perror('openModal: failed to create OTUI widget "' .. tostring(source) .. '" in ' .. tostring(self.name))
return nil
end
end
return registerModalHandle(self, {
ui = ui,
controllerEventType = self.currentTypeEvent
})
end
-- Destroys a handle returned by openModal.
function Controller:closeModal(handle)
if not handle then return end
if not unregisterModalHandle(self, handle) then
perror('closeModal: handle not registered in ' .. tostring(self.name))
end
destroyHandle(self, handle)
end
2024-07-12 14:31:39 -03:00
function Controller:loadUI(name, parent)
if self.ui then
return
end
if not self.dataUI then
self:setUI(name, parent)
end
self.ui = g_ui.loadUI('/' .. self.name .. '/' .. self.dataUI.name, self.dataUI.parent or g_ui.getRootWidget())
end
2024-07-03 22:27:40 -03:00
function Controller:setKeyboardAnchor(widget)
self.keyboardAnchor = widget
end
2024-07-12 14:31:39 -03:00
function Controller:setUI(name, parent)
self.dataUI = { name = name, parent = parent, onGameStart = self.currentTypeEvent == TypeEvent.GAME_INIT }
end
function Controller:terminate()
2023-05-12 21:20:34 -03:00
if self.onGameStart then
disconnect(g_game, { onGameStart = self.onGameStart })
2021-07-19 19:24:59 -03:00
end
2023-05-12 21:20:34 -03:00
if self.onGameEnd then
if g_game.isOnline() then self:onGameEnd() end
disconnect(g_game, { onGameEnd = self.onGameEnd })
end
2021-07-19 19:24:59 -03:00
if self.onTerminate then
self:onTerminate()
end
destroyModalHandles(self, TypeEvent.GAME_INIT)
destroyModalHandles(self, TypeEvent.MODULE_INIT)
for i, event in pairs(self.keyboardEvents) do
2024-07-03 22:27:40 -03:00
g_keyboard['unbind' .. event.name](event.args[1], event.args[2], event.args[3])
end
2021-07-19 19:24:59 -03:00
for i, opcode in pairs(self.extendedOpcodes) do
ProtocolGame.unregisterExtendedOpcode(opcode)
end
2021-07-19 19:24:59 -03:00
for _, opcode in ipairs(self.opcodes) do
ProtocolGame.unregisterOpcode(opcode)
end
for type, events in pairs(self.events) do
if events ~= nil then
for _, event in pairs(events) do
2024-08-29 18:07:23 -03:00
event:destroy()
end
2023-05-12 21:20:34 -03:00
end
end
for type, events in pairs(self.scheduledEvents or {}) do
if events ~= nil then
for _, eventId in pairs(events) do
removeEvent(eventId)
end
end
end
2023-05-12 21:20:34 -03:00
if self.ui ~= nil then
2025-09-28 14:46:24 -03:00
self:destroyUI()
else
destroyUIEvents(self)
2023-05-12 21:20:34 -03:00
end
self.ui = nil
self.uiEvents = nil
2023-05-12 21:20:34 -03:00
self.attrs = nil
self.events = nil
self.dataUI = nil
self.extendedOpcodes = nil
2021-07-19 19:24:59 -03:00
self.opcodes = nil
self.keyboardEvents = nil
2024-07-03 22:27:40 -03:00
self.keyboardAnchor = nil
self.scheduledEvents = nil
self.modalHandles = nil
2025-09-28 14:46:24 -03:00
self.htmlId = nil
2023-05-12 21:20:34 -03:00
self.__onGameStart = nil
self.__onGameEnd = nil
end
--[[
If you register an event in onInit() or in the general scope of the script,
the event will be automatically registered at startup and disconnected when the module is destroyed.
If it is inside onGameStart(), the events will be connected when entering the game map and disconnected when leaving and also when the module is destroyed.
]]
function Controller:registerEvents(actor, events)
2023-05-12 21:20:34 -03:00
if self.events[self.currentTypeEvent] == nil then
self.events[self.currentTypeEvent] = {}
end
local evt = EventController:new(actor, events)
table.insert(self.events[self.currentTypeEvent], evt)
2021-07-19 19:24:59 -03:00
return evt
end
function Controller:registerUIEvents(actor, events)
local evt = EventController:new(actor, events)
table.insert(self.uiEvents, evt)
-- fix html lazy loading
if actor and actor.isOnHtml and actor:isOnHtml() then
evt:connect()
end
return evt
end
function Controller:checkWidgetsDestroyed()
table.remove_if(self.uiEvents, function(i, e)
if e:actorIsDestroyed() then
e:disconnect()
return true
end
return false
end)
end
function Controller:registerExtendedOpcode(opcode, fnc)
2021-07-19 19:24:59 -03:00
ProtocolGame.registerExtendedOpcode(opcode, fnc)
table.insert(self.extendedOpcodes, opcode)
end
function Controller:registerOpcode(opcode, fnc)
ProtocolGame.registerOpcode(opcode, fnc)
2021-07-19 19:24:59 -03:00
table.insert(self.opcodes, opcode)
end
function Controller:sendExtendedOpcode(opcode, ...)
2021-07-19 19:24:59 -03:00
local protocol = g_game.getProtocolGame()
if protocol then
protocol:sendExtendedOpcode(opcode, ...)
end
end
local function registerScheduledEvent(controller, fncRef, fnc, delay, name)
local currentType = controller.currentTypeEvent
controller.scheduledEvents = controller.scheduledEvents or {}
if controller.scheduledEvents[currentType] == nil then
controller.scheduledEvents[currentType] = {}
end
local _rmvEvent = function()
if not controller.scheduledEvents then return end
local list = controller.scheduledEvents[currentType]
if not list then return end
if name and list[name] then
removeEvent(list[name])
list[name] = nil
end
end
_rmvEvent()
local evt = nil
local action = function()
local res = fnc()
if fncRef == scheduleEvent then
if name then
_rmvEvent()
else
if controller.scheduledEvents and controller.scheduledEvents[currentType] then
table.removevalue(controller.scheduledEvents[currentType], evt)
end
end
elseif res == false then
removeEvent(evt)
end
end
evt = fncRef(action, delay)
if name then
if controller.scheduledEvents then
controller.scheduledEvents[currentType][name] = evt
end
else
if controller.scheduledEvents and controller.scheduledEvents[currentType] then
table.insert(controller.scheduledEvents[currentType], evt)
end
end
return evt
end
function Controller:scheduleEvent(fnc, delay, name)
return registerScheduledEvent(self, scheduleEvent, fnc, delay, name)
end
function Controller:cycleEvent(fnc, delay, name)
return registerScheduledEvent(self, cycleEvent, fnc, delay, name)
end
function Controller:removeEvent(evt)
if self.scheduledEvents[TypeEvent.GAME_INIT] then
if table.removevalue(self.scheduledEvents[TypeEvent.GAME_INIT], evt) then
removeEvent(evt)
return
end
end
if self.scheduledEvents[TypeEvent.MODULE_INIT] then
if table.find(self.scheduledEvents[TypeEvent.MODULE_INIT], evt) then
error('It is not possible to remove events registered at controller init.')
return
end
end
error('The event was not registered by the controller.')
end
2024-07-03 22:27:40 -03:00
function Controller:bindKeyDown(...)
local args = { ... }
if args[3] == nil or type(args[3]) == 'boolean' then
args[4] = args[3]
args[3] = self.keyboardAnchor
2024-05-27 18:16:53 -03:00
end
2022-04-29 23:14:24 -03:00
table.insert(self.keyboardEvents, {
name = 'KeyDown',
2024-08-29 18:07:23 -03:00
args = args,
controllerEventType = self.currentTypeEvent
2022-04-29 23:14:24 -03:00
})
g_keyboard.bindKeyDown(args[1], args[2], args[3], args[4])
end
2024-05-27 18:16:53 -03:00
2024-07-03 22:27:40 -03:00
function Controller:bindKeyUp(...)
local args = { ... }
if args[3] == nil or type(args[3]) == 'boolean' then
args[4] = args[3]
args[3] = self.keyboardAnchor
end
table.insert(self.keyboardEvents, {
name = 'KeyUp',
2024-08-29 18:07:23 -03:00
args = args,
controllerEventType = self.currentTypeEvent
})
2024-07-03 22:27:40 -03:00
g_keyboard.bindKeyUp(args[1], args[2], args[3], args[4])
end
2024-07-03 22:27:40 -03:00
function Controller:bindKeyPress(...)
local args = { ... }
if args[3] == nil or type(args[3]) == 'boolean' then
args[4] = args[3]
args[3] = self.keyboardAnchor
end
2022-04-29 23:14:24 -03:00
table.insert(self.keyboardEvents, {
name = 'KeyPress',
2024-08-29 18:07:23 -03:00
args = args,
controllerEventType = self.currentTypeEvent
2022-04-29 23:14:24 -03:00
})
2024-07-03 22:27:40 -03:00
g_keyboard.bindKeyPress(args[1], args[2], args[3])
end