otclient-redemption/modules/corelib/ui/uimessagebox.lua
Eduardo Dantas ef446a9d0c
feat(client): add automatic modern asset installation (#1727)
Add automatic client-asset installation for modern Tibia versions in OTC, with the UI and runtime support needed for a safe download-and-install flow.

Main changes:
- Add modules/client_assets to orchestrate descriptor and release resolution, downloads, retries, cancellation, archive extraction, optional LZMA handling, and SHA-256 integrity checks.
- Add richer install logs in the console, including download phases, install phases, and final install paths.
- Add support for packaged runtime files, including large DLLs distributed through archives.
- Make client_entergame depend on client_assets.
- Check required assets during login for modern clients version 1281 and newer.
- Prompt the user to download missing modern assets instead of requiring manual setup.
- Allow the client list and version selection to work with installed modern asset sets.

Install layout:
- Install things assets under data/things/<version>.
- Install sound assets under data/sounds/<version>.
- Keep extra runtime files in the existing client root/bin flow.
- Preserve the existing OTC asset layout as the source of truth instead of introducing a separate permanent path.

UI improvements:
- Improve missing-assets and progress dialog sizing and text wrapping.
- Improve progress behavior for downloads without Content-Length.
- Add indeterminate progress behavior for unknown-length phases.
- Add an hourglass busy indicator for indeterminate phases.
- Reduce the perception that the installer is stuck during long extraction steps.

Runtime and build support:
- Add libarchive usage and linking for archive extraction.
- Add SHA-256 helpers and Lua bindings used by client_assets.
- Add safer extraction and path normalization in ResourceManager.
- Adjust the Emscripten HTTP login path to follow the same HTTP/HTTPS fallback behavior used by native login.

Security and integrity:
- Enable strictManifestSha256 by default.
- Disable raw fallback on hash mismatch by default.
- Key releasesCache per source URL and repository to avoid stale cross-source cache reuse.
- Add an LZMA decompression output cap to reduce memory-exhaustion risk.

Compatibility:
- Keep existing classic asset behavior unchanged.
- Use the new auto-install flow only for modern versions when required assets are missing.
- Keep the scope limited to client-side asset automation and required runtime support.
- No server-side Canary behavior is changed by this PR.

This makes modern client setup safer and more automatic by downloading, validating, extracting, and installing missing assets directly from OTC while preserving the existing classic asset flow.
2026-05-28 16:18:44 -03:00

230 lines
7.2 KiB
Lua

if not UIMiniWindow then
dofile 'uiminiwindow'
end
-- @docclass
UIMessageBox = extends(UIMiniWindow, 'UIMessageBox')
-- messagebox cannot be created from otui files
function UIMessageBox.create(title, okCallback, cancelCallback)
local calendar = UIMessageBox.internalCreate()
return calendar
end
function UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback)
local rootWidth = rootWidget and rootWidget:getWidth() or 956
local rootHeight = rootWidget and rootWidget:getHeight() or 656
local staticSizes = {
width = {
max = math.min(916, math.max(260, rootWidth - 40)),
min = 246
},
height = {
min = 56,
max = math.min(616, math.max(120, rootHeight - 40))
}
}
local horizontalPadding = 32
local maxContentWidth = staticSizes.width.max - horizontalPadding
local currentSizes = {
width = 0,
height = 0
}
local messageBox = g_ui.createWidget('MessageBoxWindow', rootWidget)
messageBox.title = messageBox:getChildById('title')
messageBox.title:setText(title)
messageBox.content = messageBox:getChildById('content')
messageBox.content:setTextWrap(true)
messageBox.content:setWidth(maxContentWidth)
messageBox.content:setText(message)
messageBox.content:resizeToText()
currentSizes.width = currentSizes.width + math.min(maxContentWidth, math.max(messageBox.content:getWidth(), messageBox.content:getTextSize().width)) + horizontalPadding
currentSizes.height = currentSizes.height + messageBox.content:getHeight() + 20
messageBox.holder = messageBox:getChildById('holder')
currentSizes.height = currentSizes.height + 22
for i = 1, #buttons do
local button = messageBox:addButton(buttons[i].text, buttons[i].callback)
button:addAnchor(AnchorTop, 'parent', AnchorTop)
if i == 1 then
button:addAnchor(AnchorRight, 'parent', AnchorRight)
currentSizes.height = currentSizes.height + button:getHeight() + 22
else
button:addAnchor(AnchorRight, 'prev', AnchorLeft)
button:setMarginRight(10)
end
end
local finalWidth = math.min(staticSizes.width.max, math.max(staticSizes.width.min, currentSizes.width))
messageBox:setWidth(finalWidth)
messageBox.content:setWidth(finalWidth - horizontalPadding)
messageBox.content:resizeToText()
currentSizes.height = messageBox.content:getHeight() + 20 + 22
if #buttons > 0 then
currentSizes.height = currentSizes.height + 42
end
messageBox:setHeight(math.min(staticSizes.height.max, math.max(staticSizes.height.min, currentSizes.height)))
if onEnterCallback then
connect(messageBox, {
onEnter = onEnterCallback
})
end
if onEscapeCallback then
connect(messageBox, {
onEscape = onEscapeCallback
})
end
return messageBox
end
function alert(msg)
displayInfoBox("Alert", msg)
end
function displayInfoBox(title, message)
local messageBox
local defaultCallback = function()
messageBox:ok()
end
messageBox = UIMessageBox.display(title, message, { {
text = 'Ok',
callback = defaultCallback
} }, defaultCallback, defaultCallback)
return messageBox
end
function displayErrorBox(title, message)
local messageBox
local defaultCallback = function()
messageBox:ok()
end
messageBox = UIMessageBox.display(title, message, { {
text = 'Ok',
callback = defaultCallback
} }, defaultCallback, defaultCallback)
return messageBox
end
function displayCancelBox(title, message)
local messageBox
local defaultCallback = function()
messageBox:cancel()
end
messageBox = UIMessageBox.display(title, message, { {
text = 'Cancel',
callback = defaultCallback
} }, defaultCallback, defaultCallback)
return messageBox
end
function displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback)
return UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback)
end
function displayGeneralSHOPBox(title, message, description, buttons, onEnterCallback, onEscapeCallback)
return UIMessageBox.displaySHOP(title, message, description, buttons, onEnterCallback, onEscapeCallback)
end
function UIMessageBox:addButton(text, callback)
local holder = self:getChildById('holder')
local button = g_ui.createWidget('QtButton', holder)
button:setWidth(math.max(48, 10 + (string.len(text) * 8)))
button:setHeight(20)
button:setText(text)
connect(button, {
onClick = callback
})
return button
end
function UIMessageBox:ok()
signalcall(self.onOk, self)
self.onOk = nil
self:destroy()
end
function UIMessageBox:cancel()
signalcall(self.onCancel, self)
self.onCancel = nil
self:destroy()
end
function UIMessageBox.displaySHOP(title, message, description, buttons, onEnterCallback, onEscapeCallback)
local staticSizes = {
width = {
max = 390,
min = 390
},
height = {
min = 200,
max = 200
}
}
local currentSizes = {
width = 0,
height = 0
}
local messageBox = g_ui.createWidget('MessageBoxShopWindow', rootWidget)
messageBox.title = messageBox:getChildById('title')
messageBox.title:setText(title)
messageBox.title:setTextAutoResize(true)
messageBox.content = messageBox:getChildById('content')
messageBox.content:setText(message)
messageBox.content:setTextAutoResize(true)
messageBox.content:setTextWrap(true)
messageBox.content:setTextAlign(AlignCenter)
messageBox.additionalLabel:setText(description)
messageBox.additionalLabel:setTextWrap(true)
messageBox.additionalLabel:setTextAlign(AlignCenter)
local contentWidth = messageBox.content:getTextSize().width + messageBox.content:getPaddingLeft() +
messageBox.content:getPaddingRight()
local contentHeight = messageBox.content:getHeight() + messageBox.content:getPaddingTop() +
messageBox.content:getPaddingBottom()
currentSizes.width = contentWidth + 32
currentSizes.height = contentHeight + 20
messageBox.holder = messageBox:getChildById('holder')
currentSizes.height = currentSizes.height + 22
for i = 1, #buttons do
local button = messageBox:addButton(buttons[i].text, buttons[i].callback)
button:addAnchor(AnchorTop, 'parent', AnchorTop)
if i == 1 then
button:addAnchor(AnchorRight, 'parent', AnchorRight)
currentSizes.height = currentSizes.height + button:getHeight() + 22
button:setImageSource('/images/options/blue_large')
button:setImageClip("0 0 108 20")
else
button:addAnchor(AnchorRight, 'prev', AnchorLeft)
button:setMarginRight(10)
end
end
messageBox:setWidth(currentSizes.width)
messageBox:setHeight(math.min(staticSizes.height.max, math.max(staticSizes.height.min, currentSizes.height)))
if onEnterCallback then
connect(messageBox, {
onEnter = onEnterCallback
})
end
if onEscapeCallback then
connect(messageBox, {
onEscape = onEscapeCallback
})
end
return messageBox
end