2023-05-12 21:20:34 -03:00
|
|
|
local TypeEvent = {
|
|
|
|
|
MODULE_INIT = 1,
|
|
|
|
|
GAME_INIT = 2
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 19:44:23 -04:00
|
|
|
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
|
|
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
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
|
|
|
|
|
|
2026-05-02 19:44:23 -04:00
|
|
|
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()
|
|
|
|
|
|
2024-07-04 15:15:46 -03:00
|
|
|
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
|
|
|
|
|
|
2026-05-02 19:44:23 -04:00
|
|
|
destroyModalHandles(self, TypeEvent.GAME_INIT)
|
|
|
|
|
|
2023-05-12 21:20:34 -03:00
|
|
|
local eventList = self.events[TypeEvent.GAME_INIT]
|
|
|
|
|
if eventList ~= nil then
|
2024-07-04 15:15:46 -03:00
|
|
|
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
|
2024-07-04 15:15:46 -03:00
|
|
|
|
|
|
|
|
self.events[TypeEvent.GAME_INIT] = nil
|
2023-05-12 21:20:34 -03:00
|
|
|
end
|
|
|
|
|
|
2025-10-06 10:52:59 -03:00
|
|
|
local scheduledEventsList = self.scheduledEvents and self.scheduledEvents[TypeEvent.GAME_INIT]
|
2024-07-11 18:08:21 -03:00
|
|
|
if scheduledEventsList then
|
|
|
|
|
for _, eventId in pairs(scheduledEventsList) do
|
|
|
|
|
removeEvent(eventId)
|
|
|
|
|
end
|
|
|
|
|
|
2025-10-06 10:52:59 -03:00
|
|
|
if self.scheduledEvents then
|
|
|
|
|
self.scheduledEvents[TypeEvent.GAME_INIT] = nil
|
|
|
|
|
end
|
2024-07-11 18:08:21 -03:00
|
|
|
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
|
|
|
|
|
|
2020-07-30 02:10:06 -03:00
|
|
|
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,
|
2024-12-10 14:51:30 -03:00
|
|
|
extendedOpcodes = nil,
|
2024-07-03 22:27:40 -03:00
|
|
|
opcodes = nil,
|
2024-07-16 21:36:58 -03:00
|
|
|
events = nil,
|
2025-10-04 18:50:59 -03:00
|
|
|
uiEvents = nil,
|
2025-09-28 14:46:24 -03:00
|
|
|
htmlId = nil,
|
2024-07-16 21:36:58 -03:00
|
|
|
keyboardAnchor = nil,
|
|
|
|
|
scheduledEvents = nil,
|
2026-05-02 19:44:23 -04:00
|
|
|
keyboardEvents = nil,
|
|
|
|
|
modalHandles = nil
|
2020-07-30 02:10:06 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-28 14:46:24 -03:00
|
|
|
if not G_CONTROLLER_CALLED then
|
|
|
|
|
G_CONTROLLER_CALLED = {}
|
|
|
|
|
end
|
|
|
|
|
|
2020-07-30 02:10:06 -03:00
|
|
|
function Controller:new()
|
2025-01-18 06:31:57 -03:00
|
|
|
local module = g_modules.getCurrentModule()
|
2021-07-19 19:24:59 -03:00
|
|
|
local obj = {
|
2025-01-18 06:31:57 -03:00
|
|
|
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 = {},
|
2025-10-04 18:50:59 -03:00
|
|
|
uiEvents = {},
|
2024-07-11 18:08:21 -03:00
|
|
|
scheduledEvents = {},
|
2021-07-19 19:24:59 -03:00
|
|
|
keyboardEvents = {},
|
2026-05-02 19:44:23 -04:00
|
|
|
modalHandles = {},
|
2023-05-12 21:20:34 -03:00
|
|
|
attrs = {},
|
2024-12-10 14:51:30 -03:00
|
|
|
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
|
2020-07-30 02:10:06 -03:00
|
|
|
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
|
|
|
|
|
|
2022-08-17 12:51:56 -03:00
|
|
|
if self.onInit then
|
2023-05-12 21:20:34 -03:00
|
|
|
self.currentTypeEvent = TypeEvent.MODULE_INIT
|
2022-08-17 12:51:56 -03:00
|
|
|
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
|
2024-07-04 15:15:46 -03:00
|
|
|
|
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-01-12 20:08:18 -03:00
|
|
|
|
2023-05-12 21:20:34 -03:00
|
|
|
local eventList = self.events[TypeEvent.MODULE_INIT]
|
2024-07-04 15:15:46 -03:00
|
|
|
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
|
2024-07-26 14:01:35 -03:00
|
|
|
path = path .. suffix
|
2024-07-16 21:36:58 -03:00
|
|
|
end
|
2024-07-26 14:01:35 -03:00
|
|
|
|
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
|
|
|
|
|
|
2025-09-28 17:23:56 -03:00
|
|
|
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
|
2026-05-02 19:44:23 -04:00
|
|
|
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
|
|
|
|
|
|
2026-05-02 19:44:23 -04:00
|
|
|
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
|
|
|
|
|
|
2024-07-26 14:01:35 -03:00
|
|
|
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
|
|
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
-- 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
|
2026-05-02 19:44:23 -04:00
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
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
|
2026-05-02 19:44:23 -04:00
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
return registerModalHandle(self, {
|
|
|
|
|
htmlId = htmlId,
|
|
|
|
|
ui = g_html.getRootWidget(htmlId),
|
|
|
|
|
controllerEventType = self.currentTypeEvent
|
|
|
|
|
})
|
2026-05-02 19:44:23 -04:00
|
|
|
end
|
|
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
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
|
2026-05-02 19:44:23 -04:00
|
|
|
end
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
|
|
|
|
|
return registerModalHandle(self, {
|
|
|
|
|
ui = ui,
|
2026-05-02 19:44:23 -04:00
|
|
|
controllerEventType = self.currentTypeEvent
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
})
|
2026-05-02 19:44:23 -04:00
|
|
|
end
|
|
|
|
|
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
-- Destroys a handle returned by openModal.
|
|
|
|
|
function Controller:closeModal(handle)
|
2026-05-02 19:44:23 -04:00
|
|
|
if not handle then return end
|
|
|
|
|
if not unregisterModalHandle(self, handle) then
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
perror('closeModal: handle not registered in ' .. tostring(self.name))
|
2026-05-02 19:44:23 -04:00
|
|
|
end
|
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.
Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.
UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.
Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 10:26:11 -04:00
|
|
|
destroyHandle(self, handle)
|
2026-05-02 19:44:23 -04:00
|
|
|
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 }
|
2020-07-30 02:10:06 -03:00
|
|
|
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 })
|
2022-08-17 12:51:56 -03:00
|
|
|
end
|
2021-07-19 19:24:59 -03:00
|
|
|
|
2024-12-10 14:51:30 -03:00
|
|
|
if self.onTerminate then
|
|
|
|
|
self:onTerminate()
|
|
|
|
|
end
|
|
|
|
|
|
2026-05-02 19:44:23 -04:00
|
|
|
destroyModalHandles(self, TypeEvent.GAME_INIT)
|
|
|
|
|
destroyModalHandles(self, TypeEvent.MODULE_INIT)
|
|
|
|
|
|
2022-08-17 12:51:56 -03:00
|
|
|
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])
|
2022-08-17 12:51:56 -03:00
|
|
|
end
|
2021-07-19 19:24:59 -03:00
|
|
|
|
2024-12-10 14:51:30 -03:00
|
|
|
for i, opcode in pairs(self.extendedOpcodes) do
|
2022-08-17 12:51:56 -03:00
|
|
|
ProtocolGame.unregisterExtendedOpcode(opcode)
|
|
|
|
|
end
|
2021-07-19 19:24:59 -03:00
|
|
|
|
2024-12-10 14:51:30 -03:00
|
|
|
for _, opcode in ipairs(self.opcodes) do
|
|
|
|
|
ProtocolGame.unregisterOpcode(opcode)
|
|
|
|
|
end
|
|
|
|
|
|
2024-07-04 15:15:46 -03:00
|
|
|
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()
|
2024-07-04 15:15:46 -03:00
|
|
|
end
|
2023-05-12 21:20:34 -03:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2025-10-06 10:52:59 -03:00
|
|
|
for type, events in pairs(self.scheduledEvents or {}) do
|
2024-07-11 18:08:21 -03:00
|
|
|
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()
|
2026-05-02 19:44:23 -04:00
|
|
|
else
|
|
|
|
|
destroyUIEvents(self)
|
2023-05-12 21:20:34 -03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
self.ui = nil
|
2026-05-02 19:44:23 -04:00
|
|
|
self.uiEvents = nil
|
2023-05-12 21:20:34 -03:00
|
|
|
self.attrs = nil
|
2024-07-11 18:08:21 -03:00
|
|
|
self.events = nil
|
|
|
|
|
self.dataUI = nil
|
2024-12-10 14:51:30 -03:00
|
|
|
self.extendedOpcodes = nil
|
2021-07-19 19:24:59 -03:00
|
|
|
self.opcodes = nil
|
2024-07-11 18:08:21 -03:00
|
|
|
self.keyboardEvents = nil
|
2024-07-03 22:27:40 -03:00
|
|
|
self.keyboardAnchor = nil
|
2024-07-11 18:08:21 -03:00
|
|
|
self.scheduledEvents = nil
|
2026-05-02 19:44:23 -04:00
|
|
|
self.modalHandles = nil
|
2025-09-28 14:46:24 -03:00
|
|
|
self.htmlId = nil
|
2024-07-11 18:08:21 -03:00
|
|
|
|
2023-05-12 21:20:34 -03:00
|
|
|
self.__onGameStart = nil
|
|
|
|
|
self.__onGameEnd = nil
|
2020-07-30 02:10:06 -03:00
|
|
|
end
|
|
|
|
|
|
2024-07-04 15:15:46 -03:00
|
|
|
--[[
|
|
|
|
|
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.
|
|
|
|
|
]]
|
2022-08-17 12:51:56 -03:00
|
|
|
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
|
2020-07-30 02:10:06 -03:00
|
|
|
|
2024-07-04 15:15:46 -03:00
|
|
|
local evt = EventController:new(actor, events)
|
|
|
|
|
table.insert(self.events[self.currentTypeEvent], evt)
|
2021-07-19 19:24:59 -03:00
|
|
|
|
2025-10-04 18:50:59 -03:00
|
|
|
return evt
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function Controller:registerUIEvents(actor, events)
|
|
|
|
|
local evt = EventController:new(actor, events)
|
|
|
|
|
table.insert(self.uiEvents, evt)
|
|
|
|
|
|
2025-09-28 17:23:56 -03:00
|
|
|
-- fix html lazy loading
|
|
|
|
|
if actor and actor.isOnHtml and actor:isOnHtml() then
|
|
|
|
|
evt:connect()
|
|
|
|
|
end
|
|
|
|
|
|
2024-07-04 15:15:46 -03:00
|
|
|
return evt
|
2020-07-30 02:10:06 -03:00
|
|
|
end
|
|
|
|
|
|
2025-10-04 18:50:59 -03:00
|
|
|
function Controller:checkWidgetsDestroyed()
|
|
|
|
|
table.remove_if(self.uiEvents, function(i, e)
|
|
|
|
|
if e:actorIsDestroyed() then
|
|
|
|
|
e:disconnect()
|
|
|
|
|
return true
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
2020-07-30 02:10:06 -03:00
|
|
|
function Controller:registerExtendedOpcode(opcode, fnc)
|
2021-07-19 19:24:59 -03:00
|
|
|
ProtocolGame.registerExtendedOpcode(opcode, fnc)
|
2024-12-10 14:51:30 -03:00
|
|
|
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)
|
2020-07-30 02:10:06 -03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function Controller:sendExtendedOpcode(opcode, ...)
|
2021-07-19 19:24:59 -03:00
|
|
|
local protocol = g_game.getProtocolGame()
|
2022-08-17 12:51:56 -03:00
|
|
|
if protocol then
|
|
|
|
|
protocol:sendExtendedOpcode(opcode, ...)
|
|
|
|
|
end
|
2020-07-30 02:10:06 -03:00
|
|
|
end
|
|
|
|
|
|
2024-07-11 18:34:26 -03:00
|
|
|
local function registerScheduledEvent(controller, fncRef, fnc, delay, name)
|
2024-07-11 18:08:21 -03:00
|
|
|
local currentType = controller.currentTypeEvent
|
2025-10-06 10:52:59 -03:00
|
|
|
controller.scheduledEvents = controller.scheduledEvents or {}
|
2024-07-11 18:08:21 -03:00
|
|
|
if controller.scheduledEvents[currentType] == nil then
|
|
|
|
|
controller.scheduledEvents[currentType] = {}
|
|
|
|
|
end
|
|
|
|
|
|
2024-07-11 18:34:26 -03:00
|
|
|
local _rmvEvent = function()
|
2025-10-06 10:52:59 -03:00
|
|
|
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
|
2024-07-11 18:34:26 -03:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
_rmvEvent()
|
|
|
|
|
|
2024-07-11 18:08:21 -03:00
|
|
|
local evt = nil
|
|
|
|
|
local action = function()
|
2025-10-04 18:50:59 -03:00
|
|
|
local res = fnc()
|
2024-07-11 18:08:21 -03:00
|
|
|
|
|
|
|
|
if fncRef == scheduleEvent then
|
2024-07-11 18:34:26 -03:00
|
|
|
if name then
|
|
|
|
|
_rmvEvent()
|
|
|
|
|
else
|
2025-10-06 10:52:59 -03:00
|
|
|
if controller.scheduledEvents and controller.scheduledEvents[currentType] then
|
|
|
|
|
table.removevalue(controller.scheduledEvents[currentType], evt)
|
|
|
|
|
end
|
2024-07-11 18:34:26 -03:00
|
|
|
end
|
2025-10-04 18:50:59 -03:00
|
|
|
elseif res == false then
|
|
|
|
|
removeEvent(evt)
|
2024-07-11 18:08:21 -03:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
evt = fncRef(action, delay)
|
2024-07-11 18:34:26 -03:00
|
|
|
|
|
|
|
|
if name then
|
2025-10-06 10:52:59 -03:00
|
|
|
if controller.scheduledEvents then
|
|
|
|
|
controller.scheduledEvents[currentType][name] = evt
|
|
|
|
|
end
|
2024-07-11 18:34:26 -03:00
|
|
|
else
|
2025-10-06 10:52:59 -03:00
|
|
|
if controller.scheduledEvents and controller.scheduledEvents[currentType] then
|
|
|
|
|
table.insert(controller.scheduledEvents[currentType], evt)
|
|
|
|
|
end
|
2024-07-11 18:34:26 -03:00
|
|
|
end
|
2024-07-11 18:08:21 -03:00
|
|
|
|
|
|
|
|
return evt
|
|
|
|
|
end
|
|
|
|
|
|
2024-07-11 18:34:26 -03:00
|
|
|
function Controller:scheduleEvent(fnc, delay, name)
|
|
|
|
|
return registerScheduledEvent(self, scheduleEvent, fnc, delay, name)
|
2024-07-11 18:08:21 -03:00
|
|
|
end
|
|
|
|
|
|
2024-07-11 18:34:26 -03:00
|
|
|
function Controller:cycleEvent(fnc, delay, name)
|
|
|
|
|
return registerScheduledEvent(self, cycleEvent, fnc, delay, name)
|
2024-07-11 18:08:21 -03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
function Controller:removeEvent(evt)
|
2024-07-11 18:34:26 -03:00
|
|
|
if self.scheduledEvents[TypeEvent.GAME_INIT] then
|
|
|
|
|
if table.removevalue(self.scheduledEvents[TypeEvent.GAME_INIT], evt) then
|
|
|
|
|
removeEvent(evt)
|
|
|
|
|
return
|
2024-07-11 18:08:21 -03:00
|
|
|
end
|
2024-07-11 18:34:26 -03:00
|
|
|
end
|
2024-07-11 18:08:21 -03:00
|
|
|
|
2024-07-11 18:34:26 -03:00
|
|
|
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
|
2024-07-11 18:08:21 -03:00
|
|
|
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, {
|
2024-06-21 18:10:07 -03:00
|
|
|
name = 'KeyDown',
|
2024-08-29 18:07:23 -03:00
|
|
|
args = args,
|
|
|
|
|
controllerEventType = self.currentTypeEvent
|
2022-04-29 23:14:24 -03:00
|
|
|
})
|
2025-01-18 06:31:57 -03:00
|
|
|
g_keyboard.bindKeyDown(args[1], args[2], args[3], args[4])
|
2024-06-21 18:10:07 -03:00
|
|
|
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
|
2024-06-21 18:10:07 -03:00
|
|
|
table.insert(self.keyboardEvents, {
|
|
|
|
|
name = 'KeyUp',
|
2024-08-29 18:07:23 -03:00
|
|
|
args = args,
|
|
|
|
|
controllerEventType = self.currentTypeEvent
|
2024-06-21 18:10:07 -03:00
|
|
|
})
|
2024-07-03 22:27:40 -03:00
|
|
|
|
2025-01-18 06:31:57 -03:00
|
|
|
g_keyboard.bindKeyUp(args[1], args[2], args[3], args[4])
|
2020-07-30 02:10:06 -03:00
|
|
|
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])
|
2020-07-30 02:10:06 -03:00
|
|
|
end
|