mirror of
https://github.com/OTCv8/otclientv8.git
synced 2025-10-19 14:13:27 +02:00
Version 0.95 BETA
This commit is contained in:
373
modules/client_entergame/characterlist.lua
Normal file
373
modules/client_entergame/characterlist.lua
Normal file
@@ -0,0 +1,373 @@
|
||||
CharacterList = { }
|
||||
|
||||
-- private variables
|
||||
local charactersWindow
|
||||
local loadBox
|
||||
local characterList
|
||||
local errorBox
|
||||
local waitingWindow
|
||||
local updateWaitEvent
|
||||
local resendWaitEvent
|
||||
local loginEvent
|
||||
|
||||
-- private functions
|
||||
local function tryLogin(charInfo, tries)
|
||||
tries = tries or 1
|
||||
|
||||
if tries > 50 then
|
||||
return
|
||||
end
|
||||
|
||||
if g_game.isOnline() then
|
||||
if tries == 1 then
|
||||
g_game.safeLogout()
|
||||
end
|
||||
loginEvent = scheduleEvent(function() tryLogin(charInfo, tries+1) end, 100)
|
||||
return
|
||||
end
|
||||
|
||||
CharacterList.hide()
|
||||
|
||||
-- proxies for not http login users
|
||||
if charInfo.worldHost == "0.0.0.0" and g_proxy then
|
||||
g_proxy.clear()
|
||||
-- g_proxy.addProxy(localPort, proxyHost, proxyPort, proxyPriority)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.158.184.57", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "54.39.190.20", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.83.226.109", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "35.247.201.100", 443, 0)
|
||||
end
|
||||
|
||||
g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey)
|
||||
g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort)
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...'))
|
||||
connect(loadBox, { onCancel = function()
|
||||
loadBox = nil
|
||||
g_game.cancelLogin()
|
||||
CharacterList.show()
|
||||
end })
|
||||
|
||||
-- save last used character
|
||||
g_settings.set('last-used-character', charInfo.characterName)
|
||||
g_settings.set('last-used-world', charInfo.worldName)
|
||||
end
|
||||
|
||||
local function updateWait(timeStart, timeEnd)
|
||||
if waitingWindow then
|
||||
local time = g_clock.seconds()
|
||||
if time <= timeEnd then
|
||||
local percent = ((time - timeStart) / (timeEnd - timeStart)) * 100
|
||||
local timeStr = string.format("%.0f", timeEnd - time)
|
||||
|
||||
local progressBar = waitingWindow:getChildById('progressBar')
|
||||
progressBar:setPercent(percent)
|
||||
|
||||
local label = waitingWindow:getChildById('timeLabel')
|
||||
label:setText(tr('Trying to reconnect in %s seconds.', timeStr))
|
||||
|
||||
updateWaitEvent = scheduleEvent(function() updateWait(timeStart, timeEnd) end, 1000 * progressBar:getPercentPixels() / 100 * (timeEnd - timeStart))
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
updateWaitEvent:cancel()
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function resendWait()
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
|
||||
if updateWaitEvent then
|
||||
updateWaitEvent:cancel()
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if charactersWindow then
|
||||
local selected = characterList:getFocusedChild()
|
||||
if selected then
|
||||
local charInfo = { worldHost = selected.worldHost,
|
||||
worldPort = selected.worldPort,
|
||||
worldName = selected.worldName,
|
||||
characterName = selected.characterName }
|
||||
tryLogin(charInfo)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function onLoginWait(message, time)
|
||||
CharacterList.destroyLoadBox()
|
||||
|
||||
waitingWindow = g_ui.displayUI('waitinglist')
|
||||
|
||||
local label = waitingWindow:getChildById('infoLabel')
|
||||
label:setText(message)
|
||||
|
||||
updateWaitEvent = scheduleEvent(function() updateWait(g_clock.seconds(), g_clock.seconds() + time) end, 0)
|
||||
resendWaitEvent = scheduleEvent(resendWait, time * 1000)
|
||||
end
|
||||
|
||||
function onGameLoginError(message)
|
||||
CharacterList.destroyLoadBox()
|
||||
errorBox = displayErrorBox(tr("Login Error"), message)
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameLoginToken(unknown)
|
||||
CharacterList.destroyLoadBox()
|
||||
-- TODO: make it possible to enter a new token here / prompt token
|
||||
errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.')
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameConnectionError(message, code)
|
||||
CharacterList.destroyLoadBox()
|
||||
local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message)
|
||||
errorBox = displayErrorBox(tr("Connection Error"), text)
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameUpdateNeeded(signature)
|
||||
CharacterList.destroyLoadBox()
|
||||
errorBox = displayErrorBox(tr("Update needed"), tr('Enter with your account again to update your client.'))
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function CharacterList.init()
|
||||
connect(g_game, { onLoginError = onGameLoginError })
|
||||
connect(g_game, { onLoginToken = onGameLoginToken })
|
||||
connect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
|
||||
connect(g_game, { onConnectionError = onGameConnectionError })
|
||||
connect(g_game, { onGameStart = CharacterList.destroyLoadBox })
|
||||
connect(g_game, { onLoginWait = onLoginWait })
|
||||
connect(g_game, { onGameEnd = CharacterList.showAgain })
|
||||
|
||||
if G.characters then
|
||||
CharacterList.create(G.characters, G.characterAccount)
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.terminate()
|
||||
disconnect(g_game, { onLoginError = onGameLoginError })
|
||||
disconnect(g_game, { onLoginToken = onGameLoginToken })
|
||||
disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
|
||||
disconnect(g_game, { onConnectionError = onGameConnectionError })
|
||||
disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox })
|
||||
disconnect(g_game, { onLoginWait = onLoginWait })
|
||||
disconnect(g_game, { onGameEnd = CharacterList.showAgain })
|
||||
|
||||
if charactersWindow then
|
||||
characterList = nil
|
||||
charactersWindow:destroy()
|
||||
charactersWindow = nil
|
||||
end
|
||||
|
||||
if loadBox then
|
||||
g_game.cancelLogin()
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
removeEvent(updateWaitEvent)
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if resendWaitEvent then
|
||||
removeEvent(resendWaitEvent)
|
||||
resendWaitEvent = nil
|
||||
end
|
||||
|
||||
if loginEvent then
|
||||
removeEvent(loginEvent)
|
||||
loginEvent = nil
|
||||
end
|
||||
|
||||
CharacterList = nil
|
||||
end
|
||||
|
||||
function CharacterList.create(characters, account, otui)
|
||||
if not otui then otui = 'characterlist' end
|
||||
|
||||
if charactersWindow then
|
||||
charactersWindow:destroy()
|
||||
end
|
||||
|
||||
charactersWindow = g_ui.displayUI(otui)
|
||||
characterList = charactersWindow:getChildById('characters')
|
||||
|
||||
-- characters
|
||||
G.characters = characters
|
||||
G.characterAccount = account
|
||||
|
||||
characterList:destroyChildren()
|
||||
local accountStatusLabel = charactersWindow:getChildById('accountStatusLabel')
|
||||
|
||||
local focusLabel
|
||||
for i,characterInfo in ipairs(characters) do
|
||||
local widget = g_ui.createWidget('CharacterWidget', characterList)
|
||||
for key,value in pairs(characterInfo) do
|
||||
local subWidget = widget:getChildById(key)
|
||||
if subWidget then
|
||||
if key == 'outfit' then -- it's an exception
|
||||
subWidget:setOutfit(value)
|
||||
else
|
||||
local text = value
|
||||
if subWidget.baseText and subWidget.baseTranslate then
|
||||
text = tr(subWidget.baseText, text)
|
||||
elseif subWidget.baseText then
|
||||
text = string.format(subWidget.baseText, text)
|
||||
end
|
||||
subWidget:setText(text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- these are used by login
|
||||
widget.characterName = characterInfo.name
|
||||
widget.worldName = characterInfo.worldName
|
||||
widget.worldHost = characterInfo.worldIp
|
||||
widget.worldPort = characterInfo.worldPort
|
||||
|
||||
connect(widget, { onDoubleClick = function () CharacterList.doLogin() return true end } )
|
||||
|
||||
if i == 1 or (g_settings.get('last-used-character') == widget.characterName and g_settings.get('last-used-world') == widget.worldName) then
|
||||
focusLabel = widget
|
||||
end
|
||||
end
|
||||
|
||||
if focusLabel then
|
||||
characterList:focusChild(focusLabel, KeyboardFocusReason)
|
||||
addEvent(function() characterList:ensureChildVisible(focusLabel) end)
|
||||
end
|
||||
|
||||
-- account
|
||||
local status = ''
|
||||
if account.status == AccountStatus.Frozen then
|
||||
status = tr(' (Frozen)')
|
||||
elseif account.status == AccountStatus.Suspended then
|
||||
status = tr(' (Suspended)')
|
||||
end
|
||||
|
||||
if account.subStatus == SubscriptionStatus.Free then
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status))
|
||||
elseif account.subStatus == SubscriptionStatus.Premium then
|
||||
if account.premDays == 0 or account.premDays == 65535 then
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Gratis Premium Account'), status))
|
||||
else
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Premium Account (%s) days left', account.premDays), status))
|
||||
end
|
||||
end
|
||||
|
||||
if account.premDays > 0 and account.premDays <= 7 then
|
||||
accountStatusLabel:setOn(true)
|
||||
else
|
||||
accountStatusLabel:setOn(false)
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.destroy()
|
||||
CharacterList.hide(true)
|
||||
|
||||
if charactersWindow then
|
||||
characterList = nil
|
||||
charactersWindow:destroy()
|
||||
charactersWindow = nil
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.show()
|
||||
if loadBox or errorBox or not charactersWindow then return end
|
||||
charactersWindow:show()
|
||||
charactersWindow:raise()
|
||||
charactersWindow:focus()
|
||||
end
|
||||
|
||||
function CharacterList.hide(showLogin)
|
||||
showLogin = showLogin or false
|
||||
charactersWindow:hide()
|
||||
|
||||
if showLogin and EnterGame and not g_game.isOnline() then
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.showAgain()
|
||||
if characterList and characterList:hasChildren() then
|
||||
CharacterList.show()
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.isVisible()
|
||||
if charactersWindow and charactersWindow:isVisible() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function CharacterList.doLogin()
|
||||
local selected = characterList:getFocusedChild()
|
||||
if selected then
|
||||
local charInfo = { worldHost = selected.worldHost,
|
||||
worldPort = selected.worldPort,
|
||||
worldName = selected.worldName,
|
||||
characterName = selected.characterName }
|
||||
charactersWindow:hide()
|
||||
if loginEvent then
|
||||
removeEvent(loginEvent)
|
||||
loginEvent = nil
|
||||
end
|
||||
tryLogin(charInfo)
|
||||
else
|
||||
displayErrorBox(tr('Error'), tr('You must select a character to login!'))
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.destroyLoadBox()
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.cancelWait()
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
removeEvent(updateWaitEvent)
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if resendWaitEvent then
|
||||
removeEvent(resendWaitEvent)
|
||||
resendWaitEvent = nil
|
||||
end
|
||||
|
||||
CharacterList.destroyLoadBox()
|
||||
CharacterList.showAgain()
|
||||
end
|
134
modules/client_entergame/characterlist.otui
Normal file
134
modules/client_entergame/characterlist.otui
Normal file
@@ -0,0 +1,134 @@
|
||||
CharacterWidget < UIWidget
|
||||
height: 14
|
||||
background-color: alpha
|
||||
&updateOnStates: |
|
||||
function(self)
|
||||
local children = self:getChildren()
|
||||
for i=1,#children do
|
||||
children[i]:setOn(self:isFocused())
|
||||
end
|
||||
end
|
||||
@onFocusChange: self:updateOnStates()
|
||||
@onSetup: self:updateOnStates()
|
||||
|
||||
$focus:
|
||||
background-color: #ffffff22
|
||||
|
||||
Label
|
||||
id: name
|
||||
color: #bbbbbb
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
font: verdana-11px-monochrome
|
||||
text-auto-resize: true
|
||||
background-color: alpha
|
||||
text-offset: 2 0
|
||||
|
||||
$on:
|
||||
color: #ffffff
|
||||
|
||||
Label
|
||||
id: worldName
|
||||
color: #bbbbbb
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
margin-right: 5
|
||||
font: verdana-11px-monochrome
|
||||
text-auto-resize: true
|
||||
background-color: alpha
|
||||
&baseText: '(%s)'
|
||||
|
||||
$on:
|
||||
color: #ffffff
|
||||
|
||||
StaticMainWindow
|
||||
id: charactersWindow
|
||||
!text: tr('Character List')
|
||||
visible: false
|
||||
@onEnter: CharacterList.doLogin()
|
||||
@onEscape: CharacterList.hide(true)
|
||||
@onSetup: |
|
||||
g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self)
|
||||
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
|
||||
if g_game.getFeature(GamePreviewState) then
|
||||
self:setSize({width = 350, height = 400})
|
||||
else
|
||||
self:setSize({width = 250, height = 248})
|
||||
end
|
||||
|
||||
TextList
|
||||
id: characters
|
||||
background-color: #565656
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: characterListScrollBar.left
|
||||
anchors.bottom: accountStatusCaption.top
|
||||
margin-bottom: 5
|
||||
padding: 1
|
||||
focusable: false
|
||||
vertical-scrollbar: characterListScrollBar
|
||||
auto-focus: first
|
||||
|
||||
VerticalScrollBar
|
||||
id: characterListScrollBar
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: accountStatusCaption.top
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 5
|
||||
step: 14
|
||||
pixels-scroll: true
|
||||
|
||||
Label
|
||||
id: accountStatusCaption
|
||||
!text: tr('Account Status') .. ':'
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 1
|
||||
|
||||
Label
|
||||
id: accountStatusLabel
|
||||
!text: tr('Free Account')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: separator.top
|
||||
margin-bottom: 5
|
||||
text-auto-resize: true
|
||||
|
||||
$on:
|
||||
color: #FF0000
|
||||
|
||||
HorizontalSeparator
|
||||
id: separator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
|
||||
//CheckBox
|
||||
// id: charAutoLoginBox
|
||||
// !text: tr('Auto login')
|
||||
// !tooltip: tr('Auto login selected character on next charlist load')
|
||||
// anchors.left: parent.left
|
||||
// anchors.right: parent.right
|
||||
// anchors.bottom: next.top
|
||||
// margin-bottom: 6
|
||||
// margin-left: 18
|
||||
// margin-right: 18
|
||||
|
||||
Button
|
||||
id: buttonOk
|
||||
!text: tr('Ok')
|
||||
width: 64
|
||||
anchors.right: next.left
|
||||
anchors.bottom: parent.bottom
|
||||
margin-right: 10
|
||||
@onClick: CharacterList.doLogin()
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: CharacterList.hide(true)
|
495
modules/client_entergame/entergame.lua
Normal file
495
modules/client_entergame/entergame.lua
Normal file
@@ -0,0 +1,495 @@
|
||||
EnterGame = { }
|
||||
|
||||
-- private variables
|
||||
local loadBox
|
||||
local enterGame
|
||||
local enterGameButton
|
||||
local clientBox
|
||||
local protocolLogin
|
||||
local server = nil
|
||||
local versionsFound = false
|
||||
|
||||
local newLogin = nil
|
||||
local newLoginUrl = nil
|
||||
local newLoginEvent
|
||||
|
||||
local customServerSelectorPanel
|
||||
local serverSelectorPanel
|
||||
local serverSelector
|
||||
local clientVersionSelector
|
||||
local serverHostTextEdit
|
||||
local rememberPasswordBox
|
||||
local protos = {"740", "760", "772", "800", "810", "854", "860", "1090", "1096", "1099"}
|
||||
|
||||
|
||||
-- private functions
|
||||
local function onProtocolError(protocol, message, errorCode)
|
||||
if errorCode then
|
||||
return EnterGame.onError(message)
|
||||
end
|
||||
return EnterGame.onLoginError(message)
|
||||
end
|
||||
|
||||
local function onSessionKey(protocol, sessionKey)
|
||||
G.sessionKey = sessionKey
|
||||
end
|
||||
|
||||
local function onCharacterList(protocol, characters, account, otui)
|
||||
if rememberPasswordBox:isChecked() then
|
||||
local account = g_crypt.encrypt(G.account)
|
||||
local password = g_crypt.encrypt(G.password)
|
||||
|
||||
g_settings.set('account', account)
|
||||
g_settings.set('password', password)
|
||||
else
|
||||
EnterGame.clearAccountFields()
|
||||
end
|
||||
|
||||
for _, characterInfo in pairs(characters) do
|
||||
if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then
|
||||
characterInfo.worldName = characterInfo.worldName .. ', Preview'
|
||||
end
|
||||
end
|
||||
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
|
||||
CharacterList.create(characters, account, otui)
|
||||
CharacterList.show()
|
||||
|
||||
g_settings.save()
|
||||
end
|
||||
|
||||
local function onUpdateNeeded(protocol, signature)
|
||||
return EnterGame.onError(tr('Your client needs updating, try redownloading it.'))
|
||||
end
|
||||
|
||||
local function parseFeatures(features)
|
||||
for feature_id, value in pairs(features) do
|
||||
if value == "1" or value == "true" or value == true then
|
||||
g_game.enableFeature(feature_id)
|
||||
else
|
||||
g_game.disableFeature(feature_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function validateThings(things)
|
||||
local incorrectThings = ""
|
||||
if things ~= nil then
|
||||
local thingsNode = {}
|
||||
for thingtype, thingdata in pairs(things) do
|
||||
thingsNode[thingtype] = thingdata[1]
|
||||
if not g_resources.fileExists("/data/things/" .. thingdata[1]) then
|
||||
correctThings = false
|
||||
incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n"
|
||||
end
|
||||
local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower()
|
||||
if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then
|
||||
if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version
|
||||
incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
g_settings.setNode("things", thingsNode)
|
||||
else
|
||||
g_settings.setNode("things", {})
|
||||
end
|
||||
return incorrectThings
|
||||
end
|
||||
|
||||
local function onHTTPResult(data, err)
|
||||
if err then
|
||||
return EnterGame.onError(err)
|
||||
end
|
||||
if data['error'] and #data['error'] > 0 then
|
||||
return EnterGame.onLoginError(data['error'])
|
||||
end
|
||||
|
||||
local characters = data["characters"]
|
||||
local account = data["account"]
|
||||
local session = data["session"]
|
||||
|
||||
local version = data["version"]
|
||||
local things = data["things"]
|
||||
local customProtocol = data["customProtocol"]
|
||||
|
||||
local features = data["features"]
|
||||
local settings = data["settings"]
|
||||
local rsa = data["rsa"]
|
||||
local proxies = data["proxies"]
|
||||
|
||||
local incorrectThings = validateThings(things)
|
||||
if #incorrectThings > 0 then
|
||||
g_logger.info(incorrectThings)
|
||||
if Updater then
|
||||
return Updater.updateThings(things, incorrectThings)
|
||||
else
|
||||
return EnterGame.onError(incorrectThings)
|
||||
end
|
||||
end
|
||||
|
||||
-- custom protocol
|
||||
g_game.setCustomProtocolVersion(0)
|
||||
if customProtocol ~= nil then
|
||||
customProtocol = tonumber(customProtocol)
|
||||
if customProtocol ~= nil and customProtocol > 0 then
|
||||
g_game.setCustomProtocolVersion(customProtocol)
|
||||
end
|
||||
end
|
||||
|
||||
-- force player settings
|
||||
if settings ~= nil then
|
||||
for option, value in pairs(settings) do
|
||||
modules.client_options.setOption(option, value, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- version
|
||||
G.clientVersion = version
|
||||
g_game.setClientVersion(version)
|
||||
g_game.setProtocolVersion(g_game.getClientProtocolVersion(version))
|
||||
g_game.setCustomOs(-1) -- disable
|
||||
|
||||
if rsa ~= nil then
|
||||
g_game.setRsa(rsa)
|
||||
end
|
||||
|
||||
if features ~= nil then
|
||||
parseFeatures(features)
|
||||
end
|
||||
|
||||
if session ~= nil and session:len() > 0 then
|
||||
onSessionKey(nil, session)
|
||||
end
|
||||
|
||||
-- proxies
|
||||
if g_proxy then
|
||||
g_proxy.clear()
|
||||
if proxies then
|
||||
for i, proxy in ipairs(proxies) do
|
||||
g_proxy.addProxy(tonumber(proxy["localPort"]), proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
onCharacterList(nil, characters, account, nil)
|
||||
end
|
||||
|
||||
|
||||
-- public functions
|
||||
function EnterGame.init()
|
||||
enterGame = g_ui.displayUI('entergame')
|
||||
newLogin = g_ui.displayUI('entergame_new')
|
||||
|
||||
serverSelectorPanel = enterGame:getChildById('serverSelectorPanel')
|
||||
customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel')
|
||||
|
||||
serverSelector = serverSelectorPanel:getChildById('serverSelector')
|
||||
rememberPasswordBox = enterGame:getChildById('rememberPasswordBox')
|
||||
serverHostTextEdit = customServerSelectorPanel:getChildById('serverHostTextEdit')
|
||||
clientVersionSelector = customServerSelectorPanel:getChildById('clientVersionSelector')
|
||||
|
||||
if Servers ~= nil then
|
||||
for name,server in pairs(Servers) do
|
||||
serverSelector:addOption(name)
|
||||
end
|
||||
end
|
||||
if serverSelector:getOptionsCount() == 0 or ALLOW_CUSTOM_SERVERS then
|
||||
serverSelector:addOption(tr("Another"))
|
||||
end
|
||||
for i,proto in pairs(protos) do
|
||||
clientVersionSelector:addOption(proto)
|
||||
end
|
||||
|
||||
if serverSelector:getOptionsCount() == 1 then
|
||||
enterGame:setHeight(enterGame:getHeight() - serverSelectorPanel:getHeight())
|
||||
serverSelectorPanel:setOn(false)
|
||||
end
|
||||
|
||||
local account = g_crypt.decrypt(g_settings.get('account'))
|
||||
local password = g_crypt.decrypt(g_settings.get('password'))
|
||||
local server = g_settings.get('server')
|
||||
local host = g_settings.get('host')
|
||||
local clientVersion = g_settings.get('client-version')
|
||||
local hdSprites = g_settings.getBoolean('hdSprites', false)
|
||||
|
||||
if serverSelector:isOption(server) then
|
||||
serverSelector:setCurrentOption(server, false)
|
||||
if Servers == nil or Servers[server] == nil then
|
||||
serverHostTextEdit:setText(host)
|
||||
end
|
||||
clientVersionSelector:setOption(clientVersion)
|
||||
else
|
||||
server = ""
|
||||
host = ""
|
||||
end
|
||||
|
||||
enterGame:getChildById('accountPasswordTextEdit'):setText(password)
|
||||
enterGame:getChildById('accountNameTextEdit'):setText(account)
|
||||
rememberPasswordBox:setChecked(#account > 0)
|
||||
|
||||
if enterGame.hdSprites then
|
||||
enterGame.hdSprites:setChecked(hdSprites)
|
||||
end
|
||||
|
||||
g_keyboard.bindKeyDown('Ctrl+G', EnterGame.openWindow)
|
||||
|
||||
if g_game.isOnline() then
|
||||
return EnterGame.hide()
|
||||
end
|
||||
|
||||
scheduleEvent(function()
|
||||
EnterGame.show()
|
||||
end, 100)
|
||||
end
|
||||
|
||||
function EnterGame.terminate()
|
||||
g_keyboard.unbindKeyDown('Ctrl+G')
|
||||
|
||||
removeEvent(newLoginEvent)
|
||||
|
||||
enterGame:destroy()
|
||||
if newLogin then
|
||||
newLogin:destroy()
|
||||
end
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
if protocolLogin then
|
||||
protocolLogin:cancelLogin()
|
||||
protocolLogin = nil
|
||||
end
|
||||
EnterGame = nil
|
||||
end
|
||||
|
||||
function EnterGame.show()
|
||||
if Updater and Updater.isVisible() or g_game.isOnline() then
|
||||
return EnterGame.hide()
|
||||
end
|
||||
enterGame:show()
|
||||
enterGame:raise()
|
||||
enterGame:focus()
|
||||
enterGame:getChildById('accountNameTextEdit'):focus()
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
|
||||
function EnterGame.hide()
|
||||
enterGame:hide()
|
||||
newLogin:hide()
|
||||
end
|
||||
|
||||
function EnterGame.openWindow()
|
||||
if g_game.isOnline() then
|
||||
CharacterList.show()
|
||||
elseif not g_game.isLogging() and not CharacterList.isVisible() then
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.clearAccountFields()
|
||||
enterGame:getChildById('accountNameTextEdit'):clearText()
|
||||
enterGame:getChildById('accountPasswordTextEdit'):clearText()
|
||||
--enterGame:getChildById('authenticatorTokenTextEdit'):clearText()
|
||||
enterGame:getChildById('accountNameTextEdit'):focus()
|
||||
g_settings.remove('account')
|
||||
g_settings.remove('password')
|
||||
end
|
||||
|
||||
function EnterGame.hideNewLogin()
|
||||
newLogin:hide()
|
||||
newLoginUrl = nil
|
||||
end
|
||||
|
||||
function EnterGame.checkNewLoginEvent()
|
||||
newLoginEvent = scheduleEvent(function() EnterGame.checkNewLoginEvent() end, 1000)
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
|
||||
function EnterGame.checkNewLogin()
|
||||
if not newLoginUrl then
|
||||
return
|
||||
end
|
||||
local url = newLoginUrl
|
||||
HTTP.postJSON(newLoginUrl, { quick = 1 }, function(data, err)
|
||||
if url ~= newLoginUrl then return end
|
||||
if err then return end
|
||||
if not data["qrcode"] then return end
|
||||
if newLogin:isHidden() then
|
||||
newLogin:show()
|
||||
enterGame:raise()
|
||||
end
|
||||
newLogin.qrcode:setImageSourceBase64(data["qrcode"])
|
||||
newLogin.code:setText(data["code"])
|
||||
end)
|
||||
end
|
||||
|
||||
function EnterGame.onServerChange()
|
||||
server = serverSelector:getText()
|
||||
EnterGame.hideNewLogin()
|
||||
if server == tr("Another") then
|
||||
if not customServerSelectorPanel:isOn() then
|
||||
serverHostTextEdit:setText("")
|
||||
customServerSelectorPanel:setOn(true)
|
||||
enterGame:setHeight(enterGame:getHeight() + customServerSelectorPanel:getHeight())
|
||||
end
|
||||
elseif customServerSelectorPanel:isOn() then
|
||||
enterGame:setHeight(enterGame:getHeight() - customServerSelectorPanel:getHeight())
|
||||
customServerSelectorPanel:setOn(false)
|
||||
end
|
||||
if Servers and Servers[server] ~= nil then
|
||||
serverHostTextEdit:setText(Servers[server])
|
||||
newLoginUrl = Servers[server]
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.doLogin()
|
||||
if Updater and Updater.isVisible() then
|
||||
return
|
||||
end
|
||||
if g_game.isOnline() then
|
||||
local errorBox = displayErrorBox(tr('Login Error'), tr('Cannot login while already in game.'))
|
||||
connect(errorBox, { onOk = EnterGame.show })
|
||||
return
|
||||
end
|
||||
|
||||
G.account = enterGame:getChildById('accountNameTextEdit'):getText()
|
||||
G.password = enterGame:getChildById('accountPasswordTextEdit'):getText()
|
||||
--G.authenticatorToken = enterGame:getChildById('authenticatorTokenTextEdit'):getText()
|
||||
G.authenticatorToken = ""
|
||||
G.hdSprites = enterGame.hdSprites and enterGame.hdSprites:isChecked()
|
||||
G.stayLogged = true
|
||||
G.server = serverSelector:getText():trim()
|
||||
G.host = serverHostTextEdit:getText()
|
||||
G.clientVersion = tonumber(clientVersionSelector:getText())
|
||||
|
||||
if not rememberPasswordBox:isChecked() then
|
||||
g_settings.set('account', G.account)
|
||||
g_settings.set('password', G.password)
|
||||
end
|
||||
g_settings.set('host', G.host)
|
||||
g_settings.set('server', G.server)
|
||||
g_settings.set('client-version', G.clientVersion)
|
||||
g_settings.set('hdSprites', G.hdSprites)
|
||||
g_settings.save()
|
||||
|
||||
if G.host:find("http") ~= nil then
|
||||
return EnterGame.doLoginHttp()
|
||||
end
|
||||
|
||||
local server_params = G.host:split(":")
|
||||
if #server_params < 2 then
|
||||
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
|
||||
end
|
||||
local server_ip = server_params[1]
|
||||
local server_port = tonumber(server_params[2])
|
||||
if #server_params >= 3 then
|
||||
G.clientVersion = tonumber(server_params[3])
|
||||
end
|
||||
if not server_port or not G.clientVersion then
|
||||
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
|
||||
end
|
||||
|
||||
local things = {
|
||||
data = {G.clientVersion .. "/Tibia.dat", ""},
|
||||
sprites = {G.clientVersion .. "/Tibia.spr", ""},
|
||||
}
|
||||
|
||||
if G.hdSprites then
|
||||
things.sprites_hd = {G.clientVersion .. "/Tibia_hd.spr", ""}
|
||||
end
|
||||
|
||||
local incorrectThings = validateThings(things)
|
||||
if #incorrectThings > 0 then
|
||||
g_logger.info(incorrectThings)
|
||||
if Updater then
|
||||
return Updater.updateThings(things, incorrectThings)
|
||||
else
|
||||
return EnterGame.onError(incorrectThings)
|
||||
end
|
||||
end
|
||||
|
||||
protocolLogin = ProtocolLogin.create()
|
||||
protocolLogin.onLoginError = onProtocolError
|
||||
protocolLogin.onSessionKey = onSessionKey
|
||||
protocolLogin.onCharacterList = onCharacterList
|
||||
protocolLogin.onUpdateNeeded = onUpdateNeeded
|
||||
|
||||
EnterGame.hide()
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
|
||||
connect(loadBox, { onCancel = function(msgbox)
|
||||
loadBox = nil
|
||||
protocolLogin:cancelLogin()
|
||||
EnterGame.show()
|
||||
end })
|
||||
|
||||
-- if you have custom rsa or protocol edit it here
|
||||
g_game.setClientVersion(G.clientVersion)
|
||||
g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion))
|
||||
g_game.setCustomProtocolVersion(0)
|
||||
g_game.chooseRsa(G.host)
|
||||
g_game.setCustomOs(2) -- windows
|
||||
|
||||
-- you can add custom features here
|
||||
g_game.enableFeature(GameBot)
|
||||
|
||||
-- proxies
|
||||
if g_proxy then
|
||||
g_proxy.clear()
|
||||
end
|
||||
|
||||
if modules.game_things.isLoaded() then
|
||||
g_logger.info("Connection to: " .. server_ip .. ":" .. server_port)
|
||||
protocolLogin:login(server_ip, server_port, G.account, G.password, G.authenticatorToken, G.stayLogged)
|
||||
else
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.doLoginHttp()
|
||||
if G.host == nil or G.host:len() < 10 then
|
||||
return EnterGame.onError("Invalid server url: " .. G.host)
|
||||
end
|
||||
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
|
||||
connect(loadBox, { onCancel = function(msgbox)
|
||||
loadBox = nil
|
||||
EnterGame.show()
|
||||
end })
|
||||
|
||||
local data = {
|
||||
account = G.account,
|
||||
password = G.password,
|
||||
token = G.authenticatorToken,
|
||||
hdSprites = G.hdSprites,
|
||||
version = APP_VERSION,
|
||||
uid = G.UUID
|
||||
}
|
||||
HTTP.postJSON(G.host, data, onHTTPResult)
|
||||
EnterGame.hide()
|
||||
end
|
||||
|
||||
function EnterGame.onError(err)
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
local errorBox = displayErrorBox(tr('Login Error'), err)
|
||||
errorBox.onOk = EnterGame.show
|
||||
end
|
||||
|
||||
function EnterGame.onLoginError(err)
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
local errorBox = displayErrorBox(tr('Login Error'), err)
|
||||
errorBox.onOk = EnterGame.show
|
||||
EnterGame.clearAccountFields()
|
||||
end
|
9
modules/client_entergame/entergame.otmod
Normal file
9
modules/client_entergame/entergame.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_entergame
|
||||
description: Manages enter game and character list windows
|
||||
author: edubart & otclient.ovh
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ entergame, characterlist ]
|
||||
@onLoad: EnterGame.init() CharacterList.init()
|
||||
@onUnload: EnterGame.terminate() CharacterList.terminate()
|
||||
|
176
modules/client_entergame/entergame.otui
Normal file
176
modules/client_entergame/entergame.otui
Normal file
@@ -0,0 +1,176 @@
|
||||
EnterGameWindow < StaticMainWindow
|
||||
!text: tr('Enter Game')
|
||||
size: 240 310
|
||||
|
||||
EnterGameWindow
|
||||
id: enterGame
|
||||
@onEnter: EnterGame.doLogin()
|
||||
|
||||
MenuLabel
|
||||
!text: tr('Account name')
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text-auto-resize: true
|
||||
|
||||
TextEdit
|
||||
id: accountNameTextEdit
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 2
|
||||
|
||||
MenuLabel
|
||||
!text: tr('Password')
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 8
|
||||
text-auto-resize: true
|
||||
|
||||
PasswordTextEdit
|
||||
id: accountPasswordTextEdit
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 2
|
||||
|
||||
Panel
|
||||
id: serverSelectorPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
height: 52
|
||||
on: true
|
||||
focusable: false
|
||||
|
||||
$on:
|
||||
visible: true
|
||||
margin-top: 0
|
||||
|
||||
$!on:
|
||||
visible: false
|
||||
margin-top: -52
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
margin-top: 10
|
||||
|
||||
MenuLabel
|
||||
id: serverLabel
|
||||
!text: tr('Server')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
text-auto-resize: true
|
||||
margin-top: 5
|
||||
|
||||
ComboBox
|
||||
id: serverSelector
|
||||
anchors.left: prev.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: serverLabel.bottom
|
||||
margin-top: 2
|
||||
margin-right: 3
|
||||
menu-scroll: true
|
||||
menu-height: 125
|
||||
menu-scroll-step: 25
|
||||
text-offset: 5 2
|
||||
@onOptionChange: EnterGame.onServerChange()
|
||||
|
||||
Panel
|
||||
id: customServerSelectorPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
height: 52
|
||||
on: true
|
||||
focusable: true
|
||||
|
||||
$on:
|
||||
visible: true
|
||||
margin-top: 0
|
||||
|
||||
$!on:
|
||||
visible: false
|
||||
margin-top: -52
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
margin-top: 8
|
||||
|
||||
MenuLabel
|
||||
id: serverLabel
|
||||
!text: tr('IP:PORT or url')
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 8
|
||||
text-auto-resize: true
|
||||
|
||||
TextEdit
|
||||
id: serverHostTextEdit
|
||||
!tooltip: tr('Make sure that your client uses\nthe correct game client version')
|
||||
anchors.left: parent.left
|
||||
anchors.top: serverLabel.bottom
|
||||
margin-top: 2
|
||||
width: 130
|
||||
|
||||
MenuLabel
|
||||
id: clientLabel
|
||||
!text: tr('Version')
|
||||
anchors.left: serverHostTextEdit.right
|
||||
anchors.top: serverLabel.top
|
||||
text-auto-resize: true
|
||||
margin-left: 10
|
||||
|
||||
ComboBox
|
||||
id: clientVersionSelector
|
||||
anchors.top: serverHostTextEdit.top
|
||||
anchors.bottom: serverHostTextEdit.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: parent.right
|
||||
menu-scroll: true
|
||||
menu-height: 125
|
||||
menu-scroll-step: 25
|
||||
margin-right: 3
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 10
|
||||
|
||||
CheckBox
|
||||
id: rememberPasswordBox
|
||||
!text: tr('Remember password')
|
||||
!tooltip: tr('Remember account and password when starts client')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 9
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 9
|
||||
|
||||
Button
|
||||
!text: tr('Login')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 10
|
||||
margin-left: 50
|
||||
margin-right: 50
|
||||
@onClick: EnterGame.doLogin()
|
||||
|
||||
Label
|
||||
id: serverInfoLabel
|
||||
font: verdana-11px-rounded
|
||||
anchors.top: prev.top
|
||||
anchors.left: parent.left
|
||||
margin-top: 5
|
||||
color: green
|
||||
text-auto-resize: true
|
48
modules/client_entergame/entergame_new.otui
Normal file
48
modules/client_entergame/entergame_new.otui
Normal file
@@ -0,0 +1,48 @@
|
||||
StaticWindow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
margin-right: 20
|
||||
id: newLoginPanel
|
||||
width: 230
|
||||
height: 330
|
||||
!text: tr('Quick Login & Registration')
|
||||
|
||||
Label
|
||||
id: qrcode
|
||||
width: 200
|
||||
height: 180
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
margin-top: 5
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
text-auto-resize: true
|
||||
!text: tr("Scan QR code or process\nbellow code to register or login")
|
||||
height: 40
|
||||
margin-top: 10
|
||||
margin-bottom: 5
|
||||
|
||||
Label
|
||||
id: code
|
||||
height: 20
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
font: sans-bold-16px
|
||||
margin-top: 10
|
||||
text: XXXXXX
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
!text: tr("Click to get Android/iOS app")
|
||||
height: 20
|
||||
margin-top: 10
|
||||
color: #FFFFFF
|
44
modules/client_entergame/waitinglist.otui
Normal file
44
modules/client_entergame/waitinglist.otui
Normal file
@@ -0,0 +1,44 @@
|
||||
MainWindow
|
||||
id: waitingWindow
|
||||
!text: tr('Waiting List')
|
||||
size: 260 180
|
||||
@onEscape: CharacterList.cancelWait()
|
||||
|
||||
Label
|
||||
id: infoLabel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: progressBar.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text-wrap: true
|
||||
|
||||
ProgressBar
|
||||
id: progressBar
|
||||
height: 15
|
||||
background-color: #4444ff
|
||||
anchors.bottom: timeLabel.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 10
|
||||
|
||||
Label
|
||||
id: timeLabel
|
||||
anchors.bottom: separator.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 10
|
||||
|
||||
HorizontalSeparator
|
||||
id: separator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: CharacterList.cancelWait()
|
Reference in New Issue
Block a user