otclient-redemption/modules/game_things/things.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

110 lines
3.4 KiB
Lua

ThingsLoaderController = Controller:new()
local filename = nil
local loaded = false
function setFileName(name)
filename = name
end
function isLoaded()
return loaded
end
local function tryLoadDatWithFallbacks(datPath)
if g_things.loadDat(datPath) then
return true
end
local featureFlags = {
GameSpritesU32,
GameEnhancedAnimations,
GameIdleAnimations
}
local combinations = {
{ 1 }, { 2 }, { 3 },
{ 1, 2 }, { 1, 3 }, { 2, 3 },
{ 1, 2, 3 }
}
for _, combo in ipairs(combinations) do
for _, idx in ipairs(combo) do
g_game.enableFeature(featureFlags[idx])
end
if g_things.loadDat(datPath) then
return true
end
end
return false
end
local function load(version)
local errorList = {}
if version >= 1281 and not g_game.getFeature(GameLoadSprInsteadProtobuf) then
local filePath = resolvepath(string.format('/data/things/%d/', version))
if not g_things.loadAppearances(filePath) then
errorList[#errorList + 1] = "Couldn't load assets"
end
if not g_things.loadStaticData(filePath) then
errorList[#errorList + 1] = "Couldn't load staticdata"
end
if g_game.getFeature(GameProficiency) and not g_things.resolveProficienciesFile(filePath) then
g_logger.warning(string.format(
"[game_things.load()] Couldn't load /data/things/%d/proficiencies-<hash>.json", version))
end
else
local datPath, sprPath
if filename then
datPath = resolvepath('/data/things/' .. filename)
sprPath = resolvepath('/data/things/' .. filename)
else
datPath = resolvepath('/data/things/' .. version .. '/Tibia')
sprPath = resolvepath('/data/things/' .. version .. '/Tibia')
end
g_logger.setLevel(5)
if not tryLoadDatWithFallbacks(datPath) then
errorList[#errorList + 1] = tr('Unable to load dat file, please place a valid dat in \'%s.dat\'', datPath)
end
g_logger.setLevel(1)
if not g_sprites.loadSpr(sprPath) then
errorList[#errorList + 1] = tr('Unable to load spr file, please place a valid spr in \'%s.spr\'', sprPath)
end
if g_game.getFeature(GameLoadSprInsteadProtobuf) and version >= 1281 then
local staticPath = resolvepath(string.format('/data/things/%d/appearances', version))
if not g_things.loadAppearances(staticPath) then
g_logger.warning(string.format(
"[game_things.load()] Couldn't load /data/things/%d/appearances.dat, possible packets error.", version))
end
end
end
loaded = #errorList == 0
if loaded then
-- loading client files was successful, try to load sounds now
-- sound files are optional, this means that failing to load them
-- will not block logging into game
g_sounds.loadClientFiles(resolvepath(string.format('/data/sounds/%d/', version)))
return
end
local messageBox = displayErrorBox(tr('Error'), table.concat(errorList, "\n"))
addEvent(function()
messageBox:raise()
messageBox:focus()
end)
g_game.setClientVersion(0)
g_game.setProtocolVersion(0)
end
function ThingsLoaderController:onInit()
self:registerEvents(g_game, {
onClientVersionChange = load
})
end