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 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) end end local function onGameEnd(self) if self.__onGameEnd ~= nil then self:__onGameEnd() end destroyModalHandles(self, TypeEvent.GAME_INIT) local eventList = self.events[TypeEvent.GAME_INIT] if eventList ~= nil then for _, event in pairs(eventList) do event:destroy() end self.events[TypeEvent.GAME_INIT] = nil 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 if self.dataUI ~= nil and self.dataUI.onGameStart and self.ui then self:destroyUI() end 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) end Controller = { ui = nil, name = nil, attrs = nil, extendedOpcodes = nil, opcodes = nil, events = nil, uiEvents = nil, htmlId = nil, keyboardAnchor = nil, scheduledEvents = nil, keyboardEvents = nil, modalHandles = nil } if not G_CONTROLLER_CALLED then G_CONTROLLER_CALLED = {} end function Controller:new() local module = g_modules.getCurrentModule() local obj = { name = module and module:getName() or nil, currentTypeEvent = TypeEvent.MODULE_INIT, events = {}, uiEvents = {}, scheduledEvents = {}, keyboardEvents = {}, modalHandles = {}, attrs = {}, extendedOpcodes = {}, opcodes = {}, } setmetatable(obj, self) self.__index = self if obj.name then G_CONTROLLER_CALLED[obj.name] = obj end return obj end function Controller:init() if self.dataUI ~= nil then self:loadUI() end if self.onInit then self.currentTypeEvent = TypeEvent.MODULE_INIT self:onInit() end self.__onGameStart = self.onGameStart self.onGameStart = function() onGameStart(self) end 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 }) local eventList = self.events[TypeEvent.MODULE_INIT] if eventList then for _, event in pairs(eventList) do event:connect() end end end function Controller:loadHtml(path, parent) local suffix = ".html" if path:sub(- #suffix) ~= suffix then path = path .. suffix end self:setUI(path, parent) 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() end function Controller:destroyUI() if self.htmlId ~= nil then local htmlId = self.htmlId self.htmlId = nil g_html.destroy(htmlId) self.ui = nil end if self.ui then self.ui:destroy() self.ui = nil end destroyUIEvents(self) end function Controller:findWidget(query) return self.ui and self.ui:querySelector(query:trim()) end function Controller:findWidgets(query) return self.ui and self.ui:querySelectorAll(query:trim()) end function Controller:createWidgetFromHTML(html, parent) local widget = g_html.createWidgetFromHTML(html, parent, self.htmlId) return widget 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 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 function Controller:setKeyboardAnchor(widget) self.keyboardAnchor = widget end function Controller:setUI(name, parent) self.dataUI = { name = name, parent = parent, onGameStart = self.currentTypeEvent == TypeEvent.GAME_INIT } end function Controller:terminate() if self.onGameStart then disconnect(g_game, { onGameStart = self.onGameStart }) end if self.onGameEnd then if g_game.isOnline() then self:onGameEnd() end disconnect(g_game, { onGameEnd = self.onGameEnd }) end 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 g_keyboard['unbind' .. event.name](event.args[1], event.args[2], event.args[3]) end for i, opcode in pairs(self.extendedOpcodes) do ProtocolGame.unregisterExtendedOpcode(opcode) end 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 event:destroy() end 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 if self.ui ~= nil then self:destroyUI() else destroyUIEvents(self) end self.ui = nil self.uiEvents = nil self.attrs = nil self.events = nil self.dataUI = nil self.extendedOpcodes = nil self.opcodes = nil self.keyboardEvents = nil self.keyboardAnchor = nil self.scheduledEvents = nil self.modalHandles = nil self.htmlId = nil 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) 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) 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) ProtocolGame.registerExtendedOpcode(opcode, fnc) table.insert(self.extendedOpcodes, opcode) end function Controller:registerOpcode(opcode, fnc) ProtocolGame.registerOpcode(opcode, fnc) table.insert(self.opcodes, opcode) end function Controller:sendExtendedOpcode(opcode, ...) 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 function Controller:bindKeyDown(...) 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 = 'KeyDown', args = args, controllerEventType = self.currentTypeEvent }) g_keyboard.bindKeyDown(args[1], args[2], args[3], args[4]) end 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', args = args, controllerEventType = self.currentTypeEvent }) g_keyboard.bindKeyUp(args[1], args[2], args[3], args[4]) end function Controller:bindKeyPress(...) 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 = 'KeyPress', args = args, controllerEventType = self.currentTypeEvent }) g_keyboard.bindKeyPress(args[1], args[2], args[3]) end