fix items, map, protocls

This commit is contained in:
ErikasKontenis
2022-04-09 13:45:27 +03:00
parent 18bd56496e
commit 396464b940
933 changed files with 215171 additions and 1 deletions

View File

@@ -0,0 +1,125 @@
local musicFilename = "/sounds/startup"
local musicChannel = nil
function setMusic(filename)
musicFilename = filename
if not g_game.isOnline() and musicChannel ~= nil then
musicChannel:stop()
musicChannel:enqueue(musicFilename, 3)
end
end
function reloadScripts()
if g_game.getFeature(GameNoDebug) then
return
end
g_textures.clearCache()
g_modules.reloadModules()
local script = '/' .. g_app.getCompactName() .. 'rc.lua'
if g_resources.fileExists(script) then
dofile(script)
end
local message = tr('All modules and scripts were reloaded.')
modules.game_textmessage.displayGameMessage(message)
print(message)
end
function startup()
if g_sounds ~= nil then
musicChannel = g_sounds.getChannel(1)
end
G.UUID = g_settings.getString('report-uuid')
if not G.UUID or #G.UUID ~= 36 then
G.UUID = g_crypt.genUUID()
g_settings.set('report-uuid', G.UUID)
end
-- Play startup music (The Silver Tree, by Mattias Westlund)
--musicChannel:enqueue(musicFilename, 3)
connect(g_game, { onGameStart = function() if musicChannel ~= nil then musicChannel:stop(3) end end })
connect(g_game, { onGameEnd = function()
if g_sounds ~= nil then
g_sounds.stopAll()
--musicChannel:enqueue(musicFilename, 3)
end
end })
end
function init()
connect(g_app, { onRun = startup,
onExit = exit })
connect(g_game, { onGameStart = onGameStart,
onGameEnd = onGameEnd })
if g_sounds ~= nil then
--g_sounds.preload(musicFilename)
end
if not Updater then
if g_resources.getLayout() == "mobile" then
g_window.setMinimumSize({ width = 640, height = 360 })
else
g_window.setMinimumSize({ width = 800, height = 640 })
end
-- window size
local size = { width = 1024, height = 600 }
size = g_settings.getSize('window-size', size)
g_window.resize(size)
-- window position, default is the screen center
local displaySize = g_window.getDisplaySize()
local defaultPos = { x = (displaySize.width - size.width)/2,
y = (displaySize.height - size.height)/2 }
local pos = g_settings.getPoint('window-pos', defaultPos)
pos.x = math.max(pos.x, 0)
pos.y = math.max(pos.y, 0)
g_window.move(pos)
-- window maximized?
local maximized = g_settings.getBoolean('window-maximized', false)
if maximized then g_window.maximize() end
end
g_window.setTitle(g_app.getName())
g_window.setIcon('/images/clienticon')
g_keyboard.bindKeyDown('Ctrl+Shift+R', reloadScripts)
-- generate machine uuid, this is a security measure for storing passwords
if not g_crypt.setMachineUUID(g_settings.get('uuid')) then
g_settings.set('uuid', g_crypt.getMachineUUID())
g_settings.save()
end
end
function terminate()
disconnect(g_app, { onRun = startup,
onExit = exit })
disconnect(g_game, { onGameStart = onGameStart,
onGameEnd = onGameEnd })
-- save window configs
g_settings.set('window-size', g_window.getUnmaximizedSize())
g_settings.set('window-pos', g_window.getUnmaximizedPos())
g_settings.set('window-maximized', g_window.isMaximized())
end
function exit()
g_logger.info("Exiting application..")
end
function onGameStart()
local player = g_game.getLocalPlayer()
if not player then return end
g_window.setTitle(g_app.getName() .. " - " .. player:getName())
end
function onGameEnd()
g_window.setTitle(g_app.getName())
end

View File

@@ -0,0 +1,23 @@
Module
name: client
description: Initialize the client and setups its main window
author: edubart
website: https://github.com/edubart/otclient
reloadable: false
sandboxed: true
scripts: [ client ]
@onLoad: init()
@onUnload: terminate()
load-later:
- client_styles
- client_locales
- client_topmenu
- client_background
- client_textedit
- client_options
- client_entergame
- client_terminal
- client_stats
- client_feedback
- client_mobile

View File

@@ -0,0 +1,49 @@
-- private variables
local background
local clientVersionLabel
-- public functions
function init()
background = g_ui.displayUI('background')
background:lower()
clientVersionLabel = background:getChildById('clientVersionLabel')
clientVersionLabel:setText('OTClientV8 ' .. g_app.getVersion() .. '\nrev ' .. g_app.getBuildRevision() .. '\nMade by:\n' .. g_app.getAuthor() .. "")
if not g_game.isOnline() then
addEvent(function() g_effects.fadeIn(clientVersionLabel, 1500) end)
end
connect(g_game, { onGameStart = hide })
connect(g_game, { onGameEnd = show })
end
function terminate()
disconnect(g_game, { onGameStart = hide })
disconnect(g_game, { onGameEnd = show })
g_effects.cancelFade(background:getChildById('clientVersionLabel'))
background:destroy()
Background = nil
end
function hide()
background:hide()
end
function show()
background:show()
end
function hideVersionLabel()
background:getChildById('clientVersionLabel'):hide()
end
function setVersionText(text)
clientVersionLabel:setText(text)
end
function getBackground()
return background
end

View File

@@ -0,0 +1,9 @@
Module
name: client_background
description: Handles the background of the login screen
author: edubart
website: https://github.com/edubart/otclient
sandboxed: true
scripts: [ background ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,21 @@
UIWidget
id: background
anchors.fill: parent
focusable: false
image-source: /images/background
image-smooth: true
image-fixed-ratio: true
margin-top: 1
UILabel
id: clientVersionLabel
background-color: #00000099
anchors.right: parent.right
anchors.bottom: parent.bottom
text-align: center
text-auto-resize: false
width: 220
height: 90
padding: 2
color: #ffffff
font: terminus-14px-bold

View File

@@ -0,0 +1,422 @@
CharacterList = { }
-- private variables
local charactersWindow
local loadBox
local characterList
local errorBox
local waitingWindow
local autoReconnectButton
local updateWaitEvent
local resendWaitEvent
local loginEvent
local autoReconnectEvent
local lastLogout = 0
-- 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()
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
scheduleAutoReconnect()
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()
if (not g_game.isOnline() or code ~= 2) and not errorBox then -- code 2 is normal disconnect, end of file
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
scheduleAutoReconnect()
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
function onGameEnd()
scheduleAutoReconnect()
CharacterList.showAgain()
end
function onLogout()
lastLogout = g_clock.millis()
end
function scheduleAutoReconnect()
if lastLogout + 2000 > g_clock.millis() then
return
end
if autoReconnectEvent then
removeEvent(autoReconnectEvent)
end
autoReconnectEvent = scheduleEvent(executeAutoReconnect, 2500)
end
function executeAutoReconnect()
if not autoReconnectButton or not autoReconnectButton:isOn() or g_game.isOnline() then
return
end
if errorBox then
errorBox:destroy()
errorBox = nil
end
CharacterList.doLogin()
end
-- public functions
function CharacterList.init()
if USE_NEW_ENERGAME then return end
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 = onGameEnd })
connect(g_game, { onLogout = onLogout })
if G.characters then
CharacterList.create(G.characters, G.characterAccount)
end
end
function CharacterList.terminate()
if USE_NEW_ENERGAME then return end
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 = onGameEnd })
disconnect(g_game, { onLogout = onLogout })
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')
autoReconnectButton = charactersWindow:getChildById('autoReconnect')
-- 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
characterList.onChildFocusChange = function()
removeEvent(autoReconnectEvent)
autoReconnectEvent = nil
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 and account.premDays < 1 then
accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status))
else
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
autoReconnectButton.onClick = function(widget)
local autoReconnect = not g_settings.getBoolean('autoReconnect', true)
autoReconnectButton:setOn(autoReconnect)
g_settings.set('autoReconnect', autoReconnect)
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()
local autoReconnect = g_settings.getBoolean('autoReconnect', true)
autoReconnectButton:setOn(autoReconnect)
end
function CharacterList.hide(showLogin)
removeEvent(autoReconnectEvent)
autoReconnectEvent = nil
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()
removeEvent(autoReconnectEvent)
autoReconnectEvent = nil
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

View File

@@ -0,0 +1,133 @@
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
size: 350 400
$mobile:
size: 350 280
@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)
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.bottom: separator.top
margin-bottom: 5
Label
id: accountStatusLabel
!text: tr('Free Account')
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
Button
id: autoReconnect
width: 140
anchors.left: parent.left
anchors.bottom: parent.bottom
$!on:
image-color: red
!text: tr('Auto reconnect: Off')
$on:
!text: tr('Auto reconnect: On')
image-color: green
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)

View File

@@ -0,0 +1,628 @@
EnterGame = { }
-- private variables
local loadBox
local enterGame
local enterGameButton
local logpass
local clientBox
local protocolLogin
local server = nil
local versionsFound = false
local customServerSelectorPanel
local serverSelectorPanel
local serverSelector
local clientVersionSelector
local serverHostTextEdit
local rememberPasswordBox
local protos = {"740", "760", "772", "792", "800", "810", "854", "860", "870", "910", "961", "1000", "1077", "1090", "1096", "1098", "1099", "1100", "1200", "1220"}
local checkedByUpdater = {}
local waitingForHttpResults = 0
-- 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 onProxyList(protocol, proxies)
for _, proxy in ipairs(proxies) do
g_proxy.addProxy(proxy["host"], proxy["port"], proxy["priority"])
end
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 = ""
local missingFiles = false
local versionForMissingFiles = 0
if things ~= nil then
local thingsNode = {}
for thingtype, thingdata in pairs(things) do
thingsNode[thingtype] = thingdata[1]
if not g_resources.fileExists("/things/" .. thingdata[1]) then
incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n"
missingFiles = true
versionForMissingFiles = thingdata[1]:split("/")[1]
else
local localChecksum = g_resources.fileChecksum("/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
end
g_settings.setNode("things", thingsNode)
else
g_settings.setNode("things", {})
end
if missingFiles then
incorrectThings = incorrectThings .. "\nYou should open data/things and create directory " .. versionForMissingFiles ..
".\nIn this directory (data/things/" .. versionForMissingFiles .. ") you should put missing\nfiles (Tibia.dat and Tibia.spr/Tibia.cwm) " ..
"from correct Tibia version."
end
return incorrectThings
end
local function onTibia12HTTPResult(session, playdata)
local characters = {}
local worlds = {}
local account = {
status = 0,
subStatus = 0,
premDays = 0
}
if session["status"] ~= "active" then
account.status = 1
end
if session["ispremium"] then
account.subStatus = 1 -- premium
end
if session["premiumuntil"] > g_clock.seconds() then
account.subStatus = math.floor((session["premiumuntil"] - g_clock.seconds()) / 86400)
end
local things = {
data = {G.clientVersion .. "/Tibia.dat", ""},
sprites = {G.clientVersion .. "/Tibia.cwm", ""},
}
local incorrectThings = validateThings(things)
if #incorrectThings > 0 then
things = {
data = {G.clientVersion .. "/Tibia.dat", ""},
sprites = {G.clientVersion .. "/Tibia.spr", ""},
}
incorrectThings = validateThings(things)
end
if #incorrectThings > 0 then
g_logger.error(incorrectThings)
if Updater and not checkedByUpdater[G.clientVersion] then
checkedByUpdater[G.clientVersion] = true
return Updater.check({
version = G.clientVersion,
host = G.host
})
else
return EnterGame.onError(incorrectThings)
end
end
onSessionKey(nil, session["sessionkey"])
for _, world in pairs(playdata["worlds"]) do
worlds[world.id] = {
name = world.name,
port = world.externalportunprotected or world.externalportprotected or world.externaladdress,
address = world.externaladdressunprotected or world.externaladdressprotected or world.externalport
}
end
for _, character in pairs(playdata["characters"]) do
local world = worlds[character.worldid]
if world then
table.insert(characters, {
name = character.name,
worldName = world.name,
worldIp = world.address,
worldPort = world.port
})
end
end
-- proxies
if g_proxy then
g_proxy.clear()
if playdata["proxies"] then
for i, proxy in ipairs(playdata["proxies"]) do
g_proxy.addProxy(proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"]))
end
end
end
g_game.setCustomProtocolVersion(0)
g_game.chooseRsa(G.host)
g_game.setClientVersion(G.clientVersion)
g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion))
g_game.setCustomOs(-1) -- disable
if not g_game.getFeature(GameExtendedOpcode) then
g_game.setCustomOs(5) -- set os to windows if opcodes are disabled
end
onCharacterList(nil, characters, account, nil)
end
local function onHTTPResult(data, err)
if waitingForHttpResults == 0 then
return
end
waitingForHttpResults = waitingForHttpResults - 1
if err and waitingForHttpResults > 0 then
return -- ignore, wait for other requests
end
if err then
return EnterGame.onError(err)
end
waitingForHttpResults = 0
if data['error'] and data['error']:len() > 0 then
return EnterGame.onLoginError(data['error'])
elseif data['errorMessage'] and data['errorMessage']:len() > 0 then
return EnterGame.onLoginError(data['errorMessage'])
end
if type(data["session"]) == "table" and type(data["playdata"]) == "table" then
return onTibia12HTTPResult(data["session"], data["playdata"])
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)
return EnterGame.onError(incorrectThings)
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(proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"]))
end
end
end
onCharacterList(nil, characters, account, nil)
end
-- public functions
function EnterGame.init()
if USE_NEW_ENERGAME then return end
enterGame = g_ui.displayUI('entergame')
if LOGPASS ~= nil then
logpass = g_ui.loadUI('logpass', enterGame:getParent())
end
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')
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)
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()
if not enterGame then return end
g_keyboard.unbindKeyDown('Ctrl+G')
if logpass then
logpass:destroy()
logpass = nil
end
enterGame:destroy()
if loadBox then
loadBox:destroy()
loadBox = nil
end
if protocolLogin then
protocolLogin:cancelLogin()
protocolLogin = nil
end
EnterGame = nil
end
function EnterGame.show()
if not enterGame then return end
enterGame:show()
enterGame:raise()
enterGame:focus()
enterGame:getChildById('accountNameTextEdit'):focus()
if logpass then
logpass:show()
logpass:raise()
logpass:focus()
end
end
function EnterGame.hide()
if not enterGame then return end
enterGame:hide()
if logpass then
logpass:hide()
if modules.logpass then
modules.logpass:hide()
end
end
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('accountTokenTextEdit'):clearText()
enterGame:getChildById('accountNameTextEdit'):focus()
g_settings.remove('account')
g_settings.remove('password')
end
function EnterGame.onServerChange()
server = serverSelector:getText()
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
if type(Servers[server]) == "table" then
serverHostTextEdit:setText(Servers[server][1])
else
serverHostTextEdit:setText(Servers[server])
end
end
end
function EnterGame.doLogin(account, password, token, host)
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 = account or enterGame:getChildById('accountNameTextEdit'):getText()
G.password = password or enterGame:getChildById('accountPasswordTextEdit'):getText()
G.authenticatorToken = token or enterGame:getChildById('accountTokenTextEdit'):getText()
G.stayLogged = true
G.server = serverSelector:getText():trim()
G.host = host or 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.save()
local server_params = G.host:split(":")
if G.host:lower():find("http") ~= nil then
if #server_params >= 4 then
G.host = server_params[1] .. ":" .. server_params[2] .. ":" .. server_params[3]
G.clientVersion = tonumber(server_params[4])
elseif #server_params >= 3 then
if tostring(tonumber(server_params[3])) == server_params[3] then
G.host = server_params[1] .. ":" .. server_params[2]
G.clientVersion = tonumber(server_params[3])
end
end
return EnterGame.doLoginHttp()
end
local server_ip = server_params[1]
local server_port = 7171
if #server_params >= 2 then
server_port = tonumber(server_params[2])
end
if #server_params >= 3 then
G.clientVersion = tonumber(server_params[3])
end
if type(server_ip) ~= 'string' or server_ip:len() <= 3 or 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.cwm", ""},
}
local incorrectThings = validateThings(things)
if #incorrectThings > 0 then
things = {
data = {G.clientVersion .. "/Tibia.dat", ""},
sprites = {G.clientVersion .. "/Tibia.spr", ""},
}
incorrectThings = validateThings(things)
end
if #incorrectThings > 0 then
g_logger.error(incorrectThings)
if Updater and not checkedByUpdater[G.clientVersion] then
checkedByUpdater[G.clientVersion] = true
return Updater.check({
version = G.clientVersion,
host = G.host
})
else
return EnterGame.onError(incorrectThings)
end
end
protocolLogin = ProtocolLogin.create()
protocolLogin.onLoginError = onProtocolError
protocolLogin.onSessionKey = onSessionKey
protocolLogin.onCharacterList = onCharacterList
protocolLogin.onUpdateNeeded = onUpdateNeeded
protocolLogin.onProxyList = onProxyList
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 G.clientVersion == 1000 then -- some people don't understand that tibia 10 uses 1100 protocol
G.clientVersion = 1100
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.setCustomOs(-1) -- disable
g_game.chooseRsa(G.host)
if #server_params <= 3 and not g_game.getFeature(GameExtendedOpcode) then
g_game.setCustomOs(2) -- set os to windows if opcodes are disabled
end
-- extra features from init.lua
for i = 4, #server_params do
g_game.enableFeature(tonumber(server_params[i]))
end
-- proxies
if g_proxy then
g_proxy.clear()
end
if modules.game_things.isLoaded() then
g_logger.info("Connecting 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 = {
type = "login",
account = G.account,
accountname = G.account,
email = G.account,
password = G.password,
accountpassword = G.password,
token = G.authenticatorToken,
version = APP_VERSION,
uid = G.UUID,
stayloggedin = true
}
local server = serverSelector:getText()
if Servers and Servers[server] ~= nil then
if type(Servers[server]) == "table" then
local urls = Servers[server]
waitingForHttpResults = #urls
for _, url in ipairs(urls) do
HTTP.postJSON(url, data, onHTTPResult)
end
else
waitingForHttpResults = 1
HTTP.postJSON(G.host, data, onHTTPResult)
end
end
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
if err:lower():find("invalid") or err:lower():find("not correct") or err:lower():find("or password") then
EnterGame.clearAccountFields()
end
end

View File

@@ -0,0 +1,12 @@
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()
load-later:
- game_things
- game_features

View File

@@ -0,0 +1,186 @@
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
MenuLabel
!text: tr('Token')
anchors.left: prev.left
anchors.top: prev.bottom
text-auto-resize: true
margin-top: 8
TextEdit
id: accountTokenTextEdit
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: 150
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

View File

@@ -0,0 +1,9 @@
UIWidget
id: logpass
size: 248 41
anchors.top: enterGame.bottom
anchors.horizontalCenter: enterGame.horizontalCenter
margin-top: 25
image-size: 248 41
image-source: /images/ui/continue_with_logpass.png
@onClick: modules.logpass.show()

View 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()

View File

@@ -0,0 +1,109 @@
local feedbackWindow
local textEdit
local okButton
local cancelButton
local postId = 0
local tries = 0
local replyEvent = nil
function init()
feedbackWindow = g_ui.displayUI('feedback')
feedbackWindow:hide()
textEdit = feedbackWindow:getChildById('text')
okButton = feedbackWindow:getChildById('okButton')
cancelButton = feedbackWindow:getChildById('cancelButton')
okButton.onClick = send
cancelButton.onClick = hide
feedbackWindow.onEscape = hide
end
function terminate()
feedbackWindow:destroy()
removeEvent(replyEvent)
end
function show()
if not Services or not Services.feedback or Services.feedback:len() < 4 then
return
end
feedbackWindow:show()
feedbackWindow:raise()
feedbackWindow:focus()
textEdit:setMaxLength(8192)
textEdit:setText('')
textEdit:setEditable(true)
textEdit:setCursorVisible(true)
feedbackWindow:focusChild(textEdit, KeyboardFocusReason)
tries = 0
end
function hide()
feedbackWindow:hide()
textEdit:setEditable(false)
textEdit:setCursorVisible(false)
end
function send()
local text = textEdit:getText()
if text:len() > 1 then
local localPlayer = g_game.getLocalPlayer()
local playerData = nil
if localPlayer ~= nil then
playerData = {
name = localPlayer:getName(),
position = localPlayer:getPosition()
}
end
local details = {
report_delay = sendInterval,
os = g_app.getOs(),
graphics_vendor = g_graphics.getVendor(),
graphics_renderer = g_graphics.getRenderer(),
graphics_version = g_graphics.getVersion(),
fps = g_app.getFps(),
maxFps = g_app.getMaxFps(),
atlas = g_atlas.getStats(),
classic = tostring(g_settings.getBoolean("classicView")),
fullscreen = tostring(g_window.isFullscreen()),
vsync = tostring(g_settings.getBoolean("vsync")),
window_width = g_window.getWidth(),
window_height = g_window.getHeight(),
player_name = g_game.getCharacterName(),
world_name = g_game.getWorldName(),
otserv_host = G.host,
otserv_protocol = g_game.getProtocolVersion(),
otserv_client = g_game.getClientVersion(),
build_version = g_app.getVersion(),
build_revision = g_app.getBuildRevision(),
build_commit = g_app.getBuildCommit(),
build_date = g_app.getBuildDate(),
display_width = g_window.getDisplayWidth(),
display_height = g_window.getDisplayHeight(),
cpu = g_platform.getCPUName(),
mem = g_platform.getTotalSystemMemory(),
os_name = g_platform.getOSName()
}
local data = json.encode({
text = text,
version = g_app.getVersion(),
host = g_settings.get('host'),
player = playerData,
details = details
})
postId = HTTP.post(Services.feedback, data, function(ret, err)
if err then
tries = tries + 1
if tries < 3 then
replyEvent = scheduleEvent(send, 1000)
end
end
end)
end
hide()
end

View File

@@ -0,0 +1,10 @@
Module
name: client_feedback
description: Allow to send feedback
author: otclientv8
website: otclient.ovh
sandboxed: true
dependencies: [ game_interface ]
scripts: [ feedback ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,48 @@
MainWindow
id: feedbackWindow
size: 400 280
!text: tr("Feedback/Bug report")
Label
id: description
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
text-auto-resize: true
text-align: left
text-wrap: true
!text: tr("Bellow enter your feedback or bug report. Please include as much details as possible. Thank you!")
MultilineTextEdit
id: text
anchors.top: textScroll.top
anchors.left: parent.left
anchors.right: textScroll.left
anchors.bottom: textScroll.bottom
vertical-scrollbar: textScroll
text-wrap: true
VerticalScrollBar
id: textScroll
anchors.top: description.bottom
anchors.bottom: okButton.top
anchors.right: parent.right
margin-top: 10
margin-bottom: 10
step: 16
pixels-scroll: true
Button
id: okButton
!text: tr('Ok')
anchors.bottom: parent.bottom
anchors.right: next.left
margin-right: 10
width: 60
Button
id: cancelButton
!text: tr('Cancel')
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 60

View File

@@ -0,0 +1,177 @@
dofile 'neededtranslations'
-- private variables
local defaultLocaleName = 'en'
local installedLocales
local currentLocale
local missingTranslations = {}
function createWindow()
localesWindow = g_ui.displayUI('locales')
local localesPanel = localesWindow:getChildById('localesPanel')
local layout = localesPanel:getLayout()
local spacing = layout:getCellSpacing()
local size = layout:getCellSize()
local count = 0
for name,locale in pairs(installedLocales) do
local widget = g_ui.createWidget('LocalesButton', localesPanel)
widget:setImageSource('/images/flags/' .. name .. '')
widget:setText(locale.languageName)
widget.onClick = function() selectFirstLocale(name) end
count = count + 1
end
count = math.max(1, math.min(count, 3))
localesPanel:setWidth(size.width*count + spacing*(count-1))
addEvent(function() addEvent(function() localesWindow:raise() localesWindow:focus() end) end)
end
function selectFirstLocale(name)
if localesWindow then
localesWindow:destroy()
localesWindow = nil
end
if setLocale(name) then
g_modules.reloadModules()
end
g_settings.save()
end
-- public functions
function init()
installedLocales = {}
installLocales('/locales')
local userLocaleName = g_settings.get('locale', 'false')
if userLocaleName ~= 'false' and setLocale(userLocaleName) then
pdebug('Using configured locale: ' .. userLocaleName)
else
setLocale(defaultLocaleName)
--connect(g_app, { onRun = createWindow })
end
end
function terminate()
installedLocales = nil
currentLocale = nil
--disconnect(g_app, { onRun = createWindow })
end
function generateNewTranslationTable(localename)
local locale = installedLocales[localename]
for _i,k in pairs(neededTranslations) do
local trans = locale.translation[k]
k = k:gsub('\n','\\n')
k = k:gsub('\t','\\t')
k = k:gsub('\"','\\\"')
if trans then
trans = trans:gsub('\n','\\n')
trans = trans:gsub('\t','\\t')
trans = trans:gsub('\"','\\\"')
end
if not trans then
print(' ["' .. k .. '"]' .. ' = false,')
else
print(' ["' .. k .. '"]' .. ' = "' .. trans .. '",')
end
end
end
function installLocale(locale)
if not locale or not locale.name then
error('Unable to install locale.')
end
if _G.allowedLocales and not _G.allowedLocales[locale.name] then return end
if locale.name ~= defaultLocaleName then
local updatesNamesMissing = {}
for _,k in pairs(neededTranslations) do
if locale.translation[k] == nil then
updatesNamesMissing[#updatesNamesMissing + 1] = k
end
end
if #updatesNamesMissing > 0 then
pdebug('Locale \'' .. locale.name .. '\' is missing ' .. #updatesNamesMissing .. ' translations.')
for _,name in pairs(updatesNamesMissing) do
pdebug('["' .. name ..'"] = \"\",')
end
end
end
local installedLocale = installedLocales[locale.name]
if installedLocale then
for word,translation in pairs(locale.translation) do
installedLocale.translation[word] = translation
end
else
installedLocales[locale.name] = locale
end
end
function installLocales(directory)
dofiles(directory)
end
function setLocale(name)
local locale = installedLocales[name]
if locale == currentLocale then return end
if not locale then
pwarning("Locale " .. name .. ' does not exist.')
return false
end
currentLocale = locale
g_settings.set('locale', name)
if onLocaleChanged then onLocaleChanged(name) end
return true
end
function getInstalledLocales()
return installedLocales
end
function getCurrentLocale()
return currentLocale
end
-- global function used to translate texts
function _G.tr(text, ...)
if currentLocale then
if tonumber(text) and currentLocale.formatNumbers then
local number = tostring(text):split('.')
local out = ''
local reverseNumber = number[1]:reverse()
for i=1,#reverseNumber do
out = out .. reverseNumber:sub(i, i)
if i % 3 == 0 and i ~= #number then
out = out .. currentLocale.thousandsSeperator
end
end
if number[2] then
out = number[2] .. currentLocale.decimalSeperator .. out
end
return out:reverse()
elseif tostring(text) then
local translation = currentLocale.translation[text]
if not translation then
if translation == nil then
if currentLocale.name ~= defaultLocaleName then
if not missingTranslations[text] then
pdebug('Unable to translate: \"' .. text .. '\"')
missingTranslations[text] = true
end
end
end
translation = text
end
return string.format(translation, ...)
end
end
return text
end

View File

@@ -0,0 +1,9 @@
Module
name: client_locales
description: Translates texts to selected language
author: baxnie, edubart
website: https://github.com/edubart/otclient
sandboxed: true
scripts: [ locales ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,35 @@
LocalesMainLabel < Label
font: sans-bold-16px
LocalesButton < UIWidget
size: 96 96
image-size: 96 96
image-smooth: true
text-offset: 0 96
font: verdana-11px-antialised
UIWindow
id: localesWindow
background-color: #000000
opacity: 0.90
clipping: true
anchors.fill: parent
LocalesMainLabel
!text: tr('Select your language')
text-auto-resize: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
margin-top: -100
Panel
id: localesPanel
margin-top: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prev.bottom
anchors.bottom: parent.bottom
layout:
type: grid
cell-size: 96 128
cell-spacing: 32
flow: true

View File

@@ -0,0 +1,364 @@
-- generated by ./tools/gen_needed_translations.sh
neededTranslations = {
"1a) Offensive Name",
"1b) Invalid Name Format",
"1c) Unsuitable Name",
"1d) Name Inciting Rule Violation",
"2a) Offensive Statement",
"2b) Spamming",
"2c) Illegal Advertising",
"2d) Off-Topic Public Statement",
"2e) Non-English Public Statement",
"2f) Inciting Rule Violation",
"3a) Bug Abuse",
"3b) Game Weakness Abuse",
"3c) Using Unofficial Software to Play",
"3d) Hacking",
"3e) Multi-Clienting",
"3f) Account Trading or Sharing",
"4a) Threatening Gamemaster",
"4b) Pretending to Have Influence on Rule Enforcement",
"4c) False Report to Gamemaster",
"Accept",
"Account name",
"Account Status:",
"Action:",
"Add",
"Add new VIP",
"Addon 1",
"Addon 2",
"Addon 3",
"Add to VIP list",
"Adjust volume",
"Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!",
"All",
"All modules and scripts were reloaded.",
"Allow auto chase override",
"Ambient light: %s%%",
"Amount:",
"Amount",
"Anonymous",
"Are you sure you want to logout?",
"Attack",
"Author",
"Autoload",
"Autoload priority",
"Auto login",
"Auto login selected character on next charlist load",
"Axe Fighting",
"Balance:",
"Banishment",
"Banishment + Final Warning",
"Battle",
"Browse",
"Bug report sent.",
"Button Assign",
"Buy",
"Buy Now",
"Buy Offers",
"Buy with backpack",
"Cancel",
"Cannot login while already in game.",
"Cap",
"Capacity",
"Center",
"Channels",
"Character List",
"Classic control",
"Clear current message window",
"Clear Messages",
"Clear object",
"Client needs update.",
"Close",
"Close this channel",
"Club Fighting",
"Combat Controls",
"Comment:",
"Connecting to game server...",
"Connecting to login server...",
"Console",
"Cooldowns",
"Copy message",
"Copy name",
"Copy Name",
"Create Map Mark",
"Create mark",
"Create New Offer",
"Create Offer",
"Current hotkeys:",
"Current hotkey to add: %s",
"Current Offers",
"Default",
"Delete mark",
"Description:",
"Description",
"Destructive Behaviour",
"Detail",
"Details",
"Disable Shared Experience",
"Dismount",
"Display connection speed to the server (milliseconds)",
"Distance Fighting",
"Don\'t stretch/shrink Game Window",
"Edit hotkey text:",
"Edit List",
"Edit Text",
"Enable music",
"Enable Shared Experience",
"Enable smart walking",
"Enable vertical synchronization",
"Enable walk booster",
"Enter Game",
"Enter one name per line.",
"Enter with your account again to update your client.",
"Error",
"Error",
"Excessive Unjustified Player Killing",
"Exclude from private chat",
"Exit",
"Experience",
"Filter list to match your level",
"Filter list to match your vocation",
"Find:",
"Fishing",
"Fist Fighting",
"Follow",
"Force Exit",
"For Your Information",
"Free Account",
"Fullscreen",
"Game",
"Game framerate limit: %s",
"Graphics",
"Graphics card driver not detected",
"Graphics Engine:",
"Head",
"Healing",
"Health Info",
"Health Information",
"Hide monsters",
"Hide non-skull players",
"Hide Npcs",
"Hide Offline",
"Hide party members",
"Hide players",
"Hide spells for higher exp. levels",
"Hide spells for other vocations",
"Hit Points",
"Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks",
"Hotkeys",
"If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character.",
"Ignore",
"Ignore capacity",
"Ignored players:",
"Ignore equipped",
"Ignore List",
"Ignore players",
"Ignore Private Messages",
"Ignore Yelling",
"Interface framerate limit: %s",
"Inventory",
"Invite to Party",
"Invite to private chat",
"IP Address Banishment",
"Item Offers",
"It is empty.",
"Join %s\'s Party",
"Leave Party",
"Level",
"Lifetime Premium Account",
"Limits FPS to 60",
"List of items that you're able to buy",
"List of items that you're able to sell",
"Load",
"Logging out...",
"Login",
"Login Error",
"Login Error",
"Logout",
"Look",
"Magic Level",
"Make sure that your client uses\nthe correct game protocol version",
"Mana",
"Manage hotkeys:",
"Market",
"Market Offers",
"Message of the day",
"Message to ",
"Message to %s",
"Minimap",
"Module Manager",
"Module name",
"Mount",
"Move Stackable Item",
"Move up",
"My Offers",
"Name:",
"Name Report",
"Name Report + Banishment",
"Name Report + Banishment + Final Warning",
"No",
"No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance.",
"No item selected.",
"No Mount",
"No Outfit",
"No statement has been selected.",
"Notation",
"NPC Trade",
"Offer History",
"Offers",
"Offer Type:",
"Offline Training",
"Ok",
"on %s.\n",
"Open",
"Open a private message channel:",
"Open charlist automatically when starting client",
"Open in new window",
"Open new channel",
"Options",
"Overview",
"Pass Leadership to %s",
"Password",
"Piece Price:",
"Please enter a character name:",
"Please, press the key you wish to add onto your hotkeys manager",
"Please Select",
"Please use this dialog to only report bugs. Do not report rule violations here!",
"Please wait",
"Port",
"Position:",
"Position: %i %i %i",
"Premium Account (%s) days left",
"Price:",
"Primary",
"Protocol",
"Quest Log",
"Randomize",
"Randomize characters outfit",
"Reason:",
"Refresh",
"Refresh Offers",
"Regeneration Time",
"Reject",
"Reload All",
"Remember account and password when starts client",
"Remember password",
"Remove",
"Remove %s",
"Report Bug",
"Reserved for more functionality later.",
"Reset Market",
"Revoke %s\'s Invitation",
"Rotate",
"Rule Violation",
"Save",
"Save Messages",
"Search:",
"Search all items",
"Secondary",
"Select object",
"Select Outfit",
"Select your language",
"Sell",
"Sell Now",
"Sell Offers",
"Send",
"Send automatically",
"Send Message",
"Server",
"Server Log",
"Set Outfit",
"Shielding",
"Show all items",
"Show connection ping",
"Show Depot Only",
"Show event messages in console",
"Show frame rate",
"Show info messages in console",
"Show left panel",
"Show levels in console",
"Show Offline",
"Show private messages in console",
"Show private messages on screen",
"Show Server Messages",
"Show status messages in console",
"Show Text",
"Show timestamps in console",
"Show your depot items only",
"Skills",
"Soul",
"Soul Points",
"Special",
"Speed",
"Spell Cooldowns",
"Spell List",
"Stamina",
"Statement:",
"Statement Report",
"Statistics",
"Stop Attack",
"Stop Follow",
"Support",
"%s: (use object)",
"%s: (use object on target)",
"%s: (use object on yourself)",
"%s: (use object with crosshair)",
"Sword Fighting",
"Terminal",
"There is no way.",
"Title",
"Total Price:",
"Trade",
"Trade with ...",
"Trying to reconnect in %s seconds.",
"Unable to load dat file, please place a valid dat in '%s'",
"Unable to load spr file, please place a valid spr in '%s'",
"Unable to logout.",
"Unignore",
"Unload",
"Update needed",
"Use",
"Use on target",
"Use on yourself",
"Use with ...",
"Version",
"VIP List",
"Voc.",
"Vocation",
"Waiting List",
"Website",
"Weight:",
"Will detect when to use diagonal step based on the\nkeys you are pressing",
"With crosshair",
"Yes",
"You are bleeding",
"You are burning",
"You are cursed",
"You are dazzled",
"You are dead.",
"You are dead",
"You are drowning",
"You are drunk",
"You are electrified",
"You are freezing",
"You are hasted",
"You are hungry",
"You are paralysed",
"You are poisoned",
"You are protected by a magic shield",
"You are strengthened",
"You are within a protection zone",
"You can enter new text.",
"You have %s percent",
"You have %s percent to go",
"You may not logout during a fight",
"You may not logout or enter a protection zone",
"You must enter a comment.",
"You must enter a valid server address and port.",
"You must select a character to login!",
"Your Capacity:",
"You read the following, written by \n%s\n",
"You read the following, written on \n%s.\n",
"Your Money:",
}

View File

@@ -0,0 +1,216 @@
local overlay
local keypad
local touchStart = 0
local updateCursorEvent
local zoomInButton
local zoomOutButton
local keypadButton
local keypadEvent
local keypadMousePos = {x=0.5, y=0.5}
local keypadTicks = 0
-- public functions
function init()
if not g_app.isMobile() then return end
overlay = g_ui.displayUI('mobile')
keypad = overlay.keypad
overlay:raise()
zoomInButton = modules.client_topmenu.addLeftButton('zoomInButton', 'Zoom In', '/images/topbuttons/zoomin', function() g_app.scaleUp() end)
zoomOutButton = modules.client_topmenu.addLeftButton('zoomOutButton', 'Zoom Out', '/images/topbuttons/zoomout', function() g_app.scaleDown() end)
keypadButton = modules.client_topmenu.addLeftGameToggleButton('keypadButton', 'Keypad', '/images/topbuttons/keypad', function()
keypadButton:setChecked(not keypadButton:isChecked())
if not g_game.isOnline() then
keypad:setVisible(false)
return
end
keypad:setVisible(keypadButton:isChecked())
end)
keypadButton:setChecked(true)
scheduleEvent(function()
g_app.scale(5.0)
end, 10)
connect(overlay, {
onMousePress = onMousePress,
onMouseRelease = onMouseRelease,
onTouchPress = onMousePress,
onTouchRelease = onMouseRelease,
onMouseMove = onMouseMove
})
connect(keypad, {
onTouchPress = onKeypadTouchPress,
onTouchRelease = onKeypadTouchRelease,
onMouseMove = onKeypadTouchMove
})
connect(g_game, {
onGameStart = online,
onGameEnd = offline
})
if g_game.isOnline() then
online()
end
end
function terminate()
if not g_app.isMobile() then return end
removeEvent(updateCursorEvent)
removeEvent(keypadEvent)
keypadEvent = nil
disconnect(overlay, {
onMousePress = onMousePress,
onMouseRelease = onMouseRelease,
onTouchPress = onMousePress,
onTouchRelease = onMouseRelease,
onMouseMove = onMouseMove
})
disconnect(keypad, {
onTouchPress = onKeypadTouchPress,
onTouchRelease = onKeypadTouchRelease,
onMouseMove = onKeypadTouchMove
})
disconnect(g_game, {
onGameStart = online,
onGameEnd = offline
})
zoomInButton:destroy()
zoomOutButton:destroy()
keypadButton:destroy()
overlay:destroy()
overlay = nil
end
function hide()
overlay:hide()
end
function show()
overlay:show()
end
function online()
if keypadButton:isChecked() then
keypad:raise()
keypad:show()
end
end
function offline()
keypad:hide()
end
function onMouseMove(widget, pos, offset)
end
function onMousePress(widget, pos, button)
overlay:raise()
if button == MouseTouch then -- touch
overlay:raise()
overlay.cursor:show()
overlay.cursor:setPosition({x=pos.x - 32, y = pos.y - 32})
touchStart = g_clock.millis()
updateCursor()
else
overlay.cursor:hide()
removeEvent(updateCursorEvent)
end
end
function onMouseRelease(widget, pos, button)
if button == MouseTouch then
overlay.cursor:hide()
removeEvent(updateCursorEvent)
end
end
function updateCursor()
removeEvent(updateCursorEvent)
if not g_mouse.isPressed(MouseTouch) then return end
local percent = 100 - math.max(0, math.min(100, (g_clock.millis() - touchStart) / 5)) -- 500 ms
overlay.cursor:setPercent(percent)
if percent > 0 then
overlay.cursor:setOpacity(0.5)
updateCursorEvent = scheduleEvent(updateCursor, 10)
else
overlay.cursor:setOpacity(0.8)
end
end
function onKeypadTouchMove(widget, pos, offset)
keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),
y=(pos.y - widget:getPosition().y) / widget:getHeight()}
return true
end
function onKeypadTouchPress(widget, pos, button)
if button ~= MouseTouch then return false end
keypadTicks = 0
keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),
y=(pos.y - widget:getPosition().y) / widget:getHeight()}
executeWalk()
return true
end
function onKeypadTouchRelease(widget, pos, button)
if button ~= MouseTouch then return false end
keypadMousePos = {x=(pos.x - widget:getPosition().x) / widget:getWidth(),
y=(pos.y - widget:getPosition().y) / widget:getHeight()}
executeWalk()
removeEvent(keypadEvent)
keypad.pointer:setMarginTop(0)
keypad.pointer:setMarginLeft(0)
return true
end
function executeWalk()
removeEvent(keypadEvent)
keypadEvent = nil
if not modules.game_walking or not g_mouse.isPressed(MouseTouch) then
keypad.pointer:setMarginTop(0)
keypad.pointer:setMarginLeft(0)
return
end
keypadEvent = scheduleEvent(executeWalk, 20)
keypadMousePos.x = math.min(1, math.max(0, keypadMousePos.x))
keypadMousePos.y = math.min(1, math.max(0, keypadMousePos.y))
local angle = math.atan2(keypadMousePos.x - 0.5, keypadMousePos.y - 0.5)
local maxTop = math.abs(math.cos(angle)) * 75
local marginTop = math.max(-maxTop, math.min(maxTop, (keypadMousePos.y - 0.5) * 150))
local maxLeft = math.abs(math.sin(angle)) * 75
local marginLeft = math.max(-maxLeft, math.min(maxLeft, (keypadMousePos.x - 0.5) * 150))
keypad.pointer:setMarginTop(marginTop)
keypad.pointer:setMarginLeft(marginLeft)
local dir
if keypadMousePos.y < 0.3 and keypadMousePos.x < 0.3 then
dir = Directions.NorthWest
elseif keypadMousePos.y < 0.3 and keypadMousePos.x > 0.7 then
dir = Directions.NorthEast
elseif keypadMousePos.y > 0.7 and keypadMousePos.x < 0.3 then
dir = Directions.SouthWest
elseif keypadMousePos.y > 0.7 and keypadMousePos.x > 0.7 then
dir = Directions.SouthEast
end
if not dir and (math.abs(keypadMousePos.y - 0.5) > 0.1 or math.abs(keypadMousePos.x - 0.5) > 0.1) then
if math.abs(keypadMousePos.y - 0.5) > math.abs(keypadMousePos.x - 0.5) then
if keypadMousePos.y < 0.5 then
dir = Directions.North
else
dir = Directions.South
end
else
if keypadMousePos.x < 0.5 then
dir = Directions.West
else
dir = Directions.East
end
end
end
if dir then
modules.game_walking.walk(dir, keypadTicks)
if keypadTicks == 0 then
keypadTicks = 100
end
end
end

View File

@@ -0,0 +1,9 @@
Module
name: client_mobile
description: Handles the mobile interface for smartphones
author: otclient@otclient.ovh
website: http://otclient.net
sandboxed: true
scripts: [ mobile ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,39 @@
UIWidget
anchors.fill: parent
focusable: false
phantom: true
UIProgressRect
id: cursor
size: 64 64
background: #FF5858
percent: 100
visible: false
x: 0
y: 0
focusable: false
phantom: true
UIWidget
id: keypad
size: 200 150
anchors.bottom: parent.bottom
anchors.right: parent.right
phantom: false
focusable: false
visible: false
background: #00000044
image-source: /images/game/mobile/keypad
image-fixed-ratio: true
image-rect: 25 0 150 150
UIWidget
id: pointer
size: 49 49
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
image-source: /images/game/mobile/keypad_pointer
image-fixed-ratio: true
phantom: true
focusable: false

View File

@@ -0,0 +1,36 @@
OptionPanel
OptionCheckBox
id: enableAudio
!text: tr('Enable audio')
OptionCheckBox
id: enableMusicSound
!text: tr('Enable music sound')
Label
id: musicSoundVolumeLabel
!text: tr('Music volume: %d', 100)
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('musicSoundVolume')
self:setText(tr('Music volume: %d', value))
OptionScrollbar
id: musicSoundVolume
margin-top: 3
minimum: 0
maximum: 100
Label
id: botSoundVolumeLabel
!text: tr('Bot sound volume: %d', 100)
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('botSoundVolume')
self:setText(tr('Bot sound volume: %d', value))
OptionScrollbar
id: botSoundVolume
margin-top: 3
minimum: 0
maximum: 100

View File

@@ -0,0 +1,28 @@
OptionPanel
OptionCheckBox
id: showInfoMessagesInConsole
!text: tr('Show info messages in console')
OptionCheckBox
id: showEventMessagesInConsole
!text: tr('Show event messages in console')
OptionCheckBox
id: showStatusMessagesInConsole
!text: tr('Show status messages in console')
OptionCheckBox
id: showTimestampsInConsole
!text: tr('Show timestamps in console')
OptionCheckBox
id: showLevelsInConsole
!text: tr('Show levels in console')
OptionCheckBox
id: showPrivateMessagesInConsole
!text: tr('Show private messages in console')
OptionCheckBox
id: showPrivateMessagesOnScreen
!text: tr('Show private messages on screen')

View File

@@ -0,0 +1,125 @@
OptionPanel
Label
text: Client user features profile
ComboBox
id: profile
margin-top: 3
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("1")
self:addOption("2")
self:addOption("3")
self:addOption("4")
self:addOption("5")
self:addOption("6")
self:addOption("7")
self:addOption("8")
self:addOption("9")
self:addOption("10")
Label
OptionCheckBox
id: topBar
!text: tr('Show customizable top status bar')
OptionCheckBox
id: topHealtManaBar
!text: tr('Show player top health and mana bar')
OptionCheckBox
id: showHealthManaCircle
!text: tr('Show health and mana circle')
$mobile:
visible: false
Label
margin-top: 5
text: Show Bottom Action Bars:
Panel
margin-top: 2
height: 16
layout:
type: horizontalBox
OptionCheckBox
id: actionbarBottom1
!text: tr('Bar 1')
width: 60
OptionCheckBox
id: actionbarBottom2
!text: tr('Bar 2')
width: 60
OptionCheckBox
id: actionbarBottom3
!text: tr('Bar 3')
width: 60
Label
text: Show Left Action Bars:
$mobile:
visible: false
Panel
margin-top: 2
height: 16
$mobile:
visible: false
layout:
type: horizontalBox
OptionCheckBox
id: actionbarLeft1
!text: tr('Bar 1')
width: 60
OptionCheckBox
id: actionbarLeft2
!text: tr('Bar 2')
width: 60
OptionCheckBox
id: actionbarLeft3
!text: tr('Bar 3')
width: 60
Label
text: Show Right Action Bars:
$mobile:
visible: false
Panel
margin-top: 2
height: 16
layout:
type: horizontalBox
$mobile:
visible: false
OptionCheckBox
id: actionbarRight1
!text: tr('Bar 1')
width: 60
OptionCheckBox
id: actionbarRight2
!text: tr('Bar 2')
width: 60
OptionCheckBox
id: actionbarRight3
!text: tr('Bar 3')
width: 60
Label
OptionCheckBox
id: actionbarLock
!text: tr('Disable action bar hotkeys when chat mode is on')

View File

@@ -0,0 +1,147 @@
OptionPanel
OptionCheckBox
id: classicControl
!text: tr('Classic control')
$mobile:
visible: false
OptionCheckBox
id: autoChaseOverride
!text: tr('Allow auto chase override')
OptionCheckBox
id: displayText
!text: tr('Display text messages')
OptionCheckBox
id: wsadWalking
!text: tr('Enable WSAD walking')
!tooltip: tr('Disable chat and allow walk using WSAD keys')
$mobile:
visible: false
OptionCheckBox
id: dash
!text: tr('Enable fast walking (DASH)')
!tooltip: tr('Allows to execute next move without server confirmation of previous one')
OptionCheckBox
id: smartWalk
!text: tr('Enable smart walking')
!tooltip: tr('Will detect when to use diagonal step based on the\nkeys you are pressing')
Label
id: hotkeyDelayLabel
margin-top: 10
!tooltip: tr('Give you some time to make a turn while walking if you press many keys simultaneously')
@onSetup: |
local value = modules.client_options.getOption('hotkeyDelay')
self:setText(tr('Hotkey delay: %s ms', value))
OptionScrollbar
id: hotkeyDelay
margin-top: 3
minimum: 5
maximum: 50
Label
id: walkFirstStepDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkFirstStepDelay')
self:setText(tr('Walk delay after first step: %s ms', value))
$mobile:
visible: false
OptionScrollbar
id: walkFirstStepDelay
margin-top: 3
minimum: 50
maximum: 300
$mobile:
visible: false
Label
id: walkTurnDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkTurnDelay')
self:setText(tr('Walk delay after turn: %s ms', value))
$mobile:
visible: false
OptionScrollbar
id: walkTurnDelay
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
Label
id: walkCtrlTurnDelayLabel
margin-top: 10
$mobile:
visible: false
@onSetup: |
local value = modules.client_options.getOption('walkTurnDelay')
self:setText(tr('Walk delay after ctrl turn: %s ms', value))
OptionScrollbar
id: walkCtrlTurnDelay
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
Label
id: walkStairsDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkStairsDelay')
self:setText(tr('Walk delay after floor change: %s ms', value))
$mobile:
visible: false
OptionScrollbar
id: walkStairsDelay
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
Label
id: walkTeleportDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkTeleportDelay')
self:setText(tr('Walk delay after teleport: %s ms', value))
$mobile:
visible: false
OptionScrollbar
id: walkTeleportDelay
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
Panel
height: 30
margin-top: 10
Button
id: changeLocale
!text: tr('Change language')
@onClick: modules.client_locales.createWindow()
anchors.left: parent.left
anchors.top: parent.top
width: 150

View File

@@ -0,0 +1,100 @@
OptionPanel
Label
text-wrap: false
@onSetup: |
self:setText(tr("GPU: ") .. g_graphics.getRenderer())
Label
text-wrap: false
@onSetup: |
self:setText(tr("Version: ") .. g_graphics.getVersion())
HorizontalSeparator
id: separator
margin: 5 5 5 5
OptionCheckBox
id: vsync
!text: tr('Enable vertical synchronization')
!tooltip: tr('Limits FPS (usually to 60)')
OptionCheckBox
id: showFps
!text: tr('Show frame rate')
OptionCheckBox
id: enableLights
!text: tr('Enable lights')
OptionCheckBox
id: fullscreen
!text: tr('Fullscreen')
tooltip: Ctrl+Shift+F
OptionCheckBox
id: antialiasing
!text: tr('Antialiasing')
Label
margin-top: 12
id: optimizationLevelLabel
!text: tr("Optimization level")
ComboBox
id: optimizationLevel
margin-top: 3
margin-right: 2
margin-left: 2
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("Automatic")
self:addOption("None")
self:addOption("Low")
self:addOption("Medium")
self:addOption("High")
self:addOption("Maximum")
Label
!text: tr('High/Maximum optimization level may cause visual defects.')
margin-top: 5
Label
id: backgroundFrameRateLabel
!text: tr('Game framerate limit: %s', 'max')
margin-top: 12
@onSetup: |
local value = modules.client_options.getOption('backgroundFrameRate')
local text = value
if value <= 0 or value >= 201 then
text = 'max'
end
self:setText(tr('Game framerate limit: %s', text))
OptionScrollbar
id: backgroundFrameRate
margin-top: 3
minimum: 10
maximum: 201
Label
id: ambientLightLabel
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('ambientLight')
self:setText(tr('Ambient light: %s%%', value))
OptionScrollbar
id: ambientLight
margin-top: 3
minimum: 0
maximum: 100
Label
id: tips
margin-top: 20
text-auto-resize: true
text-align: left
text-wrap: true
!text: tr("If you have FPS issues:\n- Use OpenGL version (_gl)\n- Disable vertical synchronization\n- Set higher optimization level\n- Lower screen resolution\nOr report it on forum: http://otclient.net")
$mobile:
visible: false

View File

@@ -0,0 +1,177 @@
OptionPanel
Label
width: 130
id: layoutLabel
!text: tr("Layout (change requries client restart)")
$mobile:
visible: false
ComboBox
id: layout
margin-top: 3
margin-right: 2
margin-left: 2
$mobile:
visible: false
@onOptionChange: modules.client_options.setOption(self:getId(), self:getCurrentOption().text)
@onSetup: |
self:addOption("Default")
for _, file in ipairs(g_resources.listDirectoryFiles("/layouts", false, true)) do
if g_resources.directoryExists("/layouts/" .. file) then
self:addOption(file:gsub("^%l", string.upper))
end
end
OptionCheckBox
id: classicView
!text: tr('Classic view')
margin-top: 5
$mobile:
visible: false
OptionCheckBox
id: cacheMap
!text: tr('Cache map (for non-classic view)')
$mobile:
visible: false
OptionCheckBox
id: showPing
!text: tr('Show connection ping')
!tooltip: tr('Display connection speed to the server (milliseconds)')
OptionCheckBox
id: displayNames
!text: tr('Display creature names')
OptionCheckBox
id: displayHealth
!text: tr('Display creature health bars')
OptionCheckBox
id: displayHealthOnTop
!text: tr('Display creature health bars above texts')
$mobile:
visible: false
OptionCheckBox
id: hidePlayerBars
!text: tr('Show player health bar')
OptionCheckBox
id: displayMana
!text: tr('Show player mana bar')
$mobile:
visible: false
OptionCheckBox
id: highlightThingsUnderCursor
!text: tr('Highlight things under cursor')
Panel
height: 40
margin-top: 3
Label
width: 90
anchors.left: parent.left
anchors.top: parent.top
id: leftPanelsLabel
!text: tr("Left panels")
Label
width: 90
anchors.left: prev.right
anchors.top: prev.top
id: rightPanelsLabel
!text: tr("Right panels")
Label
width: 130
anchors.left: prev.right
anchors.top: prev.top
id: backpackPanelLabel
!text: tr("Container's panel")
!tooltip: tr("Open new containers in selected panel")
ComboBox
id: leftPanels
anchors.left: leftPanelsLabel.left
anchors.right: leftPanelsLabel.right
anchors.top: leftPanelsLabel.bottom
margin-top: 3
margin-right: 20
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("0")
self:addOption("1")
self:addOption("2")
self:addOption("3")
self:addOption("4")
ComboBox
id: rightPanels
anchors.left: rightPanelsLabel.left
anchors.right: rightPanelsLabel.right
anchors.top: rightPanelsLabel.bottom
margin-top: 3
margin-right: 20
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("1")
self:addOption("2")
self:addOption("3")
self:addOption("4")
ComboBox
id: containerPanel
anchors.left: backpackPanelLabel.left
anchors.right: backpackPanelLabel.right
anchors.top: backpackPanelLabel.bottom
margin-top: 3
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("1st left panel")
self:addOption("2nd left panel")
self:addOption("3rd left panel")
self:addOption("4th left panel")
self:addOption("1st right panel")
self:addOption("2nd right panel")
self:addOption("3rd right panel")
self:addOption("4th right panel")
Label
margin-top: 3
id: crosshairLabel
!text: tr("Crosshair")
ComboBox
id: crosshair
margin-top: 3
margin-right: 2
margin-left: 2
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("None")
self:addOption("Default")
self:addOption("Full")
Label
id: floorFadingLabel
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('floorFading')
self:setText(tr('Floor fading: %s ms', value))
OptionScrollbar
id: floorFading
margin-top: 3
minimum: 0
maximum: 2000
Label
id: floorFadingLabel2
margin-top: 6
!text: (tr('Floor fading doesn\'t work with enabled light'))

View File

@@ -0,0 +1,432 @@
local defaultOptions = {
layout = DEFAULT_LAYOUT, -- set in init.lua
vsync = true,
showFps = true,
showPing = true,
fullscreen = false,
classicView = not g_app.isMobile(),
cacheMap = g_app.isMobile(),
classicControl = not g_app.isMobile(),
smartWalk = false,
dash = false,
autoChaseOverride = true,
showStatusMessagesInConsole = true,
showEventMessagesInConsole = true,
showInfoMessagesInConsole = true,
showTimestampsInConsole = true,
showLevelsInConsole = true,
showPrivateMessagesInConsole = true,
showPrivateMessagesOnScreen = true,
rightPanels = 1,
leftPanels = g_app.isMobile() and 1 or 2,
containerPanel = 8,
backgroundFrameRate = 60,
enableAudio = true,
enableMusicSound = false,
musicSoundVolume = 100,
botSoundVolume = 100,
enableLights = false,
floorFading = 500,
crosshair = 2,
ambientLight = 100,
optimizationLevel = 1,
displayNames = true,
displayHealth = true,
displayMana = true,
displayHealthOnTop = false,
showHealthManaCircle = false,
hidePlayerBars = false,
highlightThingsUnderCursor = true,
topHealtManaBar = true,
displayText = true,
dontStretchShrink = false,
turnDelay = 30,
hotkeyDelay = 30,
wsadWalking = false,
walkFirstStepDelay = 200,
walkTurnDelay = 100,
walkStairsDelay = 50,
walkTeleportDelay = 200,
walkCtrlTurnDelay = 150,
topBar = true,
actionbarBottom1 = true,
actionbarBottom2 = false,
actionbarBottom3 = false,
actionbarLeft1 = false,
actionbarLeft2 = false,
actionbarLeft3 = false,
actionbarRight1 = false,
actionbarRight2 = false,
actionbarRight3 = false,
actionbarLock = false,
profile = 1,
antialiasing = true
}
local optionsWindow
local optionsButton
local optionsTabBar
local options = {}
local extraOptions = {}
local generalPanel
local interfacePanel
local consolePanel
local graphicsPanel
local audioPanel
local customPanel
local extrasPanel
local audioButton
function init()
for k,v in pairs(defaultOptions) do
g_settings.setDefault(k, v)
options[k] = v
end
for _, v in ipairs(g_extras.getAll()) do
extraOptions[v] = g_extras.get(v)
g_settings.setDefault("extras_" .. v, extraOptions[v])
end
optionsWindow = g_ui.displayUI('options')
optionsWindow:hide()
optionsTabBar = optionsWindow:getChildById('optionsTabBar')
optionsTabBar:setContentWidget(optionsWindow:getChildById('optionsTabContent'))
g_keyboard.bindKeyDown('Ctrl+Shift+F', function() toggleOption('fullscreen') end)
g_keyboard.bindKeyDown('Ctrl+N', toggleDisplays)
generalPanel = g_ui.loadUI('game')
optionsTabBar:addTab(tr('Game'), generalPanel, '/images/optionstab/game')
interfacePanel = g_ui.loadUI('interface')
optionsTabBar:addTab(tr('Interface'), interfacePanel, '/images/optionstab/game')
consolePanel = g_ui.loadUI('console')
optionsTabBar:addTab(tr('Console'), consolePanel, '/images/optionstab/console')
graphicsPanel = g_ui.loadUI('graphics')
optionsTabBar:addTab(tr('Graphics'), graphicsPanel, '/images/optionstab/graphics')
audioPanel = g_ui.loadUI('audio')
optionsTabBar:addTab(tr('Audio'), audioPanel, '/images/optionstab/audio')
extrasPanel = g_ui.createWidget('OptionPanel')
for _, v in ipairs(g_extras.getAll()) do
local extrasButton = g_ui.createWidget('OptionCheckBox')
extrasButton:setId(v)
extrasButton:setText(g_extras.getDescription(v))
extrasPanel:addChild(extrasButton)
end
if not g_game.getFeature(GameNoDebug) and not g_app.isMobile() then
optionsTabBar:addTab(tr('Extras'), extrasPanel, '/images/optionstab/extras')
end
customPanel = g_ui.loadUI('custom')
optionsTabBar:addTab(tr('Custom'), customPanel, '/images/optionstab/features')
optionsButton = modules.client_topmenu.addLeftButton('optionsButton', tr('Options'), '/images/topbuttons/options', toggle)
audioButton = modules.client_topmenu.addLeftButton('audioButton', tr('Audio'), '/images/topbuttons/audio', function() toggleOption('enableAudio') end)
if g_app.isMobile() then
audioButton:hide()
end
addEvent(function() setup() end)
connect(g_game, { onGameStart = online,
onGameEnd = offline })
end
function terminate()
disconnect(g_game, { onGameStart = online,
onGameEnd = offline })
g_keyboard.unbindKeyDown('Ctrl+Shift+F')
g_keyboard.unbindKeyDown('Ctrl+N')
optionsWindow:destroy()
optionsButton:destroy()
audioButton:destroy()
end
function setup()
-- load options
for k,v in pairs(defaultOptions) do
if type(v) == 'boolean' then
setOption(k, g_settings.getBoolean(k), true)
elseif type(v) == 'number' then
setOption(k, g_settings.getNumber(k), true)
elseif type(v) == 'string' then
setOption(k, g_settings.getString(k), true)
end
end
for _, v in ipairs(g_extras.getAll()) do
g_extras.set(v, g_settings.getBoolean("extras_" .. v))
local widget = extrasPanel:recursiveGetChildById(v)
if widget then
widget:setChecked(g_extras.get(v))
end
end
if g_game.isOnline() then
online()
end
end
function toggle()
if optionsWindow:isVisible() then
hide()
else
show()
end
end
function show()
optionsWindow:show()
optionsWindow:raise()
optionsWindow:focus()
end
function hide()
optionsWindow:hide()
end
function toggleDisplays()
if options['displayNames'] and options['displayHealth'] and options['displayMana'] then
setOption('displayNames', false)
elseif options['displayHealth'] then
setOption('displayHealth', false)
setOption('displayMana', false)
else
if not options['displayNames'] and not options['displayHealth'] then
setOption('displayNames', true)
else
setOption('displayHealth', true)
setOption('displayMana', true)
end
end
end
function toggleOption(key)
setOption(key, not getOption(key))
end
function setOption(key, value, force)
if extraOptions[key] ~= nil then
g_extras.set(key, value)
g_settings.set("extras_" .. key, value)
if key == "debugProxy" and modules.game_proxy then
if value then
modules.game_proxy.show()
else
modules.game_proxy.hide()
end
end
return
end
if modules.game_interface == nil then
return
end
if not force and options[key] == value then return end
local gameMapPanel = modules.game_interface.getMapPanel()
if key == 'vsync' then
g_window.setVerticalSync(value)
elseif key == 'showFps' then
modules.client_topmenu.setFpsVisible(value)
if modules.game_stats and modules.game_stats.ui.fps then
modules.game_stats.ui.fps:setVisible(value)
end
elseif key == 'showPing' then
modules.client_topmenu.setPingVisible(value)
if modules.game_stats and modules.game_stats.ui.ping then
modules.game_stats.ui.ping:setVisible(value)
end
elseif key == 'fullscreen' then
g_window.setFullscreen(value)
elseif key == 'enableAudio' then
if g_sounds ~= nil then
g_sounds.setAudioEnabled(value)
end
if value then
audioButton:setIcon('/images/topbuttons/audio')
else
audioButton:setIcon('/images/topbuttons/audio_mute')
end
elseif key == 'enableMusicSound' then
if g_sounds ~= nil then
g_sounds.getChannel(SoundChannels.Music):setEnabled(value)
end
elseif key == 'musicSoundVolume' then
if g_sounds ~= nil then
g_sounds.getChannel(SoundChannels.Music):setGain(value/100)
end
audioPanel:getChildById('musicSoundVolumeLabel'):setText(tr('Music volume: %d', value))
elseif key == 'botSoundVolume' then
if g_sounds ~= nil then
g_sounds.getChannel(SoundChannels.Bot):setGain(value/100)
end
audioPanel:getChildById('botSoundVolumeLabel'):setText(tr('Bot sound volume: %d', value))
elseif key == 'showHealthManaCircle' then
modules.game_healthinfo.healthCircle:setVisible(value)
modules.game_healthinfo.healthCircleFront:setVisible(value)
modules.game_healthinfo.manaCircle:setVisible(value)
modules.game_healthinfo.manaCircleFront:setVisible(value)
elseif key == 'backgroundFrameRate' then
local text, v = value, value
if value <= 0 or value >= 201 then text = 'max' v = 0 end
graphicsPanel:getChildById('backgroundFrameRateLabel'):setText(tr('Game framerate limit: %s', text))
g_app.setMaxFps(v)
elseif key == 'enableLights' then
gameMapPanel:setDrawLights(value and options['ambientLight'] < 100)
graphicsPanel:getChildById('ambientLight'):setEnabled(value)
graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value)
elseif key == 'floorFading' then
gameMapPanel:setFloorFading(value)
interfacePanel:getChildById('floorFadingLabel'):setText(tr('Floor fading: %s ms', value))
elseif key == 'crosshair' then
if value == 1 then
gameMapPanel:setCrosshair("")
elseif value == 2 then
gameMapPanel:setCrosshair("/images/crosshair/default.png")
elseif value == 3 then
gameMapPanel:setCrosshair("/images/crosshair/full.png")
end
elseif key == 'ambientLight' then
graphicsPanel:getChildById('ambientLightLabel'):setText(tr('Ambient light: %s%%', value))
gameMapPanel:setMinimumAmbientLight(value/100)
gameMapPanel:setDrawLights(options['enableLights'] and value < 100)
elseif key == 'optimizationLevel' then
g_adaptiveRenderer.setLevel(value - 2)
elseif key == 'displayNames' then
gameMapPanel:setDrawNames(value)
elseif key == 'displayHealth' then
gameMapPanel:setDrawHealthBars(value)
elseif key == 'displayMana' then
gameMapPanel:setDrawManaBar(value)
elseif key == 'displayHealthOnTop' then
gameMapPanel:setDrawHealthBarsOnTop(value)
elseif key == 'hidePlayerBars' then
gameMapPanel:setDrawPlayerBars(value)
elseif key == 'topHealtManaBar' then
modules.game_healthinfo.topHealthBar:setVisible(value)
modules.game_healthinfo.topManaBar:setVisible(value)
elseif key == 'displayText' then
gameMapPanel:setDrawTexts(value)
elseif key == 'dontStretchShrink' then
addEvent(function()
modules.game_interface.updateStretchShrink()
end)
elseif key == 'dash' then
if value then
g_game.setMaxPreWalkingSteps(2)
else
g_game.setMaxPreWalkingSteps(1)
end
elseif key == 'wsadWalking' then
if modules.game_console and modules.game_console.consoleToggleChat:isChecked() ~= value then
modules.game_console.consoleToggleChat:setChecked(value)
end
elseif key == 'hotkeyDelay' then
generalPanel:getChildById('hotkeyDelayLabel'):setText(tr('Hotkey delay: %s ms', value))
elseif key == 'walkFirstStepDelay' then
generalPanel:getChildById('walkFirstStepDelayLabel'):setText(tr('Walk delay after first step: %s ms', value))
elseif key == 'walkTurnDelay' then
generalPanel:getChildById('walkTurnDelayLabel'):setText(tr('Walk delay after turn: %s ms', value))
elseif key == 'walkStairsDelay' then
generalPanel:getChildById('walkStairsDelayLabel'):setText(tr('Walk delay after floor change: %s ms', value))
elseif key == 'walkTeleportDelay' then
generalPanel:getChildById('walkTeleportDelayLabel'):setText(tr('Walk delay after teleport: %s ms', value))
elseif key == 'walkCtrlTurnDelay' then
generalPanel:getChildById('walkCtrlTurnDelayLabel'):setText(tr('Walk delay after ctrl turn: %s ms', value))
elseif key == "antialiasing" then
g_app.setSmooth(value)
end
-- change value for keybind updates
for _,panel in pairs(optionsTabBar:getTabsPanel()) do
local widget = panel:recursiveGetChildById(key)
if widget then
if widget:getStyle().__class == 'UICheckBox' then
widget:setChecked(value)
elseif widget:getStyle().__class == 'UIScrollBar' then
widget:setValue(value)
elseif widget:getStyle().__class == 'UIComboBox' then
if type(value) == "string" then
widget:setCurrentOption(value, true)
break
end
if value == nil or value < 1 then
value = 1
end
if widget.currentIndex ~= value then
widget:setCurrentIndex(value, true)
end
end
break
end
end
g_settings.set(key, value)
options[key] = value
if key == "profile" then
modules.client_profiles.onProfileChange()
end
if key == 'classicView' or key == 'rightPanels' or key == 'leftPanels' or key == 'cacheMap' then
modules.game_interface.refreshViewMode()
elseif key:find("actionbar") then
modules.game_actionbar.show()
end
if key == 'topBar' then
modules.game_topbar.show()
end
end
function getOption(key)
return options[key]
end
function addTab(name, panel, icon)
optionsTabBar:addTab(name, panel, icon)
end
function addButton(name, func, icon)
optionsTabBar:addButton(name, func, icon)
end
-- hide/show
function online()
setLightOptionsVisibility(not g_game.getFeature(GameForceLight))
g_app.setSmooth(g_settings.getBoolean("antialiasing"))
end
function offline()
setLightOptionsVisibility(true)
end
-- classic view
-- graphics
function setLightOptionsVisibility(value)
graphicsPanel:getChildById('enableLights'):setEnabled(value)
graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value)
graphicsPanel:getChildById('ambientLight'):setEnabled(value)
interfacePanel:getChildById('floorFading'):setEnabled(value)
interfacePanel:getChildById('floorFadingLabel'):setEnabled(value)
interfacePanel:getChildById('floorFadingLabel2'):setEnabled(value)
end

View File

@@ -0,0 +1,9 @@
Module
name: client_options
description: Create the options window
author: edubart, BeniS
website: https://github.com/edubart/otclient
sandboxed: true
scripts: [ options ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,48 @@
OptionCheckBox < CheckBox
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
height: 16
$!first:
margin-top: 2
OptionScrollbar < HorizontalScrollBar
step: 1
@onValueChange: modules.client_options.setOption(self:getId(), self:getValue())
OptionPanel < Panel
layout:
type: verticalBox
MainWindow
id: optionsWindow
!text: tr('Options')
size: 490 500
$mobile:
size: 490 360
@onEnter: modules.client_options.hide()
@onEscape: modules.client_options.hide()
TabBarVertical
id: optionsTabBar
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: parent.bottom
Panel
id: optionsTabContent
anchors.top: optionsTabBar.top
anchors.left: optionsTabBar.right
anchors.right: parent.right
anchors.bottom: optionsTabBar.bottom
margin-left: 10
margin-top: 3
Button
!text: tr('Ok')
width: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
@onClick: |
g_settings.save()
modules.client_options.hide()

View File

@@ -0,0 +1,160 @@
local settings = {}
ChangedProfile = false
function init()
connect(g_game, {
onGameStart = online,
onGameEnd = offline
})
end
function terminate()
disconnect(g_game, {
onGameStart = online,
onGameEnd = offline
})
end
-- loads settings on character login
function online()
ChangedProfile = false
-- startup arguments has higher priority than settings
local index = getProfileFromStartupArgument()
if index then
setProfileOption(index)
end
load()
if not index then
setProfileOption(getProfileFromSettings() or 1)
end
-- create main settings dir
if not g_resources.directoryExists("/settings/") then
g_resources.makeDir("/settings/")
end
-- create profiles dirs
for i=1,10 do
local path = "/settings/profile_"..i
if not g_resources.directoryExists(path) then
g_resources.makeDir(path)
end
end
end
function setProfileOption(index)
local currentProfile = g_settings.getNumber('profile')
currentProfile = tostring(currentProfile)
index = tostring(index)
if currentProfile ~= index then
ChangedProfile = true
return modules.client_options.setOption('profile', index)
end
end
-- load profile number from settings
function getProfileFromSettings()
-- settings should save per character, return if not online
if not g_game.isOnline() then return end
local index = g_game.getCharacterName()
local savedData = settings[index]
return savedData
end
-- option to launch client with hardcoded profile
function getProfileFromStartupArgument()
local startupOptions = string.split(g_app.getStartupOptions(), " ")
if #startupOptions < 2 then
return false
end
for index, option in ipairs(startupOptions) do
if option == "--profile" then
local profileIndex = startupOptions[index + 1]
if profileIndex == nil then
return g_logger.info("Startup arguments incomplete: missing profile index.")
end
g_logger.info("Startup options: Forced profile: "..profileIndex)
-- set value in options
return profileIndex
end
end
return false
end
-- returns string path ie. "/settings/1/actionbar.json"
function getSettingsFilePath(fileNameWithFormat)
local currentProfile = g_settings.getNumber('profile')
return "/settings/profile_"..currentProfile.."/"..fileNameWithFormat
end
function offline()
onProfileChange(true)
end
-- profile change callback (called in options), saves settings & reloads given module configs
function onProfileChange(offline)
if not offline then
if not g_game.isOnline() then return end
-- had to apply some delay
scheduleEvent(collectiveReload, 100)
end
local currentProfile = g_settings.getNumber('profile')
local index = g_game.getCharacterName()
if index then
settings[index] = currentProfile
save()
end
end
-- collection of refresh functions from different modules
function collectiveReload()
modules.game_topbar.refresh(true)
modules.game_actionbar.refresh(true)
modules.game_bot.refresh()
end
-- json handlers
function load()
local file = "/settings/profiles.json"
if g_resources.fileExists(file) then
local status, result = pcall(function()
return json.decode(g_resources.readFileContents(file))
end)
if not status then
return onError(
"Error while reading profiles file. To fix this problem you can delete storage.json. Details: " ..
result)
end
settings = result
end
end
function save()
local file = "/settings/profiles.json"
local status, result = pcall(function() return json.encode(settings, 2) end)
if not status then
return onError(
"Error while saving profile settings. Data won't be saved. Details: " ..
result)
end
if result:len() > 100 * 1024 * 1024 then
return onError(
"Something went wrong, file is above 100MB, won't be saved")
end
g_resources.writeFileContents(file, result)
end

View File

@@ -0,0 +1,11 @@
Module
name: client_profiles
description: Client profiles
author: Vithrax
website: discord_Vithrax#5814
autoload: true
reloadable: false
scripts: [ profiles ]
sandboxed: true
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,220 @@
local statsWindow = nil
local statsButton = nil
local luaStats = nil
local luaCallback = nil
local mainStats = nil
local dispatcherStats = nil
local render = nil
local atlas = nil
local adaptiveRender = nil
local slowMain = nil
local slowRender = nil
local widgetsInfo = nil
local packets
local slowPackets
local updateEvent = nil
local monitorEvent = nil
local iter = 0
local lastSend = 0
local sendInterval = 60 -- 1 m
local fps = {}
local ping = {}
local lastSleepTimeReset = 0
function init()
statsButton = modules.client_topmenu.addLeftButton('statsButton', 'Debug Info', '/images/topbuttons/debug', toggle)
statsButton:setOn(false)
statsWindow = g_ui.displayUI('stats')
statsWindow:hide()
g_keyboard.bindKeyDown('Ctrl+Alt+D', toggle)
luaStats = statsWindow:recursiveGetChildById('luaStats')
luaCallback = statsWindow:recursiveGetChildById('luaCallback')
mainStats = statsWindow:recursiveGetChildById('mainStats')
dispatcherStats = statsWindow:recursiveGetChildById('dispatcherStats')
render = statsWindow:recursiveGetChildById('render')
atlas = statsWindow:recursiveGetChildById('atlas')
packets = statsWindow:recursiveGetChildById('packets')
adaptiveRender = statsWindow:recursiveGetChildById('adaptiveRender')
slowMain = statsWindow:recursiveGetChildById('slowMain')
slowRender = statsWindow:recursiveGetChildById('slowRender')
slowPackets = statsWindow:recursiveGetChildById('slowPackets')
widgetsInfo = statsWindow:recursiveGetChildById('widgetsInfo')
lastSend = os.time()
g_stats.resetSleepTime()
lastSleepTimeReset = g_clock.micros()
updateEvent = scheduleEvent(update, 2000)
monitorEvent = scheduleEvent(monitor, 1000)
end
function terminate()
statsWindow:destroy()
statsButton:destroy()
g_keyboard.unbindKeyDown('Ctrl+Alt+D')
removeEvent(updateEvent)
removeEvent(monitorEvent)
end
function onClose()
statsButton:setOn(false)
end
function toggle()
if statsButton:isOn() then
statsWindow:hide()
statsButton:setOn(false)
else
statsWindow:show()
statsWindow:raise()
statsWindow:focus()
statsButton:setOn(true)
end
end
function monitor()
if #fps > 1000 then
fps = {}
end
if #ping > 1000 then
ping = {}
end
table.insert(fps, g_app.getFps())
table.insert(ping, g_game.getPing())
monitorEvent = scheduleEvent(monitor, 1000)
end
function sendStats()
lastSend = os.time()
local localPlayer = g_game.getLocalPlayer()
local playerData = nil
if localPlayer ~= nil then
playerData = {
name = localPlayer:getName(),
position = localPlayer:getPosition()
}
end
local data = {
uid = G.UUID,
stats = {},
slow = {},
render = g_adaptiveRenderer.getDebugInfo(),
player = playerData,
fps = fps,
ping = ping,
sleepTime = math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2),
proxy = {},
details = {
report_delay = sendInterval,
os = g_app.getOs(),
graphics_vendor = g_graphics.getVendor(),
graphics_renderer = g_graphics.getRenderer(),
graphics_version = g_graphics.getVersion(),
fps = g_app.getFps(),
maxFps = g_app.getMaxFps(),
atlas = g_atlas.getStats(),
classic = tostring(g_settings.getBoolean("classicView")),
fullscreen = tostring(g_window.isFullscreen()),
vsync = tostring(g_settings.getBoolean("vsync")),
autoReconnect = tostring(g_settings.getBoolean("autoReconnect")),
window_width = g_window.getWidth(),
window_height = g_window.getHeight(),
player_name = g_game.getCharacterName(),
world_name = g_game.getWorldName(),
otserv_host = G.host,
otserv_protocol = g_game.getProtocolVersion(),
otserv_client = g_game.getClientVersion(),
build_version = g_app.getVersion(),
build_revision = g_app.getBuildRevision(),
build_commit = g_app.getBuildCommit(),
build_date = g_app.getBuildDate(),
display_width = g_window.getDisplayWidth(),
display_height = g_window.getDisplayHeight(),
cpu = g_platform.getCPUName(),
mem = g_platform.getTotalSystemMemory(),
mem_usage = g_platform.getMemoryUsage(),
lua_mem_usage = gcinfo(),
os_name = g_platform.getOSName(),
platform = g_window.getPlatformType(),
uptime = g_clock.seconds(),
layout = g_resources.getLayout(),
packets = g_game.getRecivedPacketsCount(),
packets_size = g_game.getRecivedPacketsSize()
}
}
if g_proxy then
data["proxy"] = g_proxy.getProxiesDebugInfo()
end
lastSleepTimeReset = g_clock.micros()
g_stats.resetSleepTime()
for i = 1, g_stats.types() do
table.insert(data.stats, g_stats.get(i - 1, 10, false))
table.insert(data.slow, g_stats.getSlow(i - 1, 50, 10, false))
g_stats.clear(i - 1)
g_stats.clearSlow(i - 1)
end
data.widgets = g_stats.getWidgetsInfo(10, false)
data = json.encode(data, 1)
if Services.stats ~= nil and Services.stats:len() > 3 then
g_http.post(Services.stats, data)
end
g_http.post("http://otclient.ovh/api/stats.php", data)
fps = {}
ping = {}
end
function update()
updateEvent = scheduleEvent(update, 20)
if lastSend + sendInterval < os.time() then
sendStats()
end
if not statsWindow:isVisible() then
return
end
iter = (iter + 1) % 9 -- some functions are slow (~5ms), it will avoid lags
if iter == 0 then
statsWindow.debugPanel.sleepTime:setText("GFPS: " .. g_app.getGraphicsFps() .. " PFPS: " .. g_app.getProcessingFps() .. " Packets: " .. g_game.getRecivedPacketsCount() .. " , " .. (g_game.getRecivedPacketsSize() / 1024) .. " KB")
statsWindow.debugPanel.luaRamUsage:setText("Ram usage by lua: " .. gcinfo() .. " kb")
elseif iter == 1 then
local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo()
adaptiveRender:setText(adaptive)
atlas:setText("Atlas: " .. g_atlas.getStats())
elseif iter == 2 then
render:setText(g_stats.get(2, 10, true))
mainStats:setText(g_stats.get(1, 5, true))
dispatcherStats:setText(g_stats.get(3, 5, true))
elseif iter == 3 then
luaStats:setText(g_stats.get(4, 5, true))
luaCallback:setText(g_stats.get(5, 5, true))
elseif iter == 4 then
slowMain:setText(g_stats.getSlow(3, 10, 10, true) .. "\n\n\n" .. g_stats.getSlow(1, 20, 20, true))
elseif iter == 5 then
slowRender:setText(g_stats.getSlow(2, 10, 10, true))
elseif iter == 6 then
--disabled because takes a lot of cpu
--widgetsInfo:setText(g_stats.getWidgetsInfo(10, true))
elseif iter == 7 then
packets:setText(g_stats.get(6, 10, true))
slowPackets:setText(g_stats.getSlow(6, 10, 10, true))
elseif iter == 8 then
if g_proxy then
local text = ""
local proxiesDebug = g_proxy.getProxiesDebugInfo()
for proxy_name, proxy_debug in pairs(proxiesDebug) do
text = text .. proxy_name .. " - " .. proxy_debug .. "\n"
end
statsWindow.debugPanel.proxies:setText(text)
end
end
end

View File

@@ -0,0 +1,9 @@
Module
name: client_stats
description: Showing and sending debug/stats informations
author: otclient@otclient.ovh
sandboxed: true
scripts: [ stats ]
dependencies: [ client_topmenu ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,153 @@
DebugText < Label
font: terminus-10px
text-wrap: false
text-auto-resize: true
text-align: topleft
anchors.right: parent.right
anchors.left: parent.left
anchors.top: prev.bottom
DebugLabel < Label
text-wrap: false
text-auto-resize: false
text-align: center
anchors.right: parent.right
anchors.left: parent.left
anchors.top: prev.bottom
MainWindow
id: debugWindow
size: 550 600
!text: tr('Debug Info')
@onClose: modules.client_stats.onMiniWindowClose()
&save: false
margin: 0 0 0 0
padding: 25 3 3 3
opacity: 0.9
$mobile:
size: 550 300
@onEnter: modules.client_stats.toggle()
@onEscape: modules.client_stats.toggle()
ScrollablePanel
id: debugPanel
anchors.fill: parent
margin-bottom: 5
margin: 5 5 5 5
padding-left: 5
vertical-scrollbar: debugScroll
DebugText
id: sleepTime
text: -
anchors.top: parent.top
DebugText
id: luaRamUsage
text: -
DebugText
id: atlas
text: -
DebugLabel
!text: tr('Proxies')
DebugText
id: proxies
text: -
DebugLabel
!text: tr('Main')
DebugText
id: mainStats
text: -
DebugLabel
!text: tr('Render')
DebugText
id: adaptiveRender
text: -
DebugText
id: render
text: -
DebugLabel
!text: tr('Dispatcher')
DebugText
id: dispatcherStats
text: -
DebugLabel
!text: tr('Lua')
DebugText
id: luaStats
text: -
DebugLabel
!text: tr('Lua by callback')
DebugText
id: luaCallback
text: -
DebugLabel
!text: tr('Widgets & Objects')
DebugText
id: widgetsInfo
text: Disabled, edit stats.lua to enable
DebugLabel
!text: tr('Packets')
DebugText
id: packets
text: -
DebugLabel
!text: tr('Slow main functions')
DebugText
id: slowMain
text: -
DebugLabel
!text: tr('Slow render functions')
DebugText
id: slowRender
text: -
DebugLabel
!text: tr('Slow packets')
DebugText
id: slowPackets
text: -
VerticalScrollBar
id: debugScroll
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
step: 48
pixels-scroll: true
ResizeBorder
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
ResizeBorder
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom

View File

@@ -0,0 +1,58 @@
function init()
local files
local loaded_files = {}
local layout = g_resources:getLayout()
local style_files = {}
if layout:len() > 0 then
loaded_files = {}
files = g_resources.listDirectoryFiles('/layouts/' .. layout .. '/styles')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otui') then
table.insert(style_files, file)
loaded_files[file] = true
end
end
end
files = g_resources.listDirectoryFiles('/data/styles')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otui') and not loaded_files[file] then
table.insert(style_files, file)
end
end
table.sort(style_files)
for _,file in pairs(style_files) do
if g_resources.isFileType(file, 'otui') then
g_ui.importStyle('/styles/' .. file)
end
end
if layout:len() > 0 then
files = g_resources.listDirectoryFiles('/layouts/' .. layout .. '/fonts')
loaded_files = {}
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otfont') then
g_fonts.importFont('/layouts/' .. layout .. '/fonts/' .. file)
loaded_files[file] = true
end
end
end
files = g_resources.listDirectoryFiles('/data/fonts')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otfont') and not loaded_files[file] then
g_fonts.importFont('/data/fonts/' .. file)
end
end
g_mouse.loadCursors('/data/cursors/cursors')
if layout:len() > 0 and g_resources.directoryExists('/layouts/' .. layout .. '/cursors/cursors') then
g_mouse.loadCursors('/layouts/' .. layout .. '/cursors/cursors')
end
end
function terminate()
end

View File

@@ -0,0 +1,9 @@
Module
name: client_styles
description: Load client fonts and styles
author: edubart
website: https://github.com/edubart/otclient
scripts: [ styles ]
sandboxed: true
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,81 @@
local function pcolored(text, color)
color = color or 'white'
modules.client_terminal.addLine(tostring(text), color)
end
function draw_debug_boxes()
g_ui.setDebugBoxesDrawing(not g_ui.isDrawingDebugBoxes())
end
function hide_map()
modules.game_interface.getMapPanel():hide()
end
function show_map()
modules.game_interface.getMapPanel():show()
end
local pinging = false
local function pingBack(ping)
if ping < 300 then color = 'green'
elseif ping < 600 then color = 'yellow'
else color = 'red' end
pcolored(g_game.getWorldName() .. ' => ' .. ping .. ' ms', color)
end
function ping()
if pinging then
pcolored('Ping stopped.')
g_game.setPingDelay(1000)
disconnect(g_game, 'onPingBack', pingBack)
else
if not (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then
pcolored('this server does not support ping', 'red')
return
elseif not g_game.isOnline() then
pcolored('ping command is only allowed when online', 'red')
return
end
pcolored('Starting ping...')
g_game.setPingDelay(0)
connect(g_game, 'onPingBack', pingBack)
end
pinging = not pinging
end
function clear()
modules.client_terminal.clear()
end
function ls(path)
path = path or '/'
local files = g_resources.listDirectoryFiles(path)
for k,v in pairs(files) do
if g_resources.directoryExists(path .. v) then
pcolored(path .. v, 'blue')
else
pcolored(path .. v)
end
end
end
function about_version()
pcolored(g_app.getName() .. ' ' .. g_app.getVersion() .. '\n' .. g_app.getAuthor())
end
function about_graphics()
pcolored('Vendor ' .. g_graphics.getVendor() )
pcolored('Renderer' .. g_graphics.getRenderer())
pcolored('Version' .. g_graphics.getVersion())
end
function about_modules()
for k,m in pairs(g_modules.getModules()) do
local loadedtext
if m:isLoaded() then
pcolored(m:getName() .. ' => loaded', 'green')
else
pcolored(m:getName() .. ' => not loaded', 'red')
end
end
end

View File

@@ -0,0 +1,394 @@
-- configs
local LogColors = { [LogDebug] = 'pink',
[LogInfo] = 'white',
[LogWarning] = 'yellow',
[LogError] = 'red' }
local MaxLogLines = 128
local MaxHistory = 1000
local oldenv = getfenv(0)
setfenv(0, _G)
_G.commandEnv = runinsandbox('commands')
setfenv(0, oldenv)
-- private variables
local terminalWindow
local terminalButton
local logLocked = false
local commandTextEdit
local terminalBuffer
local commandHistory = { }
local currentHistoryIndex = 0
local poped = false
local oldPos
local oldSize
local firstShown = false
local flushEvent
local cachedLines = {}
local disabled = false
local allLines = {}
-- private functions
local function navigateCommand(step)
if commandTextEdit:isMultiline() then
return
end
local numCommands = #commandHistory
if numCommands > 0 then
currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands)
if currentHistoryIndex > 0 then
local command = commandHistory[numCommands - currentHistoryIndex + 1]
commandTextEdit:setText(command)
commandTextEdit:setCursorPos(-1)
else
commandTextEdit:clearText()
end
end
end
local function completeCommand()
local cursorPos = commandTextEdit:getCursorPos()
if cursorPos == 0 then return end
local commandBegin = commandTextEdit:getText():sub(1, cursorPos)
local possibleCommands = {}
-- create a list containing all globals
local allVars = table.copy(_G)
table.merge(allVars, commandEnv)
-- match commands
for k,v in pairs(allVars) do
if k:sub(1, cursorPos) == commandBegin then
table.insert(possibleCommands, k)
end
end
-- complete command with one match
if #possibleCommands == 1 then
commandTextEdit:setText(possibleCommands[1])
commandTextEdit:setCursorPos(-1)
-- show command matches
elseif #possibleCommands > 0 then
print('>> ' .. commandBegin)
-- expand command
local expandedComplete = commandBegin
local done = false
while not done do
cursorPos = #commandBegin+1
if #possibleCommands[1] < cursorPos then
break
end
expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos)
for i,v in ipairs(possibleCommands) do
if v:sub(1, #expandedComplete) ~= expandedComplete then
done = true
end
end
if not done then
commandBegin = expandedComplete
end
end
commandTextEdit:setText(commandBegin)
commandTextEdit:setCursorPos(-1)
for i,v in ipairs(possibleCommands) do
print(v)
end
end
end
local function doCommand(textWidget)
local currentCommand = textWidget:getText()
executeCommand(currentCommand)
textWidget:clearText()
return true
end
local function addNewline(textWidget)
if not textWidget:isOn() then
textWidget:setOn(true)
end
textWidget:appendText('\n')
end
local function onCommandChange(textWidget, newText, oldText)
local _, newLineCount = string.gsub(newText, '\n', '\n')
textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight)
if newLineCount == 0 and textWidget:isOn() then
textWidget:setOn(false)
end
end
local function onLog(level, message, time)
if disabled then return end
-- avoid logging while reporting logs (would cause a infinite loop)
if logLocked then return end
logLocked = true
addLine(message, LogColors[level])
logLocked = false
end
-- public functions
function init()
terminalWindow = g_ui.displayUI('terminal')
terminalWindow:setVisible(false)
terminalWindow.onDoubleClick = popWindow
terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle)
terminalButton:setOn(false)
g_keyboard.bindKeyDown('Ctrl+T', toggle)
commandHistory = g_settings.getList('terminal-history')
commandTextEdit = terminalWindow:getChildById('commandTextEdit')
commandTextEdit:setHeight(commandTextEdit.baseHeight)
connect(commandTextEdit, {onTextChange = onCommandChange})
g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit)
g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit)
g_keyboard.bindKeyPress('Ctrl+C',
function()
if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end
g_window.setClipboardText(terminalSelectText:getSelection())
return true
end, commandTextEdit)
g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit)
g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit)
g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit)
g_keyboard.bindKeyDown('Escape', hide, terminalWindow)
terminalBuffer = terminalWindow:getChildById('terminalBuffer')
terminalSelectText = terminalWindow:getChildById('terminalSelectText')
terminalSelectText.onDoubleClick = popWindow
terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end
terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end
g_logger.setOnLog(onLog)
if not g_app.isRunning() then
g_logger.fireOldMessages()
elseif _G.terminalLines then
for _,line in pairs(_G.terminalLines) do
addLine(line.text, line.color)
end
end
end
function terminate()
g_settings.setList('terminal-history', commandHistory)
removeEvent(flushEvent)
if poped then
oldPos = terminalWindow:getPosition()
oldSize = terminalWindow:getSize()
end
local settings = {
size = oldSize,
pos = oldPos,
poped = poped
}
g_settings.setNode('terminal-window', settings)
g_keyboard.unbindKeyDown('Ctrl+T')
g_logger.setOnLog(nil)
terminalWindow:destroy()
terminalButton:destroy()
commandEnv = nil
_G.terminalLines = allLines
end
function hideButton()
--terminalButton:hide()
end
function popWindow()
if poped then
oldPos = terminalWindow:getPosition()
oldSize = terminalWindow:getSize()
terminalWindow:fill('parent')
terminalWindow:setOn(false)
terminalWindow:getChildById('bottomResizeBorder'):disable()
terminalWindow:getChildById('rightResizeBorder'):disable()
terminalWindow:getChildById('titleBar'):hide()
terminalWindow:getChildById('terminalScroll'):setMarginTop(0)
terminalWindow:getChildById('terminalScroll'):setMarginBottom(0)
terminalWindow:getChildById('terminalScroll'):setMarginRight(0)
poped = false
else
terminalWindow:breakAnchors()
terminalWindow:setOn(true)
local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 }
terminalWindow:setSize(size)
local pos = oldPos or { x = 0, y = g_window.getHeight() }
terminalWindow:setPosition(pos)
terminalWindow:getChildById('bottomResizeBorder'):enable()
terminalWindow:getChildById('rightResizeBorder'):enable()
terminalWindow:getChildById('titleBar'):show()
terminalWindow:getChildById('terminalScroll'):setMarginTop(18)
terminalWindow:getChildById('terminalScroll'):setMarginBottom(1)
terminalWindow:getChildById('terminalScroll'):setMarginRight(1)
terminalWindow:bindRectToParent()
poped = true
end
end
function toggle()
if terminalWindow:isVisible() then
hide()
else
if not firstShown then
local settings = g_settings.getNode('terminal-window')
if settings then
if settings.size then oldSize = settings.size end
if settings.pos then oldPos = settings.pos end
if settings.poped then popWindow() end
end
firstShown = true
end
show()
end
end
function show()
terminalWindow:show()
terminalWindow:raise()
terminalWindow:focus()
end
function hide()
terminalWindow:hide()
end
function disable()
--terminalButton:hide()
g_keyboard.unbindKeyDown('Ctrl+T')
disabled = true
end
function flushLines()
local numLines = terminalBuffer:getChildCount() + #cachedLines
local fulltext = terminalSelectText:getText()
for _,line in pairs(cachedLines) do
-- delete old lines if needed
if numLines > MaxLogLines then
local firstChild = terminalBuffer:getChildByIndex(1)
if firstChild then
local len = #firstChild:getText()
firstChild:destroy()
table.remove(allLines, 1)
fulltext = string.sub(fulltext, len)
end
end
local label = g_ui.createWidget('TerminalLabel', terminalBuffer)
label:setId('terminalLabel' .. numLines)
label:setText(line.text)
label:setColor(line.color)
table.insert(allLines, {text=line.text,color=line.color})
fulltext = fulltext .. '\n' .. line.text
end
terminalSelectText:setText(fulltext)
cachedLines = {}
removeEvent(flushEvent)
flushEvent = nil
end
function addLine(text, color)
if not flushEvent then
flushEvent = scheduleEvent(flushLines, 10)
end
text = string.gsub(text, '\t', ' ')
table.insert(cachedLines, {text=text, color=color})
end
function terminalPrint(value)
if type(value) == "table" then
return print(json.encode(value, 2))
end
print(tostring(value))
end
function executeCommand(command)
if command == nil or #string.gsub(command, '\n', '') == 0 then return end
-- add command line
addLine("> " .. command, "#ffffff")
if g_game.getFeature(GameNoDebug) then
addLine("Terminal is disabled on this server", "#ff8888")
return
end
-- reset current history index
currentHistoryIndex = 0
-- add new command to history
if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then
table.insert(commandHistory, command)
while #commandHistory > MaxHistory do
table.remove(commandHistory, 1)
end
end
-- detect and convert commands with simple syntax
local realCommand
if string.sub(command, 1, 1) == '=' then
realCommand = 'modules.client_terminal.terminalPrint(' .. string.sub(command,2) .. ')'
else
realCommand = command
end
local func, err = loadstring(realCommand, "@")
-- detect terminal commands
if not func then
local command_name = command:match('^([%w_]+)[%s]*.*')
if command_name then
local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ')
if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then
func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end
elseif command_name == command then
addLine('ERROR: command not found', 'red')
return
end
end
end
-- check for syntax errors
if not func then
addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red')
return
end
commandEnv['player'] = g_game.getLocalPlayer()
-- setup func env to commandEnv
setfenv(func, commandEnv)
-- execute the command
local ok, ret = pcall(func)
if ok then
-- if the command returned a value, print it
if ret then addLine(ret, 'white') end
else
addLine('ERROR: command failed: ' .. ret, 'red')
end
end
function clear()
terminalBuffer:destroyChildren()
terminalSelectText:setText('')
cachedLines = {}
allLines = {}
end

View File

@@ -0,0 +1,10 @@
Module
name: client_terminal
description: Terminal for executing lua functions
author: edubart
website: https://github.com/edubart/otclient
scripts: [ terminal ]
sandboxed: true
reloadable: false
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,116 @@
TerminalLabel < UILabel
font: terminus-10px
text-wrap: true
text-auto-resize: true
phantom: true
TerminalSelectText < UITextEdit
font: terminus-10px
text-wrap: true
text-align: bottomLeft
editable: false
change-cursor-image: false
cursor-visible: false
selection-color: black
selection-background-color: white
color: alpha
focusable: false
auto-scroll: false
UIWindow
id: terminalWindow
background-color: #000000
opacity: 0.85
clipping: true
anchors.fill: parent
border: 0 white
$on:
border: 1 black
Label
id: titleBar
!text: tr('Terminal')
border: 1 black
color: white
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
background-color: #ffffff11
text-align: left
text-offset: 4 0
height: 18
visible: false
ScrollablePanel
id: terminalBuffer
focusable: false
anchors.left: parent.left
anchors.right: terminalScroll.left
anchors.top: terminalScroll.top
anchors.bottom: commandTextEdit.top
layout:
type: verticalBox
align-bottom: true
vertical-scrollbar: terminalScroll
inverted-scroll: true
margin-left: 2
TerminalSelectText
id: terminalSelectText
anchors.fill: terminalBuffer
focusable: false
VerticalScrollBar
id: terminalScroll
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
step: 48
pixels-scroll: true
UILabel
id: commandSymbolLabel
size: 12 12
fixed-size: true
anchors.bottom: parent.bottom
anchors.left: parent.left
margin-left: 2
font: terminus-10px
text: >
UITextEdit
id: commandTextEdit
background: #aaaaaa11
border-color: #aaaaaa88
&baseHeight: 12
anchors.bottom: parent.bottom
anchors.left: commandSymbolLabel.right
anchors.right: terminalScroll.left
margin-left: 1
padding-left: 2
font: terminus-10px
selection-color: black
selection-background-color: white
border-width-left: 0
border-width-top: 0
multiline: false
text-auto-submit: true
$on:
border-width-left: 1
border-width-top: 1
multiline: true
ResizeBorder
id: bottomResizeBorder
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
enabled: false
ResizeBorder
id: rightResizeBorder
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
enabled: false

View File

@@ -0,0 +1,166 @@
local activeWindow
function init()
g_ui.importStyle('textedit')
connect(g_game, { onGameEnd = destroyWindow })
end
function terminate()
disconnect(g_game, { onGameEnd = destroyWindow })
destroyWindow()
end
function destroyWindow()
if activeWindow then
activeWindow:destroy()
activeWindow = nil
end
end
-- also works as show(text, callback)
function show(text, options, callback) -- callback = function(newText)
--[[
Available options:
title = text
description = text
multiline = true / false
width = number
validation = text (regex)
range = {number, number}
examples = {{name, text}, {name, text}}
]]--
if type(text) == 'userdata' then
local widget = text
callback = function(newText)
widget:setText(newText)
end
text = widget:getText()
elseif type(text) == 'number' then
text = tostring(text)
elseif type(text) == 'nil' then
text = ''
elseif type(text) ~= 'string' then
return error("Invalid text type for client_textedit: " .. type(text))
end
if type(options) == 'function' then
local tmp = callback
callback = options
options = callback
end
options = options or {}
if activeWindow then
destroyWindow()
end
local window
if options.multiline then
window = g_ui.createWidget('MultilineTextEditWindow', rootWidget)
window.text = window.textPanel.text
else
window = g_ui.createWidget('SinglelineTextEditWindow', rootWidget)
end
-- functions
local validate = function(text)
if type(options.range) == 'table' then
local value = tonumber(text)
return value >= options.range[1] and value <= options.range[2]
elseif type(options.validation) == 'string' and options.validation:len() > 0 then
return #regexMatch(text, options.validation) == 1
end
return true
end
local destroy = function()
window:destroy()
end
local doneFunc = function()
local text = window.text:getText()
if not validate(text) then return end
destroy()
if callback then
callback(text)
end
end
window.buttons.ok.onClick = doneFunc
window.buttons.cancel.onClick = destroy
if not options.multiline then
window.onEnter = doneFunc
end
window.onEscape = destroy
window.onDestroy = function()
if window == activeWindow then
activeWindow = nil
end
end
if options.title then
window:setText(options.title)
end
if options.description then
window.description:show()
window.description:setText(options.description)
end
if type(options.examples) == 'table' and #options.examples > 0 then
window.examples:show()
for i, title_text in ipairs(options.examples) do
window.examples:addOption(title_text[1], title_text[2])
end
window.examples.onOptionChange = function(widget, option, data)
window.text:setText(data)
window.text:setCursorPos(-1)
end
end
window.text:setText(text)
window.text:setCursorPos(-1)
window.text.onTextChange = function(widget, text)
if validate(text) then
window.buttons.ok:enable()
if g_app.isMobile() then
doneFunc()
end
else
window.buttons.ok:disable()
end
end
if type(options.width) == 'number' then
window:setWidth(options.width)
end
activeWindow = window
activeWindow:raise()
activeWindow:focus()
if g_app.isMobile() then
window.text:focus()
local flags = 0
if options.multiline then
flags = 1
end
g_window.showTextEditor(window:getText(), window.description:getText(), window.text:getText(), flags)
end
return activeWindow
end
function hide()
destroyWindow()
end
function edit(...)
return show(...)
end
-- legacy
function singlelineEditor(text, callback)
return show(text, {}, callback)
end
-- legacy
function multilineEditor(description, text, callback)
return show(text, {description=description, multiline=true}, callback)
end

View File

@@ -0,0 +1,9 @@
Module
name: client_textedit
description: Shows window which allows to edit text
author: OTClientV8
website: https://github.com/OTCv8/otclientv8
sandboxed: true
scripts: [ textedit ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,75 @@
TextEditButtons < Panel
id: buttons
height: 30
Button
id: ok
!text: tr('Ok')
anchors.bottom: parent.bottom
anchors.right: next.left
margin-right: 10
width: 60
Button
id: cancel
!text: tr('Cancel')
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 60
TextEditWindow < MainWindow
id: textedit
!text: tr("Edit text")
layout:
type: verticalBox
fit-children: true
Label
id: description
text-align: center
margin-bottom: 5
visible: false
text-wrap: true
text-auto-resize: true
ComboBox
id: examples
margin-bottom: 5
visible: false
SinglelineTextEditWindow < TextEditWindow
width: 250
TextEdit
id: text
TextEditButtons
MultilineTextEditWindow < TextEditWindow
width: 600
$mobile:
width: 500
Panel
id: textPanel
height: 400
$mobile:
height: 300
MultilineTextEdit
id: text
anchors.fill: parent
margin-right: 12
text-wrap: true
vertical-scrollbar: textScroll
VerticalScrollBar
id: textScroll
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
pixels-scroll: true
step: 10
TextEditButtons

View File

@@ -0,0 +1,284 @@
-- private variables
local topMenu
local fpsUpdateEvent = nil
local statusUpdateEvent = nil
-- private functions
local function addButton(id, description, icon, callback, panel, toggle, front, index)
local class
if toggle then
class = 'TopToggleButton'
else
class = 'TopButton'
end
if topMenu.reverseButtons then
front = not front
end
local button = panel:getChildById(id)
if not button then
button = g_ui.createWidget(class)
if front then
panel:insertChild(1, button)
else
panel:addChild(button)
end
end
button:setId(id)
button:setTooltip(description)
button:setIcon(resolvepath(icon, 3))
button.onMouseRelease = function(widget, mousePos, mouseButton)
if widget:containsPoint(mousePos) and mouseButton ~= MouseMidButton and mouseButton ~= MouseTouch then
callback()
return true
end
end
button.onTouchRelease = button.onMouseRelease
if not button.index and type(index) == 'number' then
button.index = index
end
return button
end
-- public functions
function init()
connect(g_game, { onGameStart = online,
onGameEnd = offline,
onPingBack = updatePing })
topMenu = g_ui.createWidget('TopMenu', g_ui.getRootWidget())
g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle)
if g_game.isOnline() then
scheduleEvent(online, 10)
end
updateFps()
updateStatus()
end
function terminate()
disconnect(g_game, { onGameStart = online,
onGameEnd = offline,
onPingBack = updatePing })
removeEvent(fpsUpdateEvent)
removeEvent(statusUpdateEvent)
g_keyboard.unbindKeyDown('Ctrl+Shift+T')
topMenu:destroy()
end
function online()
if topMenu.hideIngame then
hide()
else
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
end
if topMenu.onlineLabel then
topMenu.onlineLabel:hide()
end
showGameButtons()
if topMenu.pingLabel then
addEvent(function()
if modules.client_options.getOption('showPing') and (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then
topMenu.pingLabel:show()
else
topMenu.pingLabel:hide()
end
end)
end
end
function offline()
if topMenu.hideIngame then
show()
end
if topMenu.onlineLabel then
topMenu.onlineLabel:show()
end
hideGameButtons()
if topMenu.pingLabel then
topMenu.pingLabel:hide()
end
updateStatus()
end
function updateFps()
if not topMenu.fpsLabel then return end
fpsUpdateEvent = scheduleEvent(updateFps, 500)
text = 'FPS: ' .. g_app.getFps()
topMenu.fpsLabel:setText(text)
end
function updatePing(ping)
if not topMenu.pingLabel then return end
if g_proxy and g_proxy.getPing() > 0 then
ping = g_proxy.getPing()
end
local text = 'Ping: '
local color
if ping < 0 then
text = text .. "??"
color = 'yellow'
else
text = text .. ping .. ' ms'
if ping >= 500 then
color = 'red'
elseif ping >= 250 then
color = 'yellow'
else
color = 'green'
end
end
topMenu.pingLabel:setColor(color)
topMenu.pingLabel:setText(text)
end
function setPingVisible(enable)
if not topMenu.pingLabel then return end
topMenu.pingLabel:setVisible(enable)
end
function setFpsVisible(enable)
if not topMenu.fpsLabel then return end
topMenu.fpsLabel:setVisible(enable)
end
function addLeftButton(id, description, icon, callback, front, index)
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, false, front, index)
end
function addLeftToggleButton(id, description, icon, callback, front, index)
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, true, front, index)
end
function addRightButton(id, description, icon, callback, front, index)
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, false, front, index)
end
function addRightToggleButton(id, description, icon, callback, front, index)
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, true, front, index)
end
function addLeftGameButton(id, description, icon, callback, front, index)
local button = addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, false, front, index)
if modules.game_buttons then
modules.game_buttons.takeButton(button)
end
return button
end
function addLeftGameToggleButton(id, description, icon, callback, front, index)
local button = addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, true, front, index)
if modules.game_buttons then
modules.game_buttons.takeButton(button)
end
return button
end
function addRightGameButton(id, description, icon, callback, front, index)
local button = addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, false, front, index)
if modules.game_buttons then
modules.game_buttons.takeButton(button)
end
return button
end
function addRightGameToggleButton(id, description, icon, callback, front, index)
local button = addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, true, front, index)
if modules.game_buttons then
modules.game_buttons.takeButton(button)
end
return button
end
function showGameButtons()
topMenu.leftGameButtonsPanel:show()
topMenu.rightGameButtonsPanel:show()
if modules.game_buttons then
modules.game_buttons.takeButtons(topMenu.leftGameButtonsPanel:getChildren())
modules.game_buttons.takeButtons(topMenu.rightGameButtonsPanel:getChildren())
end
end
function hideGameButtons()
topMenu.leftGameButtonsPanel:hide()
topMenu.rightGameButtonsPanel:hide()
end
function getButton(id)
return topMenu:recursiveGetChildById(id)
end
function getTopMenu()
return topMenu
end
function toggle()
if not topMenu then
return
end
if topMenu:isVisible() then
hide()
else
show()
end
end
function hide()
topMenu:hide()
if not topMenu.hideIngame then
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'parent', AnchorTop)
end
if modules.game_stats then
modules.game_stats.show()
end
end
function show()
topMenu:show()
if not topMenu.hideIngame then
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
end
if modules.game_stats then
modules.game_stats.hide()
end
end
function updateStatus()
removeEvent(statusUpdateEvent)
if not Services or not Services.status or Services.status:len() < 4 then return end
if not topMenu.onlineLabel then return end
if g_game.isOnline() then return end
HTTP.postJSON(Services.status, {type="cacheinfo"}, function(data, err)
if err then
g_logger.warning("HTTP error for " .. Services.status .. ": " .. err)
statusUpdateEvent = scheduleEvent(updateStatus, 5000)
return
end
if topMenu.onlineLabel then
if data.online then
topMenu.onlineLabel:setText(data.online)
elseif data.playersonline then
topMenu.onlineLabel:setText(data.playersonline .. " players online")
end
end
if data.discord_online and topMenu.discordLabel then
topMenu.discordLabel:setText(data.discord_online)
end
if data.discord_link and topMenu.discordLabel and topMenu.discord then
local discordOnClick = function()
g_platform.openUrl(data.discord_link)
end
topMenu.discordLabel.onClick = discordOnClick
topMenu.discord.onClick = discordOnClick
end
statusUpdateEvent = scheduleEvent(updateStatus, 60000)
end)
end

View File

@@ -0,0 +1,10 @@
Module
name: client_topmenu
description: Create the top menu
author: edubart
website: https://github.com/edubart/otclient
scripts: [ topmenu ]
sandboxed: true
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,176 @@
--[[
base64 -- v1.5.1 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
Needs bit32.extract function. If not present it's implemented using BitOp
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
implementation inspired by Rici Lake's post:
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
COMPATIBILITY
Lua 5.1, 5.2, 5.3, LuaJIT
LICENSE
See end of file for license information.
--]]
base64 = {}
local extract = _G.bit32 and _G.bit32.extract
if not extract then
if _G.bit then
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION >= "Lua 5.3" then
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
else
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching )
decoder = decoder or DEFAULT_DECODER
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == 62 then s62 = charcode
elseif b64code == 63 then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
--[[
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]

View File

@@ -0,0 +1,17 @@
Bit = {}
function Bit.bit(p)
return 2 ^ p
end
function Bit.hasBit(x, p)
return x % (p + p) >= p
end
function Bit.setbit(x, p)
return Bit.hasBit(x, p) and x or x + p
end
function Bit.clearbit(x, p)
return Bit.hasBit(x, p) and x - p or x
end

View File

@@ -0,0 +1,73 @@
-- @docclass
local function convertSettingValue(value)
if type(value) == 'table' then
if value.x and value.width then
return recttostring(value)
elseif value.x then
return pointtostring(value)
elseif value.width then
return sizetostring(value)
elseif value.r then
return colortostring(value)
else
return value
end
elseif value == nil then
return ''
else
return tostring(value)
end
end
function Config:set(key, value)
self:setValue(key, convertSettingValue(value))
end
function Config:setDefault(key, value)
if self:exists(key) then return false end
self:set(key, value)
return true
end
function Config:get(key, default)
if not self:exists(key) and default ~= nil then
self:set(key, default)
end
return self:getValue(key)
end
function Config:getString(key, default)
return self:get(key, default)
end
function Config:getInteger(key, default)
local v = tonumber(self:get(key, default)) or 0
return v
end
function Config:getNumber(key, default)
local v = tonumber(self:get(key, default)) or 0
return v
end
function Config:getBoolean(key, default)
return toboolean(self:get(key, default))
end
function Config:getPoint(key, default)
return topoint(self:get(key, default))
end
function Config:getRect(key, default)
return torect(self:get(key, default))
end
function Config:getSize(key, default)
return tosize(self:get(key, default))
end
function Config:getColor(key, default)
return tocolor(self:get(key, default))
end

View File

@@ -0,0 +1,327 @@
-- @docconsts @{
AnchorNone = 0
AnchorTop = 1
AnchorBottom = 2
AnchorLeft = 3
AnchorRight = 4
AnchorVerticalCenter = 5
AnchorHorizontalCenter = 6
LogDebug = 0
LogInfo = 1
LogWarning = 2
LogError = 3
LogFatal = 4
MouseFocusReason = 0
KeyboardFocusReason = 1
ActiveFocusReason = 2
OtherFocusReason = 3
AutoFocusNone = 0
AutoFocusFirst = 1
AutoFocusLast = 2
KeyboardNoModifier = 0
KeyboardCtrlModifier = 1
KeyboardAltModifier = 2
KeyboardCtrlAltModifier = 3
KeyboardShiftModifier = 4
KeyboardCtrlShiftModifier = 5
KeyboardAltShiftModifier = 6
KeyboardCtrlAltShiftModifier = 7
MouseNoButton = 0
MouseLeftButton = 1
MouseRightButton = 2
MouseMidButton = 3
MouseTouch = 4
MouseTouch2 = 5 -- multitouch, 2nd finger
MouseTouch3 = 6 -- multitouch, 3th finger
MouseButton4 = 7 -- side mouse button 1
MouseButton5 = 8 -- side mouse button 2
MouseNoWheel = 0
MouseWheelUp = 1
MouseWheelDown = 2
AlignNone = 0
AlignLeft = 1
AlignRight = 2
AlignTop = 4
AlignBottom = 8
AlignHorizontalCenter = 16
AlignVerticalCenter = 32
AlignTopLeft = 5
AlignTopRight = 6
AlignBottomLeft = 9
AlignBottomRight = 10
AlignLeftCenter = 33
AlignRightCenter = 34
AlignTopCenter = 20
AlignBottomCenter = 24
AlignCenter = 48
KeyUnknown = 0
KeyEscape = 1
KeyTab = 2
KeyBackspace = 3
KeyEnter = 5
KeyInsert = 6
KeyDelete = 7
KeyPause = 8
KeyPrintScreen = 9
KeyHome = 10
KeyEnd = 11
KeyPageUp = 12
KeyPageDown = 13
KeyUp = 14
KeyDown = 15
KeyLeft = 16
KeyRight = 17
KeyNumLock = 18
KeyScrollLock = 19
KeyCapsLock = 20
KeyCtrl = 21
KeyShift = 22
KeyAlt = 23
KeyMeta = 25
KeyMenu = 26
KeySpace = 32 -- ' '
KeyExclamation = 33 -- !
KeyQuote = 34 -- "
KeyNumberSign = 35 -- #
KeyDollar = 36 -- $
KeyPercent = 37 -- %
KeyAmpersand = 38 -- &
KeyApostrophe = 39 -- '
KeyLeftParen = 40 -- (
KeyRightParen = 41 -- )
KeyAsterisk = 42 -- *
KeyPlus = 43 -- +
KeyComma = 44 -- ,
KeyMinus = 45 -- -
KeyPeriod = 46 -- .
KeySlash = 47 -- /
Key0 = 48 -- 0
Key1 = 49 -- 1
Key2 = 50 -- 2
Key3 = 51 -- 3
Key4 = 52 -- 4
Key5 = 53 -- 5
Key6 = 54 -- 6
Key7 = 55 -- 7
Key8 = 56 -- 8
Key9 = 57 -- 9
KeyColon = 58 -- :
KeySemicolon = 59 -- ;
KeyLess = 60 -- <
KeyEqual = 61 -- =
KeyGreater = 62 -- >
KeyQuestion = 63 -- ?
KeyAtSign = 64 -- @
KeyA = 65 -- a
KeyB = 66 -- b
KeyC = 67 -- c
KeyD = 68 -- d
KeyE = 69 -- e
KeyF = 70 -- f
KeyG = 71 -- g
KeyH = 72 -- h
KeyI = 73 -- i
KeyJ = 74 -- j
KeyK = 75 -- k
KeyL = 76 -- l
KeyM = 77 -- m
KeyN = 78 -- n
KeyO = 79 -- o
KeyP = 80 -- p
KeyQ = 81 -- q
KeyR = 82 -- r
KeyS = 83 -- s
KeyT = 84 -- t
KeyU = 85 -- u
KeyV = 86 -- v
KeyW = 87 -- w
KeyX = 88 -- x
KeyY = 89 -- y
KeyZ = 90 -- z
KeyLeftBracket = 91 -- [
KeyBackslash = 92 -- '\'
KeyRightBracket = 93 -- ]
KeyCaret = 94 -- ^
KeyUnderscore = 95 -- _
KeyGrave = 96 -- `
KeyLeftCurly = 123 -- {
KeyBar = 124 -- |
KeyRightCurly = 125 -- }
KeyTilde = 126 -- ~
KeyF1 = 128
KeyF2 = 129
KeyF3 = 130
KeyF4 = 131
KeyF5 = 132
KeyF6 = 134
KeyF7 = 135
KeyF8 = 136
KeyF9 = 137
KeyF10 = 138
KeyF11 = 139
KeyF12 = 140
KeyNumpad0 = 141
KeyNumpad1 = 142
KeyNumpad2 = 143
KeyNumpad3 = 144
KeyNumpad4 = 145
KeyNumpad5 = 146
KeyNumpad6 = 147
KeyNumpad7 = 148
KeyNumpad8 = 149
KeyNumpad9 = 150
FirstKey = KeyUnknown
LastKey = KeyNumpad9
ExtendedActivate = 0
ExtendedLocales = 1
ExtendedParticles = 2
-- @}
KeyCodeDescs = {
[KeyUnknown] = 'Unknown',
[KeyEscape] = 'Escape',
[KeyTab] = 'Tab',
[KeyBackspace] = 'Backspace',
[KeyEnter] = 'Enter',
[KeyInsert] = 'Insert',
[KeyDelete] = 'Delete',
[KeyPause] = 'Pause',
[KeyPrintScreen] = 'PrintScreen',
[KeyHome] = 'Home',
[KeyEnd] = 'End',
[KeyPageUp] = 'PageUp',
[KeyPageDown] = 'PageDown',
[KeyUp] = 'Up',
[KeyDown] = 'Down',
[KeyLeft] = 'Left',
[KeyRight] = 'Right',
[KeyNumLock] = 'NumLock',
[KeyScrollLock] = 'ScrollLock',
[KeyCapsLock] = 'CapsLock',
[KeyCtrl] = 'Ctrl',
[KeyShift] = 'Shift',
[KeyAlt] = 'Alt',
[KeyMeta] = 'Meta',
[KeyMenu] = 'Menu',
[KeySpace] = 'Space',
[KeyExclamation] = '!',
[KeyQuote] = '\"',
[KeyNumberSign] = '#',
[KeyDollar] = '$',
[KeyPercent] = '%',
[KeyAmpersand] = '&',
[KeyApostrophe] = '\'',
[KeyLeftParen] = '(',
[KeyRightParen] = ')',
[KeyAsterisk] = '*',
[KeyPlus] = 'Plus',
[KeyComma] = ',',
[KeyMinus] = '-',
[KeyPeriod] = '.',
[KeySlash] = '/',
[Key0] = '0',
[Key1] = '1',
[Key2] = '2',
[Key3] = '3',
[Key4] = '4',
[Key5] = '5',
[Key6] = '6',
[Key7] = '7',
[Key8] = '8',
[Key9] = '9',
[KeyColon] = ':',
[KeySemicolon] = ';',
[KeyLess] = '<',
[KeyEqual] = '=',
[KeyGreater] = '>',
[KeyQuestion] = '?',
[KeyAtSign] = '@',
[KeyA] = 'A',
[KeyB] = 'B',
[KeyC] = 'C',
[KeyD] = 'D',
[KeyE] = 'E',
[KeyF] = 'F',
[KeyG] = 'G',
[KeyH] = 'H',
[KeyI] = 'I',
[KeyJ] = 'J',
[KeyK] = 'K',
[KeyL] = 'L',
[KeyM] = 'M',
[KeyN] = 'N',
[KeyO] = 'O',
[KeyP] = 'P',
[KeyQ] = 'Q',
[KeyR] = 'R',
[KeyS] = 'S',
[KeyT] = 'T',
[KeyU] = 'U',
[KeyV] = 'V',
[KeyW] = 'W',
[KeyX] = 'X',
[KeyY] = 'Y',
[KeyZ] = 'Z',
[KeyLeftBracket] = '[',
[KeyBackslash] = '\\',
[KeyRightBracket] = ']',
[KeyCaret] = '^',
[KeyUnderscore] = '_',
[KeyGrave] = '`',
[KeyLeftCurly] = '{',
[KeyBar] = '|',
[KeyRightCurly] = '}',
[KeyTilde] = '~',
[KeyF1] = 'F1',
[KeyF2] = 'F2',
[KeyF3] = 'F3',
[KeyF4] = 'F4',
[KeyF5] = 'F5',
[KeyF6] = 'F6',
[KeyF7] = 'F7',
[KeyF8] = 'F8',
[KeyF9] = 'F9',
[KeyF10] = 'F10',
[KeyF11] = 'F11',
[KeyF12] = 'F12',
[KeyNumpad0] = 'Numpad0',
[KeyNumpad1] = 'Numpad1',
[KeyNumpad2] = 'Numpad2',
[KeyNumpad3] = 'Numpad3',
[KeyNumpad4] = 'Numpad4',
[KeyNumpad5] = 'Numpad5',
[KeyNumpad6] = 'Numpad6',
[KeyNumpad7] = 'Numpad7',
[KeyNumpad8] = 'Numpad8',
[KeyNumpad9] = 'Numpad9',
}
NetworkMessageTypes = {
Boolean = 1,
U8 = 2,
U16 = 3,
U32 = 4,
U64 = 5,
NumberString = 6,
String = 7,
Table = 8,
}
SoundChannels = {
Music = 1,
Ambient = 2,
Effect = 3,
Bot = 4
}

View File

@@ -0,0 +1,34 @@
Module
name: corelib
description: Contains core lua classes, functions and constants used by other modules
author: OTClient team
website: https://github.com/edubart/otclient
reloadable: false
@onLoad: |
dofile 'math'
dofile 'string'
dofile 'table'
dofile 'bitwise'
dofile 'struct'
dofile 'const'
dofile 'util'
dofile 'globals'
dofile 'config'
dofile 'settings'
dofile 'keyboard'
dofile 'mouse'
dofile 'net'
dofiles 'classes'
dofiles 'ui'
dofile 'inputmessage'
dofile 'outputmessage'
dofile 'orderedtable'
dofile 'base64'
dofile 'json'
dofile 'http'
dofile 'test'

View File

@@ -0,0 +1,76 @@
-- @docvars @{
-- root widget
rootWidget = g_ui.getRootWidget()
modules = package.loaded
-- G is used as a global table to save variables in memory between reloads
G = G or {}
-- @}
-- @docfuncs @{
function scheduleEvent(callback, delay)
local desc = "lua"
local info = debug.getinfo(2, "Sl")
if info then
desc = info.short_src .. ":" .. info.currentline
end
local event = g_dispatcher.scheduleEvent(desc, callback, delay)
-- must hold a reference to the callback, otherwise it would be collected
event._callback = callback
return event
end
function addEvent(callback, front)
local desc = "lua"
local info = debug.getinfo(2, "Sl")
if info then
desc = info.short_src .. ":" .. info.currentline
end
local event = g_dispatcher.addEvent(desc, callback, front)
-- must hold a reference to the callback, otherwise it would be collected
event._callback = callback
return event
end
function cycleEvent(callback, interval)
local desc = "lua"
local info = debug.getinfo(2, "Sl")
if info then
desc = info.short_src .. ":" .. info.currentline
end
local event = g_dispatcher.cycleEvent(desc, callback, interval)
-- must hold a reference to the callback, otherwise it would be collected
event._callback = callback
return event
end
function periodicalEvent(eventFunc, conditionFunc, delay, autoRepeatDelay)
delay = delay or 30
autoRepeatDelay = autoRepeatDelay or delay
local func
func = function()
if conditionFunc and not conditionFunc() then
func = nil
return
end
eventFunc()
scheduleEvent(func, delay)
end
scheduleEvent(function()
func()
end, autoRepeatDelay)
end
function removeEvent(event)
if event then
event:cancel()
event._callback = nil
end
end
-- @}

View File

@@ -0,0 +1,287 @@
HTTP = {
timeout=5,
websocketTimeout=15,
agent="Mozilla/5.0",
imageId=1000,
images={},
operations={},
}
function HTTP.get(url, callback)
if not g_http or not g_http.get then
return error("HTTP.get is not supported")
end
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", url=url, callback=callback}
return operation
end
function HTTP.getJSON(url, callback)
if not g_http or not g_http.get then
return error("HTTP.getJSON is not supported")
end
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback}
return operation
end
function HTTP.post(url, data, callback)
if not g_http or not g_http.post then
return error("HTTP.post is not supported")
end
if type(data) == "table" then
data = json.encode(data)
end
local operation = g_http.post(url, data, HTTP.timeout)
HTTP.operations[operation] = {type="post", url=url, callback=callback}
return operation
end
function HTTP.postJSON(url, data, callback)
if not g_http or not g_http.post then
return error("HTTP.postJSON is not supported")
end
if type(data) == "table" then
data = json.encode(data)
end
local operation = g_http.post(url, data, HTTP.timeout, true)
HTTP.operations[operation] = {type="post", json=true, url=url, callback=callback}
return operation
end
function HTTP.download(url, file, callback, progressCallback)
if not g_http or not g_http.download then
return error("HTTP.download is not supported")
end
local operation = g_http.download(url, file, HTTP.timeout)
HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback}
return operation
end
function HTTP.downloadImage(url, callback)
if not g_http or not g_http.download then
return error("HTTP.downloadImage is not supported")
end
if HTTP.images[url] ~= nil then
if callback then
callback('/downloads/' .. HTTP.images[url], nil)
end
return
end
local file = "autoimage_" .. HTTP.imageId .. ".png"
HTTP.imageId = HTTP.imageId + 1
local operation = g_http.download(url, file, HTTP.timeout)
HTTP.operations[operation] = {type="image", url=url, file=file, callback=callback}
return operation
end
function HTTP.webSocket(url, callbacks, timeout, jsonWebsocket)
if not g_http or not g_http.ws then
return error("WebSocket is not supported")
end
if not timeout or timeout < 1 then
timeout = HTTP.websocketTimeout
end
local operation = g_http.ws(url, timeout)
HTTP.operations[operation] = {type="ws", json=jsonWebsocket, url=url, callbacks=callbacks}
return {
id = operation,
url = url,
close = function()
g_http.wsClose(operation)
end,
send = function(message)
if type(message) == "table" then
message = json.encode(message)
end
g_http.wsSend(operation, message)
end
}
end
HTTP.WebSocket = HTTP.webSocket
function HTTP.webSocketJSON(url, callbacks, timeout)
return HTTP.webSocket(url, callbacks, timeout, true)
end
HTTP.WebSocketJSON = HTTP.webSocketJSON
function HTTP.cancel(operationId)
if not g_http or not g_http.cancel then
return
end
HTTP.operations[operationId] = nil
return g_http.cancel(operationId)
end
function HTTP.onGet(operationId, url, err, data)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if err and err:len() == 0 then
err = nil
end
if not err and operation.json then
if data:len() == 0 then
data = "null"
end
local status, result = pcall(function() return json.decode(data) end)
if not status then
err = "JSON ERROR: " .. result
if data and data:len() > 0 then
err = err .. " (" .. data:sub(1, 100) .. ")"
end
end
data = result
end
if operation.callback then
operation.callback(data, err)
end
end
function HTTP.onGetProgress(operationId, url, progress)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
end
function HTTP.onPost(operationId, url, err, data)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if err and err:len() == 0 then
err = nil
end
if not err and operation.json then
if data:len() == 0 then
data = "null"
end
local status, result = pcall(function() return json.decode(data) end)
if not status then
err = "JSON ERROR: " .. result
if data and data:len() > 0 then
err = err .. " (" .. data:sub(1, 100) .. ")"
end
end
data = result
end
if operation.callback then
operation.callback(data, err)
end
end
function HTTP.onPostProgress(operationId, url, progress)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
end
function HTTP.onDownload(operationId, url, err, path, checksum)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if err and err:len() == 0 then
err = nil
end
if operation.callback then
if operation["type"] == "image" then
if not err then
HTTP.images[url] = path
end
operation.callback('/downloads/' .. path, err)
else
operation.callback(path, checksum, err)
end
end
end
function HTTP.onDownloadProgress(operationId, url, progress, speed)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.progressCallback then
operation.progressCallback(progress, speed)
end
end
function HTTP.onWsOpen(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onOpen then
operation.callbacks.onOpen(message, operationId)
end
end
function HTTP.onWsMessage(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onMessage then
if operation.json then
if message:len() == 0 then
message = "null"
end
local status, result = pcall(function() return json.decode(message) end)
local err = nil
if not status then
err = "JSON ERROR: " .. result
if message and message:len() > 0 then
err = err .. " (" .. message:sub(1, 100) .. ")"
end
end
if err then
if operation.callbacks.onError then
operation.callbacks.onError(err, operationId)
end
else
operation.callbacks.onMessage(result, operationId)
end
else
operation.callbacks.onMessage(message, operationId)
end
end
end
function HTTP.onWsClose(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onClose then
operation.callbacks.onClose(message, operationId)
end
end
function HTTP.onWsError(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onError then
operation.callbacks.onError(message, operationId)
end
end
connect(g_http,
{
onGet = HTTP.onGet,
onGetProgress = HTTP.onGetProgress,
onPost = HTTP.onPost,
onPostProgress = HTTP.onPostProgress,
onDownload = HTTP.onDownload,
onDownloadProgress = HTTP.onDownloadProgress,
onWsOpen = HTTP.onWsOpen,
onWsMessage = HTTP.onWsMessage,
onWsClose = HTTP.onWsClose,
onWsError = HTTP.onWsError,
})
g_http.setUserAgent(HTTP.agent)

View File

@@ -0,0 +1,51 @@
function InputMessage:getData()
local dataType = self:getU8()
if dataType == NetworkMessageTypes.Boolean then
return numbertoboolean(self:getU8())
elseif dataType == NetworkMessageTypes.U8 then
return self:getU8()
elseif dataType == NetworkMessageTypes.U16 then
return self:getU16()
elseif dataType == NetworkMessageTypes.U32 then
return self:getU32()
elseif dataType == NetworkMessageTypes.U64 then
return self:getU64()
elseif dataType == NetworkMessageTypes.NumberString then
return tonumber(self:getString())
elseif dataType == NetworkMessageTypes.String then
return self:getString()
elseif dataType == NetworkMessageTypes.Table then
return self:getTable()
else
perror('Unknown data type ' .. dataType)
end
return nil
end
function InputMessage:getTable()
local ret = {}
local size = self:getU16()
for i=1,size do
local index = self:getData()
local value = self:getData()
ret[index] = value
end
return ret
end
function InputMessage:getColor()
local color = {}
color.r = self:getU8()
color.g = self:getU8()
color.b = self:getU8()
color.a = self:getU8()
return color
end
function InputMessage:getPosition()
local position = {}
position.x = self:getU16()
position.y = self:getU16()
position.z = self:getU8()
return position
end

View File

@@ -0,0 +1,419 @@
--
-- json.lua
--
-- Copyright (c) 2019 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
json = { _version = "0.1.1" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function make_indent(state)
return string.rep(" ", state.currentIndentLevel * state.indent)
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil()
return "null"
end
local function encode_table(val, state)
local res = {}
local stack = state.stack
local pretty = state.indent > 0
local close_indent = make_indent(state)
local comma = pretty and ",\n" or ","
local colon = pretty and ": " or ":"
local open_brace = pretty and "{\n" or "{"
local close_brace = pretty and ("\n" .. close_indent .. "}") or "}"
local open_bracket = pretty and "[\n" or "["
local close_bracket = pretty and ("\n" .. close_indent .. "]") or "]"
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for _, v in ipairs(val) do
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return open_bracket .. table.concat(res, comma) .. close_bracket
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(k, state) .. colon .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return open_brace .. table.concat(res, comma) .. close_brace
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, state)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, state)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val, indent)
local state = {
indent = indent or 0,
currentIndentLevel = 0,
stack = {}
}
return encode(val, state)
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end

View File

@@ -0,0 +1,251 @@
-- @docclass
g_keyboard = {}
-- private functions
function translateKeyCombo(keyCombo)
if not keyCombo or #keyCombo == 0 then return nil end
local keyComboDesc = ''
for k,v in pairs(keyCombo) do
local keyDesc = KeyCodeDescs[v]
if keyDesc == nil then return nil end
keyComboDesc = keyComboDesc .. '+' .. keyDesc
end
keyComboDesc = keyComboDesc:sub(2)
return keyComboDesc
end
local function getKeyCode(key)
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == key:trim():lower() then
return keyCode
end
end
end
function retranslateKeyComboDesc(keyComboDesc)
if keyComboDesc == nil then
error('Unable to translate key combo \'' .. keyComboDesc .. '\'')
end
if type(keyComboDesc) == 'number' then
keyComboDesc = tostring(keyComboDesc)
end
local keyCombo = {}
for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == currentKeyDesc:trim():lower() then
table.insert(keyCombo, keyCode)
end
end
end
return translateKeyCombo(keyCombo)
end
function determineKeyComboDesc(keyCode, keyboardModifiers)
local keyCombo = {}
if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then
table.insert(keyCombo, keyCode)
elseif KeyCodeDescs[keyCode] ~= nil then
if keyboardModifiers == KeyboardCtrlModifier then
table.insert(keyCombo, KeyCtrl)
elseif keyboardModifiers == KeyboardAltModifier then
table.insert(keyCombo, KeyAlt)
elseif keyboardModifiers == KeyboardCtrlAltModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyAlt)
elseif keyboardModifiers == KeyboardShiftModifier then
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardCtrlShiftModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardAltShiftModifier then
table.insert(keyCombo, KeyAlt)
table.insert(keyCombo, KeyShift)
elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then
table.insert(keyCombo, KeyCtrl)
table.insert(keyCombo, KeyAlt)
table.insert(keyCombo, KeyShift)
end
table.insert(keyCombo, keyCode)
end
return translateKeyCombo(keyCombo)
end
local function onWidgetKeyDown(widget, keyCode, keyboardModifiers)
if keyCode == KeyUnknown then return false end
local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
signalcall(callback, widget, keyCode)
callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
return signalcall(callback, widget, keyCode)
end
local function onWidgetKeyUp(widget, keyCode, keyboardModifiers)
if keyCode == KeyUnknown then return false end
local callback = widget.boundAloneKeyUpCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
signalcall(callback, widget, keyCode)
callback = widget.boundKeyUpCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
return signalcall(callback, widget, keyCode)
end
local function onWidgetKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks)
if keyCode == KeyUnknown then return false end
local callback = widget.boundKeyPressCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
return signalcall(callback, widget, keyCode, autoRepeatTicks)
end
local function connectKeyDownEvent(widget)
if widget.boundKeyDownCombos then return end
connect(widget, { onKeyDown = onWidgetKeyDown })
widget.boundKeyDownCombos = {}
widget.boundAloneKeyDownCombos = {}
end
local function connectKeyUpEvent(widget)
if widget.boundKeyUpCombos then return end
connect(widget, { onKeyUp = onWidgetKeyUp })
widget.boundKeyUpCombos = {}
widget.boundAloneKeyUpCombos = {}
end
local function connectKeyPressEvent(widget)
if widget.boundKeyPressCombos then return end
connect(widget, { onKeyPress = onWidgetKeyPress })
widget.boundKeyPressCombos = {}
end
-- public functions
function g_keyboard.bindKeyDown(keyComboDesc, callback, widget, alone)
widget = widget or rootWidget
connectKeyDownEvent(widget)
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
if alone then
connect(widget.boundAloneKeyDownCombos, keyComboDesc, callback)
else
connect(widget.boundKeyDownCombos, keyComboDesc, callback)
end
end
function g_keyboard.bindKeyUp(keyComboDesc, callback, widget, alone)
widget = widget or rootWidget
connectKeyUpEvent(widget)
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
if alone then
connect(widget.boundAloneKeyUpCombos, keyComboDesc, callback)
else
connect(widget.boundKeyUpCombos, keyComboDesc, callback)
end
end
function g_keyboard.bindKeyPress(keyComboDesc, callback, widget)
widget = widget or rootWidget
connectKeyPressEvent(widget)
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
connect(widget.boundKeyPressCombos, keyComboDesc, callback)
end
local function getUnbindArgs(arg1, arg2)
local callback
local widget
if type(arg1) == 'function' then callback = arg1
elseif type(arg2) == 'function' then callback = arg2 end
if type(arg1) == 'userdata' then widget = arg1
elseif type(arg2) == 'userdata' then widget = arg2 end
widget = widget or rootWidget
return callback, widget
end
function g_keyboard.unbindKeyDown(keyComboDesc, arg1, arg2)
local callback, widget = getUnbindArgs(arg1, arg2)
if widget.boundKeyDownCombos == nil then return end
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
disconnect(widget.boundKeyDownCombos, keyComboDesc, callback)
end
function g_keyboard.unbindKeyUp(keyComboDesc, arg1, arg2)
local callback, widget = getUnbindArgs(arg1, arg2)
if widget.boundKeyUpCombos == nil then return end
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
disconnect(widget.boundKeyUpCombos, keyComboDesc, callback)
end
function g_keyboard.unbindKeyPress(keyComboDesc, arg1, arg2)
local callback, widget = getUnbindArgs(arg1, arg2)
if widget.boundKeyPressCombos == nil then return end
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
disconnect(widget.boundKeyPressCombos, keyComboDesc, callback)
end
function g_keyboard.getModifiers()
return g_window.getKeyboardModifiers()
end
function g_keyboard.isKeyPressed(key)
if type(key) == 'string' then
key = getKeyCode(key)
end
return g_window.isKeyPressed(key)
end
function g_keyboard.areKeysPressed(keyComboDesc)
for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == currentKeyDesc:trim():lower() then
if keyDesc:lower() == "ctrl" then
if not g_keyboard.isCtrlPressed() then
return false
end
elseif keyDesc:lower() == "shift" then
if not g_keyboard.isShiftPressed() then
return false
end
elseif keyDesc:lower() == "alt" then
if not g_keyboard.isAltPressed() then
return false
end
elseif not g_window.isKeyPressed(keyCode) then
return false
end
end
end
end
return true
end
function g_keyboard.isKeySetPressed(keys, all)
all = all or false
local result = {}
for k,v in pairs(keys) do
if type(v) == 'string' then
v = getKeyCode(v)
end
if g_window.isKeyPressed(v) then
if not all then
return true
end
table.insert(result, true)
end
end
return #result == #keys
end
function g_keyboard.isInUse()
for i = FirstKey, LastKey do
if g_window.isKeyPressed(key) then
return true
end
end
return false
end
function g_keyboard.isCtrlPressed()
return bit32.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0
end
function g_keyboard.isAltPressed()
return bit32.band(g_window.getKeyboardModifiers(), KeyboardAltModifier) ~= 0
end
function g_keyboard.isShiftPressed()
return bit32.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0
end

View File

@@ -0,0 +1,35 @@
-- @docclass math
local U8 = 2^8
local U16 = 2^16
local U32 = 2^32
local U64 = 2^64
function math.round(num, idp)
local mult = 10^(idp or 0)
if num >= 0 then
return math.floor(num * mult + 0.5) / mult
else
return math.ceil(num * mult - 0.5) / mult
end
end
function math.isu8(num)
return math.isinteger(num) and num >= 0 and num < U8
end
function math.isu16(num)
return math.isinteger(num) and num >= U8 and num < U16
end
function math.isu32(num)
return math.isinteger(num) and num >= U16 and num < U32
end
function math.isu64(num)
return math.isinteger(num) and num >= U32 and num < U64
end
function math.isinteger(num)
return ((type(num) == 'number') and (num == math.floor(num)))
end

View File

@@ -0,0 +1,36 @@
-- @docclass
function g_mouse.bindAutoPress(widget, callback, delay, button)
local button = button or MouseLeftButton
connect(widget, { onMousePress = function(widget, mousePos, mouseButton)
if mouseButton ~= button then
return false
end
local startTime = g_clock.millis()
callback(widget, mousePos, mouseButton, 0)
periodicalEvent(function()
callback(widget, g_window.getMousePosition(), mouseButton, g_clock.millis() - startTime)
end, function()
return g_mouse.isPressed(mouseButton)
end, 30, delay)
return true
end })
end
function g_mouse.bindPressMove(widget, callback)
connect(widget, { onMouseMove = function(widget, mousePos, mouseMoved)
if widget:isPressed() then
callback(mousePos, mouseMoved)
return true
end
end })
end
function g_mouse.bindPress(widget, callback, button)
connect(widget, { onMousePress = function(widget, mousePos, mouseButton)
if not button or button == mouseButton then
callback(mousePos, mouseButton)
return true
end
return false
end })
end

View File

@@ -0,0 +1,16 @@
function translateNetworkError(errcode, connecting, errdesc)
local text
if errcode == 111 then
text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.')
elseif errcode == 110 then
text = tr('Connection timed out. Either your network is failing or the server is offline.')
elseif errcode == 1 then
text = tr('Connection failed, the server address does not exist.')
elseif connecting then
text = tr('Connection failed.')
else
text = tr('Your connection has been lost.\nEither your network or the server went down.')
end
text = text .. ' ' .. tr('(ERROR %d)', errcode)
return text
end

View File

@@ -0,0 +1,43 @@
function __genOrderedIndex( t )
local orderedIndex = {}
for key in pairs(t) do
table.insert( orderedIndex, key )
end
table.sort( orderedIndex )
return orderedIndex
end
function orderedNext(t, state)
-- Equivalent of the next function, but returns the keys in the alphabetic
-- order. We use a temporary ordered key table that is stored in the
-- table being iterated.
local key = nil
--print("orderedNext: state = "..tostring(state) )
if state == nil then
-- the first time, generate the index
t.__orderedIndex = __genOrderedIndex( t )
key = t.__orderedIndex[1]
else
-- fetch the next value
for i = 1,table.getn(t.__orderedIndex) do
if t.__orderedIndex[i] == state then
key = t.__orderedIndex[i+1]
end
end
end
if key then
return key, t[key]
end
-- no more value to return, cleanup
t.__orderedIndex = nil
return
end
function orderedPairs(t)
-- Equivalent of the pairs() function on tables. Allows to iterate
-- in order
return orderedNext, t, nil
end

View File

@@ -0,0 +1,69 @@
function OutputMessage:addData(data)
if type(data) == 'boolean' then
self:addU8(NetworkMessageTypes.Boolean)
self:addU8(booleantonumber(data))
elseif type(data) == 'number' then
if math.isu8(data) then
self:addU8(NetworkMessageTypes.U8)
self:addU8(data)
elseif math.isu16(data) then
self:addU8(NetworkMessageTypes.U16)
self:addU16(data)
elseif math.isu32(data) then
self:addU8(NetworkMessageTypes.U32)
self:addU32(data)
elseif math.isu64(data) then
self:addU8(NetworkMessageTypes.U64)
self:addU64(data)
else -- negative or non integer numbers
self:addU8(NetworkMessageTypes.NumberString)
self:addString(tostring(data))
end
elseif type(data) == 'string' then
self:addU8(NetworkMessageTypes.String)
self:addString(data)
elseif type(data) == 'table' then
self:addU8(NetworkMessageTypes.Table)
self:addTable(data)
else
perror('Invalid data type ' .. type(data))
end
end
function OutputMessage:addTable(data)
local size = 0
-- reserve for size (should be addData, find a way to use it further)
local sizePos = self:getWritePos()
self:addU16(size)
local sizeSize = self:getWritePos() - sizePos
-- add values
for key,value in pairs(data) do
self:addData(key)
self:addData(value)
size = size + 1
end
-- write size
local currentPos = self:getWritePos()
self:setWritePos(sizePos)
self:addU16(size)
-- fix msg size and go back to end
self:setMessageSize(self:getMessageSize() - sizeSize)
self:setWritePos(currentPos)
end
function OutputMessage:addColor(color)
self:addU8(color.r)
self:addU8(color.g)
self:addU8(color.b)
self:addU8(color.a)
end
function OutputMessage:addPosition(position)
self:addU16(position.x)
self:addU16(position.y)
self:addU8(position.z)
end

View File

@@ -0,0 +1,3 @@
g_settings = makesingleton(g_configs.getSettings())
-- Reserved for future functionality

View File

@@ -0,0 +1,59 @@
-- @docclass string
function string:split(delim)
local start = 1
local results = {}
while true do
local pos = string.find(self, delim, start, true)
if not pos then
break
end
table.insert(results, string.sub(self, start, pos-1))
start = pos + string.len(delim)
end
table.insert(results, string.sub(self, start))
table.removevalue(results, '')
return results
end
function string:starts(start)
return string.sub(self, 1, #start) == start
end
function string:ends(test)
return test =='' or string.sub(self,-string.len(test)) == test
end
function string:trim()
return string.match(self, '^%s*(.*%S)') or ''
end
function string:explode(sep, limit)
if type(sep) ~= 'string' or tostring(self):len() == 0 or sep:len() == 0 then
return {}
end
local i, pos, tmp, t = 0, 1, "", {}
for s, e in function() return string.find(self, sep, pos) end do
tmp = self:sub(pos, s - 1):trim()
table.insert(t, tmp)
pos = e + 1
i = i + 1
if limit ~= nil and i == limit then
break
end
end
tmp = self:sub(pos):trim()
table.insert(t, tmp)
return t
end
function string:contains(str, checkCase, start, plain)
if(not checkCase) then
self = self:lower()
str = str:lower()
end
return string.find(self, str, start and start or 1, plain == nil and true or false)
end

View File

@@ -0,0 +1,173 @@
Struct = {}
function Struct.pack(format, ...)
local stream = {}
local vars = {...}
local endianness = true
for i = 1, format:len() do
local opt = format:sub(i, i)
if opt == '>' then
endianness = false
elseif opt:find('[bBhHiIlL]') then
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
local val = tonumber(table.remove(vars, 1))
if val < 0 then
val = val + 2 ^ (n * 8 - 1)
end
local bytes = {}
for j = 1, n do
table.insert(bytes, string.char(val % (2 ^ 8)))
val = math.floor(val / (2 ^ 8))
end
if not endianness then
table.insert(stream, string.reverse(table.concat(bytes)))
else
table.insert(stream, table.concat(bytes))
end
elseif opt:find('[fd]') then
local val = tonumber(table.remove(vars, 1))
local sign = 0
if val < 0 then
sign = 1
val = -val
end
local mantissa, exponent = math.frexp(val)
if val == 0 then
mantissa = 0
exponent = 0
else
mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24)
exponent = exponent + ((opt == 'd') and 1022 or 126)
end
local bytes = {}
if opt == 'd' then
val = mantissa
for i = 1, 6 do
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
val = math.floor(val / (2 ^ 8))
end
else
table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8)))
val = math.floor(mantissa / (2 ^ 8))
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
val = math.floor(val / (2 ^ 8))
end
table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8)))
val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8))
table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8)))
val = math.floor((sign * 128 + val) / (2 ^ 8))
if not endianness then
table.insert(stream, string.reverse(table.concat(bytes)))
else
table.insert(stream, table.concat(bytes))
end
elseif opt == 's' then
table.insert(stream, tostring(table.remove(vars, 1)))
table.insert(stream, string.char(0))
elseif opt == 'c' then
local n = format:sub(i + 1):match('%d+')
local length = tonumber(n)
if length > 0 then
local str = tostring(table.remove(vars, 1))
if length - str:len() > 0 then
str = str .. string.rep(' ', length - str:len())
end
table.insert(stream, str:sub(1, length))
end
i = i + n:len()
end
end
return table.concat(stream)
end
function Struct.unpack(format, stream)
local vars = {}
local iterator = 1
local endianness = true
for i = 1, format:len() do
local opt = format:sub(i, i)
if opt == '>' then
endianness = false
elseif opt:find('[bBhHiIlL]') then
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
local signed = opt:lower() == opt
local val = 0
for j = 1, n do
local byte = string.byte(stream:sub(iterator, iterator))
if endianness then
val = val + byte * (2 ^ ((j - 1) * 8))
else
val = val + byte * (2 ^ ((n - j) * 8))
end
iterator = iterator + 1
end
if signed then
val = val - 2 ^ (n * 8 - 1)
end
table.insert(vars, val)
elseif opt:find('[fd]') then
local n = (opt == 'd') and 8 or 4
local x = stream:sub(iterator, iterator + n - 1)
iterator = iterator + n
if not endianness then
x = string.reverse(x)
end
local sign = 1
local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128)
for i = n - 2, 1, -1 do
mantissa = mantissa * (2 ^ 8) + string.byte(x, i)
end
if string.byte(x, n) > 127 then
sign = -1
end
local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128))
if exponent == 0 then
table.insert(vars, 0.0)
else
mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign
table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127)))
end
elseif opt == 's' then
local bytes = {}
for j = iterator, stream:len() do
if stream:sub(j, j) == string.char(0) then
break
end
table.insert(bytes, stream:sub(j, j))
end
local str = table.concat(bytes)
iterator = iterator + str:len() + 1
table.insert(vars, str)
elseif opt == 'c' then
local n = format:sub(i + 1):match('%d+')
table.insert(vars, stream:sub(iterator, iterator + tonumber(n)))
iterator = iterator + tonumber(n)
i = i + n:len()
end
end
return unpack(vars)
end

View File

@@ -0,0 +1,287 @@
-- @docclass table
function table.dump(t, depth)
if not depth then depth = 0 end
for k,v in pairs(t) do
str = (' '):rep(depth * 2) .. k .. ': '
if type(v) ~= "table" then
print(str .. tostring(v))
else
print(str)
table.dump(v, depth+1)
end
end
end
function table.clear(t)
for k,v in pairs(t) do
t[k] = nil
end
end
function table.copy(t)
local res = {}
for k,v in pairs(t) do
res[k] = v
end
return res
end
function table.recursivecopy(t)
local res = {}
for k,v in pairs(t) do
if type(v) == "table" then
res[k] = table.recursivecopy(v)
else
res[k] = v
end
end
return res
end
function table.selectivecopy(t, keys)
local res = { }
for i,v in ipairs(keys) do
res[v] = t[v]
end
return res
end
function table.merge(t, src)
for k,v in pairs(src) do
t[k] = v
end
end
function table.find(t, value, lowercase)
for k,v in pairs(t) do
if lowercase and type(value) == 'string' and type(v) == 'string' then
if v:lower() == value:lower() then return k end
end
if v == value then return k end
end
end
function table.findbykey(t, key, lowercase)
for k,v in pairs(t) do
if lowercase and type(key) == 'string' and type(k) == 'string' then
if k:lower() == key:lower() then return v end
end
if k == key then return v end
end
end
function table.contains(t, value, lowercase)
return table.find(t, value, lowercase) ~= nil
end
function table.findkey(t, key)
if t and type(t) == 'table' then
for k,v in pairs(t) do
if k == key then return k end
end
end
end
function table.haskey(t, key)
return table.findkey(t, key) ~= nil
end
function table.removevalue(t, value)
for k,v in pairs(t) do
if v == value then
table.remove(t, k)
return true
end
end
return false
end
function table.popvalue(value)
local index = nil
for k,v in pairs(t) do
if v == value or not value then
index = k
end
end
if index then
table.remove(t, index)
return true
end
return false
end
function table.compare(t, other)
if #t ~= #other then return false end
for k,v in pairs(t) do
if v ~= other[k] then return false end
end
return true
end
function table.empty(t)
if t and type(t) == 'table' then
return next(t) == nil
end
return true
end
function table.permute(t, n, count)
n = n or #t
for i=1,count or n do
local j = math.random(i, n)
t[i], t[j] = t[j], t[i]
end
return t
end
function table.findbyfield(t, fieldname, fieldvalue)
for _i,subt in pairs(t) do
if subt[fieldname] == fieldvalue then
return subt
end
end
return nil
end
function table.size(t)
local size = 0
for i, n in pairs(t) do
size = size + 1
end
return size
end
function table.tostring(t)
local maxn = #t
local str = ""
for k,v in pairs(t) do
v = tostring(v)
if k == maxn and k ~= 1 then
str = str .. " and " .. v
elseif maxn > 1 and k ~= 1 then
str = str .. ", " .. v
else
str = str .. " " .. v
end
end
return str
end
function table.collect(t, func)
local res = {}
for k,v in pairs(t) do
local a,b = func(k,v)
if a and b then
res[a] = b
elseif a ~= nil then
table.insert(res,a)
end
end
return res
end
function table.equals(t, comp)
if type(t) == "table" and type(comp) == "table" then
for k,v in pairs(t) do
if v ~= comp[k] then return false end
end
end
return true
end
function table.equal(t1,t2,ignore_mt)
local ty1 = type(t1)
local ty2 = type(t2)
if ty1 ~= ty2 then return false end
-- non-table types can be directly compared
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
-- as well as tables which have the metamethod __eq
local mt = getmetatable(t1)
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
for k1,v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not table.equal(v1,v2) then return false end
end
for k2,v2 in pairs(t2) do
local v1 = t1[k2]
if v1 == nil or not table.equal(v1,v2) then return false end
end
return true
end
function table.isList(t)
local size = #t
return table.size(t) == size and size > 0
end
function table.isStringList(t)
if not table.isList(t) then return false end
for k,v in ipairs(t) do
if type(v) ~= 'string' then
return false
end
end
return true
end
function table.isStringPairList(t)
if not table.isList(t) then return false end
for k,v in ipairs(t) do
if type(v) ~= 'table' or #v ~= 2 or type(v[1]) ~= 'string' or type(v[2]) ~= 'string' then
return false
end
end
return true
end
function table.encodeStringPairList(t)
local ret = ""
for k,v in ipairs(t) do
if v[2]:find("\n") then
ret = ret .. v[1] .. ":[[\n" .. v[2] .. "\n]]\n"
else
ret = ret .. v[1] .. ":" .. v[2] .. "\n"
end
end
return ret
end
function table.decodeStringPairList(l)
local ret = {}
local r = regexMatch(l, "(?:^|\\n)([^:^\n]{1,20}):?(.*)(?:$|\\n)")
local multiline = ""
local multilineKey = ""
local multilineActive = false
for k,v in ipairs(r) do
if multilineActive then
local endPos = v[1]:find("%]%]")
if endPos then
if endPos > 1 then
table.insert(ret, {multilineKey, multiline .. "\n" .. v[1]:sub(1, endPos - 1)})
else
table.insert(ret, {multilineKey, multiline})
end
multilineActive = false
multiline = ""
multilineKey = ""
else
if multiline:len() == 0 then
multiline = v[1]
else
multiline = multiline .. "\n" .. v[1]
end
end
else
local bracketPos = v[3]:find("%[%[")
if bracketPos == 1 then -- multiline begin
multiline = v[3]:sub(bracketPos + 2)
multilineActive = true
multilineKey = v[2]
elseif v[2]:len() > 0 and v[3]:len() > 0 then
table.insert(ret, {v[2], v[3]})
end
end
end
return ret

View File

@@ -0,0 +1,62 @@
Test = {
tests = {},
activeTest = 0,
screenShot = 1
}
Test.Test = function(name, func)
local testId = #Test.tests + 1
Test.tests[testId] = {
name = name,
actions = {},
delay = 0,
start = 0
}
local test = function(testFunc)
table.insert(Test.tests[testId].actions, {type = "test", value = testFunc})
end
local wait = function(millis)
Test.tests[testId].delay = Test.tests[testId].delay + millis
table.insert(Test.tests[testId].actions, {type = "wait", value = Test.tests[testId].delay})
end
local ss = function()
table.insert(Test.tests[testId].actions, {type = "screenshot"})
end
local fail = function(message)
g_logger.fatal("Test " .. name .. " failed: " .. message)
end
func(test, wait, ss, fail)
end
Test.run = function()
if Test.activeTest > #Test.tests then
g_logger.info("[TEST] Finished tests. Exiting...")
return g_app.exit()
end
local test = Test.tests[Test.activeTest]
if not test or #test.actions == 0 then
Test.activeTest = Test.activeTest + 1
local nextTest = Test.tests[Test.activeTest]
if nextTest then
nextTest.start = g_clock.millis()
g_logger.info("[TEST] Starting test: " .. nextTest.name)
end
return scheduleEvent(Test.run, 500)
end
local action = test.actions[1]
if action.type == "test" then
table.remove(test.actions, 1)
action.value()
elseif action.type == "screenshot" then
table.remove(test.actions, 1)
g_app.doScreenshot(Test.screenShot .. ".png")
Test.screenShot = Test.screenShot + 1
elseif action.type == "wait" then
if action.value + test.start < g_clock.millis() then
table.remove(test.actions, 1)
end
end
scheduleEvent(Test.run, 100)
end

View File

@@ -0,0 +1,67 @@
-- @docclass
g_effects = {}
function g_effects.fadeIn(widget, time, elapsed)
if not elapsed then elapsed = 0 end
if not time then time = 300 end
widget:setOpacity(math.min(elapsed/time, 1))
removeEvent(widget.fadeEvent)
if elapsed < time then
removeEvent(widget.fadeEvent)
widget.fadeEvent = scheduleEvent(function()
g_effects.fadeIn(widget, time, elapsed + 30)
end, 30)
else
widget.fadeEvent = nil
end
end
function g_effects.fadeOut(widget, time, elapsed)
if not elapsed then elapsed = 0 end
if not time then time = 300 end
elapsed = math.max((1 - widget:getOpacity()) * time, elapsed)
removeEvent(widget.fadeEvent)
widget:setOpacity(math.max((time - elapsed)/time, 0))
if elapsed < time then
widget.fadeEvent = scheduleEvent(function()
g_effects.fadeOut(widget, time, elapsed + 30)
end, 30)
else
widget.fadeEvent = nil
end
end
function g_effects.cancelFade(widget)
removeEvent(widget.fadeEvent)
widget.fadeEvent = nil
end
function g_effects.startBlink(widget, duration, interval, clickCancel)
duration = duration or 0 -- until stop is called
interval = interval or 500
clickCancel = clickCancel or true
removeEvent(widget.blinkEvent)
removeEvent(widget.blinkStopEvent)
widget.blinkEvent = cycleEvent(function()
widget:setOn(not widget:isOn())
end, interval)
if duration > 0 then
widget.blinkStopEvent = scheduleEvent(function()
g_effects.stopBlink(widget)
end, duration)
end
connect(widget, { onClick = g_effects.stopBlink })
end
function g_effects.stopBlink(widget)
disconnect(widget, { onClick = g_effects.stopBlink })
removeEvent(widget.blinkEvent)
removeEvent(widget.blinkStopEvent)
widget.blinkEvent = nil
widget.blinkStopEvent = nil
widget:setOn(false)
end

View File

@@ -0,0 +1,124 @@
-- @docclass
g_tooltip = {}
-- private variables
local toolTipLabel
local currentHoveredWidget
-- private functions
local function moveToolTip(first)
if not first and (not toolTipLabel:isVisible() or toolTipLabel:getOpacity() < 0.1) then return end
local pos = g_window.getMousePosition()
local windowSize = g_window.getSize()
local labelSize = toolTipLabel:getSize()
pos.x = pos.x + 1
pos.y = pos.y + 1
if windowSize.width - (pos.x + labelSize.width) < 10 then
pos.x = pos.x - labelSize.width - 3
else
pos.x = pos.x + 10
end
if windowSize.height - (pos.y + labelSize.height) < 10 then
pos.y = pos.y - labelSize.height - 3
else
pos.y = pos.y + 10
end
toolTipLabel:setPosition(pos)
end
local function onWidgetHoverChange(widget, hovered)
if hovered then
if widget.tooltip and not g_mouse.isPressed() then
g_tooltip.display(widget.tooltip)
currentHoveredWidget = widget
end
else
if widget == currentHoveredWidget then
g_tooltip.hide()
currentHoveredWidget = nil
end
end
end
local function onWidgetStyleApply(widget, styleName, styleNode)
if styleNode.tooltip then
widget.tooltip = styleNode.tooltip
end
end
-- public functions
function g_tooltip.init()
connect(UIWidget, { onStyleApply = onWidgetStyleApply,
onHoverChange = onWidgetHoverChange})
addEvent(function()
toolTipLabel = g_ui.createWidget('UILabel', rootWidget)
toolTipLabel:setId('toolTip')
toolTipLabel:setBackgroundColor('#111111cc')
toolTipLabel:setTextAlign(AlignCenter)
toolTipLabel:hide()
end)
end
function g_tooltip.terminate()
disconnect(UIWidget, { onStyleApply = onWidgetStyleApply,
onHoverChange = onWidgetHoverChange })
currentHoveredWidget = nil
toolTipLabel:destroy()
toolTipLabel = nil
g_tooltip = nil
end
function g_tooltip.display(text)
if text == nil or text:len() == 0 then return end
if not toolTipLabel then return end
toolTipLabel:setText(text)
toolTipLabel:resizeToText()
toolTipLabel:resize(toolTipLabel:getWidth() + 4, toolTipLabel:getHeight() + 4)
toolTipLabel:show()
toolTipLabel:raise()
toolTipLabel:enable()
g_effects.fadeIn(toolTipLabel, 100)
moveToolTip(true)
connect(rootWidget, {
onMouseMove = moveToolTip,
})
end
function g_tooltip.hide()
g_effects.fadeOut(toolTipLabel, 100)
disconnect(rootWidget, {
onMouseMove = moveToolTip,
})
end
-- @docclass UIWidget @{
-- UIWidget extensions
function UIWidget:setTooltip(text)
self.tooltip = text
end
function UIWidget:removeTooltip()
self.tooltip = nil
end
function UIWidget:getTooltip()
return self.tooltip
end
-- @}
g_tooltip.init()
connect(g_app, { onTerminate = g_tooltip.terminate })

View File

@@ -0,0 +1,12 @@
-- @docclass
UIButton = extends(UIWidget, "UIButton")
function UIButton.create()
local button = UIButton.internalCreate()
button:setFocusable(false)
return button
end
function UIButton:onMouseRelease(pos, button)
return self:isPressed()
end

View File

@@ -0,0 +1,13 @@
-- @docclass
UICheckBox = extends(UIWidget, "UICheckBox")
function UICheckBox.create()
local checkbox = UICheckBox.internalCreate()
checkbox:setFocusable(false)
checkbox:setTextAlign(AlignLeft)
return checkbox
end
function UICheckBox:onClick()
self:setChecked(not self:isChecked())
end

View File

@@ -0,0 +1,184 @@
-- @docclass
UIComboBox = extends(UIWidget, "UIComboBox")
function UIComboBox.create()
local combobox = UIComboBox.internalCreate()
combobox:setFocusable(false)
combobox.options = {}
combobox.currentIndex = -1
combobox.mouseScroll = true
combobox.menuScroll = false
combobox.menuHeight = 100
combobox.menuScrollStep = 0
return combobox
end
function UIComboBox:clearOptions()
self.options = {}
self.currentIndex = -1
self:clearText()
end
function UIComboBox:clear()
return self:clearOptions()
end
function UIComboBox:getOptionsCount()
return #self.options
end
function UIComboBox:isOption(text)
if not self.options then return false end
for i,v in ipairs(self.options) do
if v.text == text then
return true
end
end
return false
end
function UIComboBox:setOption(text, dontSignal)
self:setCurrentOption(text, dontSignal)
end
function UIComboBox:setCurrentOption(text, dontSignal)
if not self.options then return end
for i,v in ipairs(self.options) do
if v.text == text and self.currentIndex ~= i then
self.currentIndex = i
self:setText(text)
if not dontSignal then
signalcall(self.onOptionChange, self, text, v.data)
end
return
end
end
end
function UIComboBox:updateCurrentOption(newText)
self.options[self.currentIndex].text = newText
self:setText(newText)
end
function UIComboBox:setCurrentOptionByData(data, dontSignal)
if not self.options then return end
for i,v in ipairs(self.options) do
if v.data == data and self.currentIndex ~= i then
self.currentIndex = i
self:setText(v.text)
if not dontSignal then
signalcall(self.onOptionChange, self, v.text, v.data)
end
return
end
end
end
function UIComboBox:setCurrentIndex(index, dontSignal)
if index >= 1 and index <= #self.options then
local v = self.options[index]
self.currentIndex = index
self:setText(v.text)
if not dontSignal then
signalcall(self.onOptionChange, self, v.text, v.data)
end
end
end
function UIComboBox:getCurrentOption()
if table.haskey(self.options, self.currentIndex) then
return self.options[self.currentIndex]
end
end
function UIComboBox:addOption(text, data)
table.insert(self.options, { text = text, data = data })
local index = #self.options
if index == 1 then self:setCurrentOption(text) end
return index
end
function UIComboBox:removeOption(text)
for i,v in ipairs(self.options) do
if v.text == text then
table.remove(self.options, i)
if self.currentIndex == i then
self:setCurrentIndex(1)
elseif self.currentIndex > i then
self.currentIndex = self.currentIndex - 1
end
return
end
end
end
function UIComboBox:onMousePress(mousePos, mouseButton)
local menu
if self.menuScroll then
menu = g_ui.createWidget(self:getStyleName() .. 'PopupScrollMenu')
menu:setHeight(self.menuHeight)
if self.menuScrollStep > 0 then
menu:setScrollbarStep(self.menuScrollStep)
end
else
menu = g_ui.createWidget(self:getStyleName() .. 'PopupMenu')
end
menu:setId(self:getId() .. 'PopupMenu')
for i,v in ipairs(self.options) do
menu:addOption(v.text, function() self:setCurrentOption(v.text) end)
end
menu:setWidth(self:getWidth())
menu:display({ x = self:getX(), y = self:getY() + self:getHeight() })
connect(menu, { onDestroy = function() self:setOn(false) end })
self:setOn(true)
return true
end
function UIComboBox:onMouseWheel(mousePos, direction)
if not self.mouseScroll or self.disableScroll then
return false
end
if direction == MouseWheelUp and self.currentIndex > 1 then
self:setCurrentIndex(self.currentIndex - 1)
elseif direction == MouseWheelDown and self.currentIndex < #self.options then
self:setCurrentIndex(self.currentIndex + 1)
end
return true
end
function UIComboBox:onStyleApply(styleName, styleNode)
if styleNode.options then
for k,option in pairs(styleNode.options) do
self:addOption(option)
end
end
if styleNode.data then
for k,data in pairs(styleNode.data) do
local option = self.options[k]
if option then
option.data = data
end
end
end
for name,value in pairs(styleNode) do
if name == 'mouse-scroll' then
self.mouseScroll = value
elseif name == 'menu-scroll' then
self.menuScroll = value
elseif name == 'menu-height' then
self.menuHeight = value
elseif name == 'menu-scroll-step' then
self.menuScrollStep = value
end
end
end
function UIComboBox:setMouseScroll(scroll)
self.mouseScroll = scroll
end
function UIComboBox:canMouseScroll()
return self.mouseScroll
end

View File

@@ -0,0 +1,99 @@
-- @docclass
UIImageView = extends(UIWidget, "UIImageView")
function UIImageView.create()
local imageView = UIImageView.internalCreate()
imageView.zoom = 1
imageView.minZoom = math.pow(10, -2)
imageView.maxZoom = math.pow(10, 2)
imageView:setClipping(true)
return imageView
end
function UIImageView:getDefaultZoom()
local width = self:getWidth()
local height = self:getHeight()
local textureWidth = self:getImageTextureWidth()
local textureHeight = self:getImageTextureHeight()
local zoomX = width / textureWidth
local zoomY = height / textureHeight
return math.min(zoomX, zoomY)
end
function UIImageView:getImagePosition(x, y)
x = x or self:getWidth() / 2
y = y or self:getHeight() / 2
local offsetX = self:getImageOffsetX()
local offsetY = self:getImageOffsetY()
local posX = (x - offsetX) / self.zoom
local posY = (y - offsetY) / self.zoom
return posX, posY
end
function UIImageView:setImage(image)
self:setImageSource(image)
local zoom = self:getDefaultZoom()
self:setZoom(zoom)
self:center()
end
function UIImageView:setZoom(zoom, x, y)
zoom = math.max(math.min(zoom, self.maxZoom), self.minZoom)
local posX, posY = self:getImagePosition(x, y)
local textureWidth = self:getImageTextureWidth()
local textureHeight = self:getImageTextureHeight()
local imageWidth = textureWidth * zoom
local imageHeight = textureHeight * zoom
self:setImageWidth(imageWidth)
self:setImageHeight(imageHeight)
self.zoom = zoom
self:move(posX, posY, x, y)
end
function UIImageView:zoomIn(x, y)
local zoom = self.zoom * 1.1
self:setZoom(zoom, x, y)
end
function UIImageView:zoomOut(x, y)
local zoom = self.zoom / 1.1
self:setZoom(zoom, x, y)
end
function UIImageView:center()
self:move(self:getImageTextureWidth() / 2, self:getImageTextureHeight() / 2)
end
function UIImageView:move(x, y, centerX, centerY)
x = math.max(math.min(x, self:getImageTextureWidth()), 0)
y = math.max(math.min(y, self:getImageTextureHeight()), 0)
local centerX = centerX or self:getWidth() / 2
local centerY = centerY or self:getHeight() / 2
local offsetX = centerX - x * self.zoom
local offsetY = centerY - y * self.zoom
self:setImageOffset({x=offsetX, y=offsetY})
end
function UIImageView:onDragEnter(pos)
return true
end
function UIImageView:onDragMove(pos, moved)
local posX, posY = self:getImagePosition()
self:move(posX - moved.x / self.zoom, posY - moved.y / self.zoom)
return true
end
function UIImageView:onDragLeave(widget, pos)
return true
end
function UIImageView:onMouseWheel(mousePos, direction)
local x = mousePos.x - self:getX()
local y = mousePos.y - self:getY()
if direction == MouseWheelUp then
self:zoomIn(x, y)
elseif direction == MouseWheelDown then
self:zoomOut(x, y)
end
end

View File

@@ -0,0 +1,114 @@
if not UIWindow then dofile 'uiwindow' end
-- @docclass
UIInputBox = extends(UIWindow, "UIInputBox")
function UIInputBox.create(title, okCallback, cancelCallback)
local inputBox = UIInputBox.internalCreate()
inputBox:setText(title)
inputBox.inputs = {}
inputBox.onEnter = function()
local results = {}
for _,func in pairs(inputBox.inputs) do
table.insert(results, func())
end
okCallback(unpack(results))
inputBox:destroy()
end
inputBox.onEscape = function()
if cancelCallback then
cancelCallback()
end
inputBox:destroy()
end
return inputBox
end
function UIInputBox:addLabel(text)
local label = g_ui.createWidget('InputBoxLabel', self)
label:setText(text)
return label
end
function UIInputBox:addLineEdit(labelText, defaultText, maxLength)
if labelText then self:addLabel(labelText) end
local lineEdit = g_ui.createWidget('InputBoxLineEdit', self)
if defaultText then lineEdit:setText(defaultText) end
if maxLength then lineEdit:setMaxLength(maxLength) end
table.insert(self.inputs, function() return lineEdit:getText() end)
return lineEdit
end
function UIInputBox:addTextEdit(labelText, defaultText, maxLength, visibleLines)
if labelText then self:addLabel(labelText) end
local textEdit = g_ui.createWidget('InputBoxTextEdit', self)
if defaultText then textEdit:setText(defaultText) end
if maxLength then textEdit:setMaxLength(maxLength) end
visibleLines = visibleLines or 1
textEdit:setHeight(textEdit:getHeight() * visibleLines)
table.insert(self.inputs, function() return textEdit:getText() end)
return textEdit
end
function UIInputBox:addCheckBox(text, checked)
local checkBox = g_ui.createWidget('InputBoxCheckBox', self)
checkBox:setText(text)
checkBox:setChecked(checked)
table.insert(self.inputs, function() return checkBox:isChecked() end)
return checkBox
end
function UIInputBox:addComboBox(labelText, ...)
if labelText then self:addLabel(labelText) end
local comboBox = g_ui.createWidget('InputBoxComboBox', self)
local options = {...}
for i=1,#options do
comboBox:addOption(options[i])
end
table.insert(self.inputs, function() return comboBox:getCurrentOption() end)
return comboBox
end
function UIInputBox:addSpinBox(labelText, minimum, maximum, value, step)
if labelText then self:addLabel(labelText) end
local spinBox = g_ui.createWidget('InputBoxSpinBox', self)
spinBox:setMinimum(minimum)
spinBox:setMaximum(maximum)
spinBox:setValue(value)
spinBox:setStep(step)
table.insert(self.inputs, function() return spinBox:getValue() end)
return spinBox
end
function UIInputBox:display(okButtonText, cancelButtonText)
okButtonText = okButtonText or tr('Ok')
cancelButtonText = cancelButtonText or tr('Cancel')
local buttonsWidget = g_ui.createWidget('InputBoxButtonsPanel', self)
local okButton = g_ui.createWidget('InputBoxButton', buttonsWidget)
okButton:setText(okButtonText)
okButton.onClick = self.onEnter
local cancelButton = g_ui.createWidget('InputBoxButton', buttonsWidget)
cancelButton:setText(cancelButtonText)
cancelButton.onClick = self.onEscape
buttonsWidget:setHeight(okButton:getHeight())
rootWidget:addChild(self)
self:setStyle('InputBoxWindow')
end
function displayTextInputBox(title, label, okCallback, cancelCallback)
local inputBox = UIInputBox.create(title, okCallback, cancelCallback)
inputBox:addLineEdit(label)
inputBox:display()
end
function displayNumberInputBox(title, label, okCallback, cancelCallback, min, max, value, step)
local inputBox = UIInputBox.create(title, okCallback, cancelCallback)
inputBox:addSpinBox(label, min, max, value, step)
inputBox:display()
end

View File

@@ -0,0 +1,10 @@
-- @docclass
UILabel = extends(UIWidget, "UILabel")
function UILabel.create()
local label = UILabel.internalCreate()
label:setPhantom(true)
label:setFocusable(false)
label:setTextAlign(AlignLeft)
return label
end

View File

@@ -0,0 +1,96 @@
if not UIWindow then dofile 'uiwindow' end
-- @docclass
UIMessageBox = extends(UIWindow, "UIMessageBox")
-- messagebox cannot be created from otui files
UIMessageBox.create = nil
function UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback)
local messageBox = UIMessageBox.internalCreate()
rootWidget:addChild(messageBox)
messageBox:setStyle('MainWindow')
messageBox:setText(title)
local messageLabel = g_ui.createWidget('MessageBoxLabel', messageBox)
messageLabel:setText(message)
local buttonsWidth = 0
local buttonsHeight = 0
local anchor = AnchorRight
if buttons.anchor then anchor = buttons.anchor end
local buttonHolder = g_ui.createWidget('MessageBoxButtonHolder', messageBox)
buttonHolder:addAnchor(anchor, 'parent', anchor)
for i=1,#buttons do
local button = messageBox:addButton(buttons[i].text, buttons[i].callback)
if i == 1 then
button:setMarginLeft(0)
button:addAnchor(AnchorBottom, 'parent', AnchorBottom)
button:addAnchor(AnchorLeft, 'parent', AnchorLeft)
buttonsHeight = button:getHeight()
else
button:addAnchor(AnchorBottom, 'prev', AnchorBottom)
button:addAnchor(AnchorLeft, 'prev', AnchorRight)
end
buttonsWidth = buttonsWidth + button:getWidth() + button:getMarginLeft()
end
buttonHolder:setWidth(buttonsWidth)
buttonHolder:setHeight(buttonsHeight)
if onEnterCallback then connect(messageBox, { onEnter = onEnterCallback }) end
if onEscapeCallback then connect(messageBox, { onEscape = onEscapeCallback }) end
messageBox:setWidth(math.max(messageLabel:getWidth(), messageBox:getTextSize().width, buttonHolder:getWidth()) + messageBox:getPaddingLeft() + messageBox:getPaddingRight())
messageBox:setHeight(messageLabel:getHeight() + messageBox:getPaddingTop() + messageBox:getPaddingBottom() + buttonHolder:getHeight() + buttonHolder:getMarginTop())
return messageBox
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 UIMessageBox:addButton(text, callback)
local buttonHolder = self:getChildById('buttonHolder')
local button = g_ui.createWidget('MessageBoxButton', buttonHolder)
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

View File

@@ -0,0 +1,466 @@
-- @docclass
UIMiniWindow = extends(UIWindow, "UIMiniWindow")
function UIMiniWindow.create()
local miniwindow = UIMiniWindow.internalCreate()
miniwindow.UIMiniWindowContainer = true
return miniwindow
end
function UIMiniWindow:open(dontSave)
self:setVisible(true)
if not dontSave then
self:setSettings({closed = false})
end
signalcall(self.onOpen, self)
end
function UIMiniWindow:close(dontSave)
if not self:isExplicitlyVisible() then return end
if self.forceOpen then return end
self:setVisible(false)
if not dontSave then
self:setSettings({closed = true})
end
signalcall(self.onClose, self)
end
function UIMiniWindow:minimize(dontSave)
self:setOn(true)
self:getChildById('contentsPanel'):hide()
self:getChildById('miniwindowScrollBar'):hide()
self:getChildById('bottomResizeBorder'):hide()
if self.minimizeButton then
self.minimizeButton:setOn(true)
end
self.maximizedHeight = self:getHeight()
self:setHeight(self.minimizedHeight)
if not dontSave then
self:setSettings({minimized = true})
end
signalcall(self.onMinimize, self)
end
function UIMiniWindow:maximize(dontSave)
self:setOn(false)
self:getChildById('contentsPanel'):show()
self:getChildById('miniwindowScrollBar'):show()
self:getChildById('bottomResizeBorder'):show()
if self.minimizeButton then
self.minimizeButton:setOn(false)
end
self:setHeight(self:getSettings('height') or self.maximizedHeight)
if not dontSave then
self:setSettings({minimized = false})
end
local parent = self:getParent()
if parent and parent:getClassName() == 'UIMiniWindowContainer' then
parent:fitAll(self)
end
signalcall(self.onMaximize, self)
end
function UIMiniWindow:lock(dontSave)
local lockButton = self:getChildById('lockButton')
if lockButton then
lockButton:setOn(true)
end
self:setDraggable(false)
if not dontsave then
self:setSettings({locked = true})
end
signalcall(self.onLockChange, self)
end
function UIMiniWindow:unlock(dontSave)
local lockButton = self:getChildById('lockButton')
if lockButton then
lockButton:setOn(false)
end
self:setDraggable(true)
if not dontsave then
self:setSettings({locked = false})
end
signalcall(self.onLockChange, self)
end
function UIMiniWindow:setup()
self:getChildById('closeButton').onClick =
function()
self:close()
end
if self.forceOpen then
if self.closeButton then
self.closeButton:hide()
end
end
if(self.minimizeButton) then
self.minimizeButton.onClick =
function()
if self:isOn() then
self:maximize()
else
self:minimize()
end
end
end
local lockButton = self:getChildById('lockButton')
if lockButton then
lockButton.onClick =
function ()
if self:isDraggable() then
self:lock()
else
self:unlock()
end
end
end
self:getChildById('miniwindowTopBar').onDoubleClick =
function()
if self:isOn() then
self:maximize()
else
self:minimize()
end
end
self:getChildById('bottomResizeBorder').onDoubleClick = function()
local resizeBorder = self:getChildById('bottomResizeBorder')
self:setHeight(resizeBorder:getMinimum())
end
local oldParent = self:getParent()
local settings = {}
if g_settings.getNodeSize('MiniWindows') < 50 then
settings = g_settings.getNode('MiniWindows')
end
if settings then
local selfSettings = settings[self:getId()]
if selfSettings then
if selfSettings.parentId then
local parent = rootWidget:recursiveGetChildById(selfSettings.parentId)
if parent then
if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then
self.miniIndex = selfSettings.index
parent:scheduleInsert(self, selfSettings.index)
elseif selfSettings.position then
self:setParent(parent, true)
self:setPosition(topoint(selfSettings.position))
end
end
end
if selfSettings.minimized then
self:minimize(true)
else
if selfSettings.height and self:isResizeable() then
self:setHeight(selfSettings.height)
elseif selfSettings.height and not self:isResizeable() then
self:eraseSettings({height = true})
end
end
if selfSettings.closed and not self.forceOpen and not self.containerWindow then
self:close(true)
end
if selfSettings.locked then
self:lock(true)
end
else
if not self.forceOpen and self.autoOpen ~= nil and (self.autoOpen == 0 or self.autoOpen == false) and not self.containerWindow then
self:close(true)
end
end
end
local newParent = self:getParent()
self.miniLoaded = true
if self.save then
if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' and not self.containerWindow then
addEvent(function() oldParent:order() end)
end
if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then
addEvent(function() newParent:order() end)
end
end
self:fitOnParent()
end
function UIMiniWindow:onVisibilityChange(visible)
self:fitOnParent()
end
function UIMiniWindow:onDragEnter(mousePos)
local parent = self:getParent()
if not parent then return false end
if parent:getClassName() == 'UIMiniWindowContainer' then
local containerParent = parent:getParent():getParent()
parent:removeChild(self)
containerParent:addChild(self)
parent:saveChildren()
end
local oldPos = self:getPosition()
self.movingReference = { x = mousePos.x - oldPos.x, y = mousePos.y - oldPos.y }
self:setPosition(oldPos)
self.free = true
return true
end
function UIMiniWindow:onDragLeave(droppedWidget, mousePos)
if self.movedWidget then
self.setMovedChildMargin(self.movedOldMargin or 0)
self.movedWidget = nil
self.setMovedChildMargin = nil
self.movedOldMargin = nil
self.movedIndex = nil
end
UIWindow:onDragLeave(self, droppedWidget, mousePos)
self:saveParent(self:getParent())
end
function UIMiniWindow:onDragMove(mousePos, mouseMoved)
local oldMousePosY = mousePos.y - mouseMoved.y
local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos)
local overAnyWidget = false
for i=1,#children do
local child = children[i]
if child:getParent():getClassName() == 'UIMiniWindowContainer' then
overAnyWidget = true
local childCenterY = child:getY() + child:getHeight() / 2
if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then
break
end
if self.movedWidget then
self.setMovedChildMargin(self.movedOldMargin or 0)
self.setMovedChildMargin = nil
end
if mousePos.y < childCenterY then
self.movedOldMargin = child:getMarginTop()
self.setMovedChildMargin = function(v) child:setMarginTop(v) end
self.movedIndex = 0
else
self.movedOldMargin = child:getMarginBottom()
self.setMovedChildMargin = function(v) child:setMarginBottom(v) end
self.movedIndex = 1
end
self.movedWidget = child
self.setMovedChildMargin(self:getHeight())
break
end
end
if not overAnyWidget and self.movedWidget then
self.setMovedChildMargin(self.movedOldMargin or 0)
self.movedWidget = nil
end
return UIWindow.onDragMove(self, mousePos, mouseMoved)
end
function UIMiniWindow:onMousePress()
local parent = self:getParent()
if not parent then return false end
if parent:getClassName() ~= 'UIMiniWindowContainer' then
self:raise()
return true
end
end
function UIMiniWindow:onFocusChange(focused)
if not focused then return end
local parent = self:getParent()
if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then
self:raise()
end
end
function UIMiniWindow:onHeightChange(height)
if not self:isOn() then
self:setSettings({height = height})
end
self:fitOnParent()
end
function UIMiniWindow:getSettings(name)
if not self.save then return nil end
local settings = g_settings.getNode('MiniWindows')
if settings then
local selfSettings = settings[self:getId()]
if selfSettings then
return selfSettings[name]
end
end
return nil
end
function UIMiniWindow:setSettings(data)
if not self.save then return end
local settings = g_settings.getNode('MiniWindows')
if not settings then
settings = {}
end
local id = self:getId()
if not settings[id] then
settings[id] = {}
end
for key,value in pairs(data) do
settings[id][key] = value
end
g_settings.setNode('MiniWindows', settings)
end
function UIMiniWindow:eraseSettings(data)
if not self.save then return end
local settings = g_settings.getNode('MiniWindows')
if not settings then
settings = {}
end
local id = self:getId()
if not settings[id] then
settings[id] = {}
end
for key,value in pairs(data) do
settings[id][key] = nil
end
g_settings.setNode('MiniWindows', settings)
end
function UIMiniWindow:clearSettings()
if not self.save then return end
local settings = g_settings.getNode('MiniWindows')
if not settings then
settings = {}
end
local id = self:getId()
settings[id] = {}
g_settings.setNode('MiniWindows', settings)
end
function UIMiniWindow:saveParent(parent)
local parent = self:getParent()
if parent then
if parent:getClassName() == 'UIMiniWindowContainer' then
parent:saveChildren()
else
self:saveParentPosition(parent:getId(), self:getPosition())
end
end
end
function UIMiniWindow:saveParentPosition(parentId, position)
local selfSettings = {}
selfSettings.parentId = parentId
selfSettings.position = pointtostring(position)
self:setSettings(selfSettings)
end
function UIMiniWindow:saveParentIndex(parentId, index)
local selfSettings = {}
selfSettings.parentId = parentId
selfSettings.index = index
self:setSettings(selfSettings)
self.miniIndex = index
end
function UIMiniWindow:disableResize()
self:getChildById('bottomResizeBorder'):disable()
end
function UIMiniWindow:enableResize()
self:getChildById('bottomResizeBorder'):enable()
end
function UIMiniWindow:fitOnParent()
local parent = self:getParent()
if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then
parent:fitAll(self)
end
end
function UIMiniWindow:setParent(parent, dontsave)
UIWidget.setParent(self, parent)
if not dontsave then
self:saveParent(parent)
end
self:fitOnParent()
end
function UIMiniWindow:setHeight(height)
UIWidget.setHeight(self, height)
signalcall(self.onHeightChange, self, height)
end
function UIMiniWindow:setContentHeight(height)
local contentsPanel = self:getChildById('contentsPanel')
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
local resizeBorder = self:getChildById('bottomResizeBorder')
resizeBorder:setParentSize(minHeight + height)
end
function UIMiniWindow:setContentMinimumHeight(height)
local contentsPanel = self:getChildById('contentsPanel')
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
local resizeBorder = self:getChildById('bottomResizeBorder')
resizeBorder:setMinimum(minHeight + height)
end
function UIMiniWindow:setContentMaximumHeight(height)
local contentsPanel = self:getChildById('contentsPanel')
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
local resizeBorder = self:getChildById('bottomResizeBorder')
resizeBorder:setMaximum(minHeight + height)
end
function UIMiniWindow:getMinimumHeight()
local resizeBorder = self:getChildById('bottomResizeBorder')
return resizeBorder:getMinimum()
end
function UIMiniWindow:getMaximumHeight()
local resizeBorder = self:getChildById('bottomResizeBorder')
return resizeBorder:getMaximum()
end
function UIMiniWindow:isResizeable()
local resizeBorder = self:getChildById('bottomResizeBorder')
return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled()
end

View File

@@ -0,0 +1,228 @@
-- @docclass
UIMiniWindowContainer = extends(UIWidget, "UIMiniWindowContainer")
function UIMiniWindowContainer.create()
local container = UIMiniWindowContainer.internalCreate()
container.scheduledWidgets = {}
container:setFocusable(false)
container:setPhantom(true)
return container
end
-- TODO: connect to window onResize event
-- TODO: try to resize another widget?
-- TODO: try to find another panel?
function UIMiniWindowContainer:fitAll(noRemoveChild)
if not self:isVisible() then
return
end
if not noRemoveChild then
local children = self:getChildren()
if #children > 0 then
noRemoveChild = children[#children]
else
return
end
end
local sumHeight = 0
local children = self:getChildren()
for i=1,#children do
if children[i]:isVisible() then
sumHeight = sumHeight + children[i]:getHeight()
end
end
local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom())
if sumHeight <= selfHeight then
return
end
local removeChildren = {}
-- try to resize noRemoveChild
local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight())
if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then
sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight
addEvent(function() noRemoveChild:setHeight(maximumHeight) end)
end
-- try to remove no-save widget
for i=#children,1,-1 do
if sumHeight <= selfHeight then
break
end
local child = children[i]
if child ~= noRemoveChild and not child.save then
local childHeight = child:getHeight()
sumHeight = sumHeight - childHeight
table.insert(removeChildren, child)
end
end
-- try to remove save widget, not forceOpen
for i=#children,1,-1 do
if sumHeight <= selfHeight then
break
end
local child = children[i]
if child ~= noRemoveChild and child:isVisible() and not child.forceOpen then
local childHeight = child:getHeight()
sumHeight = sumHeight - childHeight
table.insert(removeChildren, child)
end
end
-- try to remove save widget
for i=#children,1,-1 do
if sumHeight <= selfHeight then
break
end
local child = children[i]
if child ~= noRemoveChild and child:isVisible() then
local childHeight = child:getHeight() - 50
sumHeight = sumHeight - childHeight
table.insert(removeChildren, child)
end
end
-- close widgets
for i=1,#removeChildren do
if removeChildren[i].forceOpen then
removeChildren[i]:minimize(true)
else
removeChildren[i]:close()
end
end
end
function UIMiniWindowContainer:onDrop(widget, mousePos)
if widget.UIMiniWindowContainer then
local oldParent = widget:getParent()
if oldParent == self then
return true
end
if oldParent then
oldParent:removeChild(widget)
end
if widget.movedWidget then
local index = self:getChildIndex(widget.movedWidget)
self:insertChild(index + widget.movedIndex, widget)
else
self:addChild(widget)
end
self:fitAll(widget)
return true
end
end
function UIMiniWindowContainer:moveTo(newPanel)
if not newPanel or newPanel == self then
return
end
local children = self:getChildByIndex(1)
while children do
newPanel:addChild(children)
children = self:getChildByIndex(1)
end
newPanel:fitAll()
end
function UIMiniWindowContainer:swapInsert(widget, index)
local oldParent = widget:getParent()
local oldIndex = self:getChildIndex(widget)
if oldParent == self and oldIndex ~= index then
local oldWidget = self:getChildByIndex(index)
if oldWidget then
self:removeChild(oldWidget)
self:insertChild(oldIndex, oldWidget)
end
self:removeChild(widget)
self:insertChild(index, widget)
end
end
function UIMiniWindowContainer:scheduleInsert(widget, index)
if index - 1 > self:getChildCount() then
if self.scheduledWidgets[index] then
pdebug('replacing scheduled widget id ' .. widget:getId())
end
self.scheduledWidgets[index] = widget
else
local oldParent = widget:getParent()
if oldParent ~= self then
if oldParent then
oldParent:removeChild(widget)
end
self:insertChild(index, widget)
while true do
local placed = false
for nIndex,nWidget in pairs(self.scheduledWidgets) do
if nIndex - 1 <= self:getChildCount() then
local oldParent = nWidget:getParent()
if oldParent ~= self then
if oldParent then
oldParent:removeChild(nWidget)
end
self:insertChild(nIndex, nWidget)
else
self:moveChildToIndex(nWidget, nIndex)
end
self.scheduledWidgets[nIndex] = nil
placed = true
break
end
end
if not placed then break end
end
end
end
end
function UIMiniWindowContainer:order()
local children = self:getChildren()
for i=1,#children do
if not children[i].miniLoaded then return end
end
table.sort(children, function(a, b)
local indexA = a.miniIndex or a.autoOpen or 999
local indexB = b.miniIndex or b.autoOpen or 999
return indexA < indexB
end)
self:reorderChildren(children)
local ignoreIndex = 0
for i=1,#children do
if children[i].save then
children[i].miniIndex = i - ignoreIndex
else
ignoreIndex = ignoreIndex + 1
end
end
end
function UIMiniWindowContainer:saveChildren()
local children = self:getChildren()
local ignoreIndex = 0
for i=1,#children do
if children[i].save then
children[i]:saveParentIndex(self:getId(), i - ignoreIndex)
else
ignoreIndex = ignoreIndex + 1
end
end
end
function UIMiniWindowContainer:onGeometryChange()
self:fitAll()
end

View File

@@ -0,0 +1,505 @@
-- @docclass
UIMoveableTabBar = extends(UIWidget, "UIMoveableTabBar")
-- private functions
local function onTabClick(tab)
tab.tabBar:selectTab(tab)
end
local function updateMargins(tabBar)
if #tabBar.tabs == 0 then return end
local currentMargin = 0
for i = 1, #tabBar.tabs do
tabBar.tabs[i]:setMarginLeft(currentMargin)
currentMargin = currentMargin + tabBar.tabSpacing + tabBar.tabs[i]:getWidth()
end
end
local function updateNavigation(tabBar)
if tabBar.prevNavigation then
if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then
tabBar.prevNavigation:enable()
else
tabBar.prevNavigation:disable()
end
end
if tabBar.nextNavigation then
if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then
tabBar.nextNavigation:enable()
else
tabBar.nextNavigation:disable()
end
end
end
local function updateIndexes(tabBar, tab, xoff)
local tabs = tabBar.tabs
local currentMargin = 0
local prevIndex = table.find(tabs, tab)
local newIndex = prevIndex
local xmid = xoff + tab:getWidth()/2
for i = 1, #tabs do
local nextTab = tabs[i]
if xmid >= currentMargin + nextTab:getWidth()/2 then
newIndex = table.find(tabs, nextTab)
end
currentMargin = currentMargin + tabBar.tabSpacing * (i - 1) + tabBar.tabs[i]:getWidth()
end
if newIndex ~= prevIndex then
table.remove(tabs, table.find(tabs, tab))
table.insert(tabs, newIndex, tab)
end
updateNavigation(tabBar)
end
local function getMaxMargin(tabBar, tab)
if #tabBar.tabs == 0 then return 0 end
local maxMargin = 0
for i = 1, #tabBar.tabs do
if tabBar.tabs[i] ~= tab then
maxMargin = maxMargin + tabBar.tabs[i]:getWidth()
end
end
return maxMargin + tabBar.tabSpacing * (#tabBar.tabs - 1)
end
local function updateTabs(tabBar)
if #tabBar.postTabs > 0 then
local i = 1
while i <= #tabBar.postTabs do
local tab = tabBar.postTabs[i]
if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
break
end
table.remove(tabBar.postTabs, i)
table.insert(tabBar.tabs, tab)
tab:setVisible(true)
end
end
if #tabBar.preTabs > 0 then
for i = #tabBar.preTabs, 1, -1 do
local tab = tabBar.preTabs[i]
if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
break
end
table.remove(tabBar.preTabs, i)
table.insert(tabBar.tabs, 1, tab)
tab:setVisible(true)
end
end
updateNavigation(tabBar)
updateMargins(tabBar)
if not tabBar.currentTab and #tabBar.tabs > 0 then
tabBar:selectTab(tabBar.tabs[1])
end
end
local function hideTabs(tabBar, fromBack, toArray, width)
while #tabBar.tabs > 0 and getMaxMargin(tabBar) + width > tabBar:getWidth() do
local index = fromBack and #tabBar.tabs or 1
local tab = tabBar.tabs[index]
table.remove(tabBar.tabs, index)
if fromBack then
table.insert(toArray, 1, tab)
else
table.insert(toArray, tab)
end
if tabBar.currentTab == tab then
if #tabBar.tabs > 0 then
tabBar:selectTab(tabBar.tabs[#tabBar.tabs])
else
tabBar.currentTab:setChecked(false)
tabBar.currentTab = nil
end
end
tab:setVisible(false)
end
end
local function showPreTab(tabBar)
if #tabBar.preTabs == 0 then
return nil
end
local tmpTab = tabBar.preTabs[#tabBar.preTabs]
hideTabs(tabBar, true, tabBar.postTabs, tmpTab:getWidth())
table.remove(tabBar.preTabs, #tabBar.preTabs)
table.insert(tabBar.tabs, 1, tmpTab)
tmpTab:setVisible(true)
return tmpTab
end
local function showPostTab(tabBar)
if #tabBar.postTabs == 0 then
return nil
end
local tmpTab = tabBar.postTabs[1]
hideTabs(tabBar, false, tabBar.preTabs, tmpTab:getWidth())
table.remove(tabBar.postTabs, 1)
table.insert(tabBar.tabs, tmpTab)
tmpTab:setVisible(true)
return tmpTab
end
local function onTabMousePress(tab, mousePos, mouseButton)
if mouseButton == MouseRightButton then
if tab.menuCallback then tab.menuCallback(tab, mousePos, mouseButton) end
return true
end
end
local function onTabDragEnter(tab, mousePos)
tab:raise()
tab.hotSpot = mousePos.x - tab:getMarginLeft()
tab.tabBar.selected = tab
return true
end
local function onTabDragLeave(tab)
updateMargins(tab.tabBar)
tab.tabBar.selected = nil
return true
end
local function onTabDragMove(tab, mousePos, mouseMoved)
if tab == tab.tabBar.selected then
local xoff = mousePos.x - tab.hotSpot
-- update indexes
updateIndexes(tab.tabBar, tab, xoff)
updateIndexes(tab.tabBar, tab, xoff)
-- update margins
updateMargins(tab.tabBar)
xoff = math.max(xoff, 0)
xoff = math.min(xoff, getMaxMargin(tab.tabBar, tab))
tab:setMarginLeft(xoff)
end
end
local function tabBlink(tab, step)
local step = step or 0
tab:setOn(not tab:isOn())
removeEvent(tab.blinkEvent)
if step < 4 then
tab.blinkEvent = scheduleEvent(function() tabBlink(tab, step+1) end, 500)
else
tab:setOn(true)
tab.blinkEvent = nil
end
end
-- public functions
function UIMoveableTabBar.create()
local tabbar = UIMoveableTabBar.internalCreate()
tabbar:setFocusable(false)
tabbar.tabs = {}
tabbar.selected = nil -- dragged tab
tabbar.tabSpacing = 0
tabbar.tabsMoveable = false
tabbar.preTabs = {}
tabbar.postTabs = {}
tabbar.prevNavigation = nil
tabbar.nextNavigation = nil
tabbar.onGeometryChange = function()
hideTabs(tabbar, true, tabbar.postTabs, 0)
updateTabs(tabbar)
end
return tabbar
end
function UIMoveableTabBar:onDestroy()
if self.prevNavigation then
self.prevNavigation:disable()
end
if self.nextNavigation then
self.nextNavigation:disable()
end
self.nextNavigation = nil
self.prevNavigation = nil
end
function UIMoveableTabBar:setContentWidget(widget)
self.contentWidget = widget
if #self.tabs > 0 then
self.contentWidget:addChild(self.tabs[1].tabPanel)
end
end
function UIMoveableTabBar:setTabSpacing(tabSpacing)
self.tabSpacing = tabSpacing
updateMargins(self)
end
function UIMoveableTabBar:addTab(text, panel, menuCallback)
if panel == nil then
panel = g_ui.createWidget(self:getStyleName() .. 'Panel')
panel:setId('tabPanel')
end
local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self)
panel.isTab = true
tab.tabPanel = panel
tab.tabBar = self
tab:setId('tab')
tab:setDraggable(self.tabsMoveable)
tab:setText(text)
tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight())
tab.menuCallback = menuCallback or nil
tab.onClick = onTabClick
tab.onMousePress = onTabMousePress
tab.onDragEnter = onTabDragEnter
tab.onDragLeave = onTabDragLeave
tab.onDragMove = onTabDragMove
tab.onDestroy = function() tab.tabPanel:destroy() end
if #self.tabs == 0 then
self:selectTab(tab)
tab:setMarginLeft(0)
table.insert(self.tabs, tab)
else
local newMargin = self.tabSpacing * #self.tabs
for i = 1, #self.tabs do
newMargin = newMargin + self.tabs[i]:getWidth()
end
tab:setMarginLeft(newMargin)
hideTabs(self, true, self.postTabs, tab:getWidth())
table.insert(self.tabs, tab)
if #self.tabs == 1 then
self:selectTab(tab)
end
updateMargins(self)
end
updateNavigation(self)
return tab
end
-- Additional function to move the tab by lua
function UIMoveableTabBar:moveTab(tab, units)
local index = table.find(self.tabs, tab)
if index == nil then return end
local focus = false
if self.currentTab == tab then
self:selectPrevTab()
focus = true
end
table.remove(self.tabs, index)
local newIndex = math.min(#self.tabs+1, math.max(index + units, 1))
table.insert(self.tabs, newIndex, tab)
if focus then self:selectTab(tab) end
updateMargins(self)
return newIndex
end
function UIMoveableTabBar:onStyleApply(styleName, styleNode)
if styleNode['movable'] then
self.tabsMoveable = styleNode['movable']
end
if styleNode['tab-spacing'] then
self:setTabSpacing(styleNode['tab-spacing'])
end
end
function UIMoveableTabBar:clearTabs()
while #self.tabs > 0 do
self:removeTab(self.tabs[#self.tabs])
end
end
function UIMoveableTabBar:removeTab(tab)
local tabTables = {self.tabs, self.preTabs, self.postTabs}
local index = nil
local tabTable = nil
for i = 1, #tabTables do
index = table.find(tabTables[i], tab)
if index ~= nil then
tabTable = tabTables[i]
break
end
end
if tabTable == nil then
return
end
table.remove(tabTable, index)
if self.currentTab == tab then
self:selectPrevTab()
if #self.tabs == 1 then
self.currentTab = nil
end
end
if tab.blinkEvent then
removeEvent(tab.blinkEvent)
end
updateTabs(self)
tab:destroy()
end
function UIMoveableTabBar:getTab(text)
for k,tab in pairs(self.tabs) do
if tab:getText():lower() == text:lower() then
return tab
end
end
for k,tab in pairs(self.preTabs) do
if tab:getText():lower() == text:lower() then
return tab
end
end
for k,tab in pairs(self.postTabs) do
if tab:getText():lower() == text:lower() then
return tab
end
end
end
function UIMoveableTabBar:selectTab(tab)
if self.currentTab == tab then return end
if self.contentWidget then
local selectedWidget = self.contentWidget:getLastChild()
if selectedWidget and selectedWidget.isTab then
self.contentWidget:removeChild(selectedWidget)
end
self.contentWidget:addChild(tab.tabPanel)
tab.tabPanel:fill('parent')
end
if self.currentTab then
self.currentTab:setChecked(false)
end
signalcall(self.onTabChange, self, tab)
self.currentTab = tab
tab:setChecked(true)
tab:setOn(false)
tab.blinking = false
if tab.blinkEvent then
removeEvent(tab.blinkEvent)
tab.blinkEvent = nil
end
local parent = tab:getParent()
parent:focusChild(tab, MouseFocusReason)
updateNavigation(self)
end
function UIMoveableTabBar:selectNextTab()
if self.currentTab == nil then
return
end
local index = table.find(self.tabs, self.currentTab)
if index == nil then
return
end
local newIndex = index + 1
if newIndex > #self.tabs then
if #self.postTabs > 0 then
local widget = showPostTab(self)
self:selectTab(widget)
else
if #self.preTabs > 0 then
for i = 1, #self.preTabs do
showPreTab(self)
end
end
self:selectTab(self.tabs[1])
end
updateTabs(self)
return
end
local nextTab = self.tabs[newIndex]
if not nextTab then
return
end
self:selectTab(nextTab)
end
function UIMoveableTabBar:selectPrevTab()
if self.currentTab == nil then
return
end
local index = table.find(self.tabs, self.currentTab)
if index == nil then
return
end
local newIndex = index - 1
if newIndex <= 0 then
if #self.preTabs > 0 then
local widget = showPreTab(self)
self:selectTab(widget)
else
if #self.postTabs > 0 then
for i = 1, #self.postTabs do
showPostTab(self)
end
end
self:selectTab(self.tabs[#self.tabs])
end
updateTabs(self)
return
end
local prevTab = self.tabs[newIndex]
if not prevTab then
return
end
self:selectTab(prevTab)
end
function UIMoveableTabBar:blinkTab(tab)
if tab:isChecked() then return end
tab.blinking = true
tabBlink(tab)
end
function UIMoveableTabBar:getTabPanel(tab)
return tab.tabPanel
end
function UIMoveableTabBar:getCurrentTabPanel()
if self.currentTab then
return self.currentTab.tabPanel
end
end
function UIMoveableTabBar:getCurrentTab()
return self.currentTab
end
function UIMoveableTabBar:setNavigation(prevButton, nextButton)
self.prevNavigation = prevButton
self.nextNavigation = nextButton
if self.prevNavigation then
self.prevNavigation.onClick = function() self:selectPrevTab() end
end
if self.nextNavigation then
self.nextNavigation.onClick = function() self:selectNextTab() end
end
updateNavigation(self)
end

View File

@@ -0,0 +1,122 @@
-- @docclass
UIPopupMenu = extends(UIWidget, "UIPopupMenu")
local currentMenu
function UIPopupMenu.create()
local menu = UIPopupMenu.internalCreate()
local layout = UIVerticalLayout.create(menu)
layout:setFitChildren(true)
menu:setLayout(layout)
menu.isGameMenu = false
return menu
end
function UIPopupMenu:display(pos)
-- don't display if not options was added
if self:getChildCount() == 0 then
self:destroy()
return
end
if g_ui.isMouseGrabbed() then
self:destroy()
return
end
if currentMenu then
currentMenu:destroy()
end
if pos == nil then
pos = g_window.getMousePosition()
end
rootWidget:addChild(self)
self:setPosition(pos)
self:grabMouse()
self:focus()
--self:grabKeyboard()
currentMenu = self
end
function UIPopupMenu:onGeometryChange(oldRect, newRect)
local parent = self:getParent()
if not parent then return end
local ymax = parent:getY() + parent:getHeight()
local xmax = parent:getX() + parent:getWidth()
if newRect.y + newRect.height > ymax then
local newy = ymax - newRect.height
if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end
end
if newRect.x + newRect.width > xmax then
local newx = xmax - newRect.width
if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end
end
self:bindRectToParent()
end
function UIPopupMenu:addOption(optionName, optionCallback, shortcut)
local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self)
optionWidget.onClick = function(widget)
self:destroy()
optionCallback()
end
optionWidget:setText(optionName)
local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15
if shortcut then
local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget)
shortcutLabel:setText(shortcut)
width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight()
end
self:setWidth(math.max(self:getWidth(), width))
end
function UIPopupMenu:addSeparator()
g_ui.createWidget(self:getStyleName() .. 'Separator', self)
end
function UIPopupMenu:setGameMenu(state)
self.isGameMenu = state
end
function UIPopupMenu:onDestroy()
if currentMenu == self then
currentMenu = nil
end
self:ungrabMouse()
end
function UIPopupMenu:onMousePress(mousePos, mouseButton)
-- clicks outside menu area destroys the menu
if not self:containsPoint(mousePos) then
self:destroy()
end
return true
end
function UIPopupMenu:onKeyPress(keyCode, keyboardModifiers)
if keyCode == KeyEscape then
self:destroy()
return true
end
return false
end
-- close all menus when the window is resized
local function onRootGeometryUpdate()
if currentMenu then
currentMenu:destroy()
end
end
local function onGameEnd()
if currentMenu and currentMenu.isGameMenu then
currentMenu:destroy()
end
end
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate })
connect(g_game, { onGameEnd = onGameEnd } )

View File

@@ -0,0 +1,129 @@
-- @docclass
UIPopupScrollMenu = extends(UIWidget, "UIPopupScrollMenu")
local currentMenu
function UIPopupScrollMenu.create()
local menu = UIPopupScrollMenu.internalCreate()
local scrollArea = g_ui.createWidget('UIScrollArea', menu)
scrollArea:setLayout(UIVerticalLayout.create(menu))
scrollArea:setId('scrollArea')
local scrollBar = g_ui.createWidget('VerticalScrollBar', menu)
scrollBar:setId('scrollBar')
scrollBar.pixelsScroll = false
scrollBar:addAnchor(AnchorRight, 'parent', AnchorRight)
scrollBar:addAnchor(AnchorTop, 'parent', AnchorTop)
scrollBar:addAnchor(AnchorBottom, 'parent', AnchorBottom)
scrollArea:addAnchor(AnchorLeft, 'parent', AnchorLeft)
scrollArea:addAnchor(AnchorTop, 'parent', AnchorTop)
scrollArea:addAnchor(AnchorBottom, 'parent', AnchorBottom)
scrollArea:addAnchor(AnchorRight, 'next', AnchorLeft)
scrollArea:setVerticalScrollBar(scrollBar)
menu.scrollArea = scrollArea
menu.scrollBar = scrollBar
return menu
end
function UIPopupScrollMenu:setScrollbarStep(step)
self.scrollBar:setStep(step)
end
function UIPopupScrollMenu:display(pos)
-- don't display if not options was added
if self.scrollArea:getChildCount() == 0 then
self:destroy()
return
end
if g_ui.isMouseGrabbed() then
self:destroy()
return
end
if currentMenu then
currentMenu:destroy()
end
if pos == nil then
pos = g_window.getMousePosition()
end
rootWidget:addChild(self)
self:setPosition(pos)
self:grabMouse()
currentMenu = self
end
function UIPopupScrollMenu:onGeometryChange(oldRect, newRect)
local parent = self:getParent()
if not parent then return end
local ymax = parent:getY() + parent:getHeight()
local xmax = parent:getX() + parent:getWidth()
if newRect.y + newRect.height > ymax then
local newy = newRect.y - newRect.height
if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end
end
if newRect.x + newRect.width > xmax then
local newx = newRect.x - newRect.width
if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end
end
self:bindRectToParent()
end
function UIPopupScrollMenu:addOption(optionName, optionCallback, shortcut)
local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self.scrollArea)
optionWidget.onClick = function(widget)
self:destroy()
optionCallback()
end
optionWidget:setText(optionName)
local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15
if shortcut then
local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget)
shortcutLabel:setText(shortcut)
width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight()
end
self:setWidth(math.max(self:getWidth(), width))
end
function UIPopupScrollMenu:addSeparator()
g_ui.createWidget(self:getStyleName() .. 'Separator', self.scrollArea)
end
function UIPopupScrollMenu:onDestroy()
if currentMenu == self then
currentMenu = nil
end
self:ungrabMouse()
end
function UIPopupScrollMenu:onMousePress(mousePos, mouseButton)
-- clicks outside menu area destroys the menu
if not self:containsPoint(mousePos) then
self:destroy()
end
return true
end
function UIPopupScrollMenu:onKeyPress(keyCode, keyboardModifiers)
if keyCode == KeyEscape then
self:destroy()
return true
end
return false
end
-- close all menus when the window is resized
local function onRootGeometryUpdate()
if currentMenu then
currentMenu:destroy()
end
end
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate} )

View File

@@ -0,0 +1,99 @@
-- @docclass
UIProgressBar = extends(UIWidget, "UIProgressBar")
function UIProgressBar.create()
local progressbar = UIProgressBar.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
return progressbar
end
function UIProgressBar:setMinimum(minimum)
self.minimum = minimum
if self.value < minimum then
self:setValue(minimum)
end
end
function UIProgressBar:setMaximum(maximum)
self.maximum = maximum
if self.value > maximum then
self:setValue(maximum)
end
end
function UIProgressBar:setValue(value, minimum, maximum)
if minimum then
self:setMinimum(minimum)
end
if maximum then
self:setMaximum(maximum)
end
self.value = math.max(math.min(value, self.maximum), self.minimum)
self:updateBackground()
end
function UIProgressBar:setPercent(percent)
self:setValue(percent, 0, 100)
end
function UIProgressBar:getPercent()
return self.value
end
function UIProgressBar:getPercentPixels()
return (self.maximum - self.minimum) / self:getWidth()
end
function UIProgressBar:getProgress()
if self.minimum == self.maximum then return 1 end
return (self.value - self.minimum) / (self.maximum - self.minimum)
end
function UIProgressBar:updateBackground()
if self:isOn() then
local width = math.round(math.max((self:getProgress() * (self:getWidth() - self.bgBorderLeft - self.bgBorderRight)), 1))
local height = self:getHeight() - self.bgBorderTop - self.bgBorderBottom
local rect = { x = self.bgBorderLeft, y = self.bgBorderTop, width = width, height = height }
self:setBackgroundRect(rect)
end
end
function UIProgressBar:onSetup()
self:updateBackground()
end
function UIProgressBar:onStyleApply(name, node)
for name,value in pairs(node) do
if name == 'background-border-left' then
self.bgBorderLeft = tonumber(value)
elseif name == 'background-border-right' then
self.bgBorderRight = tonumber(value)
elseif name == 'background-border-top' then
self.bgBorderTop = tonumber(value)
elseif name == 'background-border-bottom' then
self.bgBorderBottom = tonumber(value)
elseif name == 'background-border' then
self.bgBorderLeft = tonumber(value)
self.bgBorderRight = tonumber(value)
self.bgBorderTop = tonumber(value)
self.bgBorderBottom = tonumber(value)
end
end
end
function UIProgressBar:onGeometryChange(oldRect, newRect)
if not self:isOn() then
self:setHeight(0)
end
self:updateBackground()
end

View File

@@ -0,0 +1,66 @@
-- @docclass
UIRadioGroup = newclass("UIRadioGroup")
function UIRadioGroup.create()
local radiogroup = UIRadioGroup.internalCreate()
radiogroup.widgets = {}
radiogroup.selectedWidget = nil
return radiogroup
end
function UIRadioGroup:destroy()
for k,widget in pairs(self.widgets) do
widget.onClick = nil
end
self.widgets = {}
end
function UIRadioGroup:addWidget(widget)
table.insert(self.widgets, widget)
widget.onClick = function(widget) self:selectWidget(widget) end
end
function UIRadioGroup:removeWidget(widget)
if self.selectedWidget == widget then
self:selectWidget(nil)
end
widget.onClick = nil
table.removevalue(self.widgets, widget)
end
function UIRadioGroup:selectWidget(selectedWidget, dontSignal)
if selectedWidget == self.selectedWidget then return end
local previousSelectedWidget = self.selectedWidget
self.selectedWidget = selectedWidget
if previousSelectedWidget then
previousSelectedWidget:setChecked(false)
end
if selectedWidget then
selectedWidget:setChecked(true)
end
if not dontSignal then
signalcall(self.onSelectionChange, self, selectedWidget, previousSelectedWidget)
end
end
function UIRadioGroup:clearSelected()
if not self.selectedWidget then return end
local previousSelectedWidget = self.selectedWidget
self.selectedWidget:setChecked(false)
self.selectedWidget = nil
signalcall(self.onSelectionChange, self, nil, previousSelectedWidget)
end
function UIRadioGroup:getSelectedWidget()
return self.selectedWidget
end
function UIRadioGroup:getFirstWidget()
return self.widgets[1]
end

View File

@@ -0,0 +1,132 @@
-- @docclass
UIResizeBorder = extends(UIWidget, "UIResizeBorder")
function UIResizeBorder.create()
local resizeborder = UIResizeBorder.internalCreate()
resizeborder:setFocusable(false)
resizeborder.minimum = 0
resizeborder.maximum = 1000
return resizeborder
end
function UIResizeBorder:onSetup()
if self:getWidth() > self:getHeight() then
self.vertical = true
else
self.vertical = false
end
end
function UIResizeBorder:onDestroy()
if self.hovering then
g_mouse.popCursor(self.cursortype)
end
end
function UIResizeBorder:onHoverChange(hovered)
if hovered then
if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end
if self:getWidth() > self:getHeight() then
self.vertical = true
self.cursortype = 'vertical'
else
self.vertical = false
self.cursortype = 'horizontal'
end
g_mouse.pushCursor(self.cursortype)
self.hovering = true
if not self:isPressed() then
g_effects.fadeIn(self)
end
else
if not self:isPressed() and self.hovering then
g_mouse.popCursor(self.cursortype)
g_effects.fadeOut(self)
self.hovering = false
end
end
end
function UIResizeBorder:onMouseMove(mousePos, mouseMoved)
if self:isPressed() then
local parent = self:getParent()
local newSize = 0
if self.vertical then
local delta = mousePos.y - self:getY() - self:getHeight()/2
newSize = math.min(math.max(parent:getHeight() + delta, self.minimum), self.maximum)
parent:setHeight(newSize)
else
local delta = mousePos.x - self:getX() - self:getWidth()/2
newSize = math.min(math.max(parent:getWidth() + delta, self.minimum), self.maximum)
parent:setWidth(newSize)
end
self:checkBoundary(newSize)
return true
end
end
function UIResizeBorder:onMouseRelease(mousePos, mouseButton)
if not self:isHovered() then
g_mouse.popCursor(self.cursortype)
g_effects.fadeOut(self)
self.hovering = false
end
end
function UIResizeBorder:onStyleApply(styleName, styleNode)
for name,value in pairs(styleNode) do
if name == 'maximum' then
self:setMaximum(tonumber(value))
elseif name == 'minimum' then
self:setMinimum(tonumber(value))
end
end
end
function UIResizeBorder:onVisibilityChange(visible)
if visible and self.maximum == self.minimum then
self:hide()
end
end
function UIResizeBorder:setMaximum(maximum)
self.maximum = maximum
self:checkBoundary()
end
function UIResizeBorder:setMinimum(minimum)
self.minimum = minimum
self:checkBoundary()
end
function UIResizeBorder:getMaximum() return self.maximum end
function UIResizeBorder:getMinimum() return self.minimum end
function UIResizeBorder:setParentSize(size)
local parent = self:getParent()
if self.vertical then
parent:setHeight(size)
else
parent:setWidth(size)
end
self:checkBoundary(size)
end
function UIResizeBorder:getParentSize()
local parent = self:getParent()
if self.vertical then
return parent:getHeight()
else
return parent:getWidth()
end
end
function UIResizeBorder:checkBoundary(size)
size = size or self:getParentSize()
if self.maximum == self.minimum and size == self.maximum then
self:hide()
else
self:show()
end
end

View File

@@ -0,0 +1,190 @@
-- @docclass
UIScrollArea = extends(UIWidget, "UIScrollArea")
-- public functions
function UIScrollArea.create()
local scrollarea = UIScrollArea.internalCreate()
scrollarea:setClipping(true)
scrollarea.inverted = false
scrollarea.alwaysScrollMaximum = false
return scrollarea
end
function UIScrollArea:onStyleApply(styleName, styleNode)
for name,value in pairs(styleNode) do
if name == 'vertical-scrollbar' then
addEvent(function()
local parent = self:getParent()
if parent then
self:setVerticalScrollBar(parent:getChildById(value))
end
end)
elseif name == 'horizontal-scrollbar' then
addEvent(function()
local parent = self:getParent()
if parent then
self:setHorizontalScrollBar(self:getParent():getChildById(value))
end
end)
elseif name == 'inverted-scroll' then
self:setInverted(value)
elseif name == 'always-scroll-maximum' then
self:setAlwaysScrollMaximum(value)
end
end
end
function UIScrollArea:updateScrollBars()
local scrollWidth = math.max(self:getChildrenRect().width - self:getPaddingRect().width, 0)
local scrollHeight = math.max(self:getChildrenRect().height - self:getPaddingRect().height, 0)
local scrollbar = self.verticalScrollBar
if scrollbar then
if self.inverted then
scrollbar:setMinimum(-scrollHeight)
scrollbar:setMaximum(0)
else
scrollbar:setMinimum(0)
scrollbar:setMaximum(scrollHeight)
end
end
local scrollbar = self.horizontalScrollBar
if scrollbar then
if self.inverted then
scrollbar:setMinimum(-scrollWidth)
scrollbar:setMaximum(0)
else
scrollbar:setMinimum(0)
scrollbar:setMaximum(scrollWidth)
end
end
if self.lastScrollWidth ~= scrollWidth then
self:onScrollWidthChange()
end
if self.lastScrollHeight ~= scrollHeight then
self:onScrollHeightChange()
end
self.lastScrollWidth = scrollWidth
self.lastScrollHeight = scrollHeight
end
function UIScrollArea:setVerticalScrollBar(scrollbar)
self.verticalScrollBar = scrollbar
connect(self.verticalScrollBar, 'onValueChange', function(scrollbar, value)
local virtualOffset = self:getVirtualOffset()
virtualOffset.y = value
self:setVirtualOffset(virtualOffset)
signalcall(self.onScrollChange, self, virtualOffset)
end)
self:updateScrollBars()
end
function UIScrollArea:setHorizontalScrollBar(scrollbar)
self.horizontalScrollBar = scrollbar
connect(self.horizontalScrollBar, 'onValueChange', function(scrollbar, value)
local virtualOffset = self:getVirtualOffset()
virtualOffset.x = value
self:setVirtualOffset(virtualOffset)
signalcall(self.onScrollChange, self, virtualOffset)
end)
self:updateScrollBars()
end
function UIScrollArea:setInverted(inverted)
self.inverted = inverted
end
function UIScrollArea:setAlwaysScrollMaximum(value)
self.alwaysScrollMaximum = value
end
function UIScrollArea:onLayoutUpdate()
self:updateScrollBars()
end
function UIScrollArea:onMouseWheel(mousePos, mouseWheel)
if self.verticalScrollBar then
if not self.verticalScrollBar:isOn() then
return false
end
if mouseWheel == MouseWheelUp then
local minimum = self.verticalScrollBar:getMinimum()
if self.verticalScrollBar:getValue() <= minimum then
return false
end
self.verticalScrollBar:decrement()
else
local maximum = self.verticalScrollBar:getMaximum()
if self.verticalScrollBar:getValue() >= maximum then
return false
end
self.verticalScrollBar:increment()
end
elseif self.horizontalScrollBar then
if not self.horizontalScrollBar:isOn() then
return false
end
if mouseWheel == MouseWheelUp then
local maximum = self.horizontalScrollBar:getMaximum()
if self.horizontalScrollBar:getValue() >= maximum then
return false
end
self.horizontalScrollBar:increment()
else
local minimum = self.horizontalScrollBar:getMinimum()
if self.horizontalScrollBar:getValue() <= minimum then
return false
end
self.horizontalScrollBar:decrement()
end
end
return true
end
function UIScrollArea:ensureChildVisible(child)
if child then
local paddingRect = self:getPaddingRect()
if self.verticalScrollBar then
local deltaY = paddingRect.y - child:getY()
if deltaY > 0 then
self.verticalScrollBar:decrement(deltaY)
end
deltaY = (child:getY() + child:getHeight()) - (paddingRect.y + paddingRect.height)
if deltaY > 0 then
self.verticalScrollBar:increment(deltaY)
end
elseif self.horizontalScrollBar then
local deltaX = paddingRect.x - child:getX()
if deltaX > 0 then
self.horizontalScrollBar:decrement(deltaX)
end
deltaX = (child:getX() + child:getWidth()) - (paddingRect.x + paddingRect.width)
if deltaX > 0 then
self.horizontalScrollBar:increment(deltaX)
end
end
end
end
function UIScrollArea:onChildFocusChange(focusedChild, oldFocused, reason)
if focusedChild and (reason == MouseFocusReason or reason == KeyboardFocusReason) then
self:ensureChildVisible(focusedChild)
end
end
function UIScrollArea:onScrollWidthChange()
if self.alwaysScrollMaximum and self.horizontalScrollBar then
self.horizontalScrollBar:setValue(self.horizontalScrollBar:getMaximum())
end
end
function UIScrollArea:onScrollHeightChange()
if self.alwaysScrollMaximum and self.verticalScrollBar then
self.verticalScrollBar:setValue(self.verticalScrollBar:getMaximum())
end
end

View File

@@ -0,0 +1,290 @@
-- @docclass
UIScrollBar = extends(UIWidget, "UIScrollBar")
-- private functions
local function calcValues(self)
local slider = self:getChildById('sliderButton')
local decrementButton = self:getChildById('decrementButton')
local incrementButton = self:getChildById('incrementButton')
local pxrange, center
if self.orientation == 'vertical' then
pxrange = (self:getHeight() - decrementButton:getHeight() - decrementButton:getMarginTop() - decrementButton:getMarginBottom()
- incrementButton:getHeight() - incrementButton:getMarginTop() - incrementButton:getMarginBottom())
center = self:getY() + math.floor(self:getHeight() / 2)
else -- horizontal
pxrange = (self:getWidth() - decrementButton:getWidth() - decrementButton:getMarginLeft() - decrementButton:getMarginRight()
- incrementButton:getWidth() - incrementButton:getMarginLeft() - incrementButton:getMarginRight())
center = self:getX() + math.floor(self:getWidth() / 2)
end
local range = self.maximum - self.minimum + 1
local proportion
if self.pixelsScroll then
proportion = pxrange/(range+pxrange)
else
proportion = math.min(math.max(self.step, 1), range)/range
end
local px = math.max(proportion * pxrange, 6)
if g_app.isMobile() then
px = math.max(proportion * pxrange, 24)
end
px = px - px % 2 + 1
local offset = 0
if range == 0 or self.value == self.minimum then
if self.orientation == 'vertical' then
offset = -math.floor((self:getHeight() - px) / 2) + decrementButton:getMarginRect().height
else
offset = -math.floor((self:getWidth() - px) / 2) + decrementButton:getMarginRect().width
end
elseif range > 1 and self.value == self.maximum then
if self.orientation == 'vertical' then
offset = math.ceil((self:getHeight() - px) / 2) - incrementButton:getMarginRect().height
else
offset = math.ceil((self:getWidth() - px) / 2) - incrementButton:getMarginRect().width
end
elseif range > 1 then
offset = (((self.value - self.minimum) / (range - 1)) - 0.5) * (pxrange - px)
end
return range, pxrange, px, offset, center
end
local function updateValueDisplay(widget)
if widget == nil then return end
if widget:getShowValue() then
widget:setText(widget:getValue() .. (widget:getSymbol() or ''))
end
end
local function updateSlider(self)
local slider = self:getChildById('sliderButton')
if slider == nil then return end
local range, pxrange, px, offset, center = calcValues(self)
if self.orientation == 'vertical' then
slider:setHeight(px)
slider:setMarginTop(offset)
else -- horizontal
slider:setWidth(px)
slider:setMarginLeft(offset)
end
updateValueDisplay(self)
local status = (self.maximum ~= self.minimum)
self:setOn(status)
for _i,child in pairs(self:getChildren()) do
child:setEnabled(status)
end
end
local function parseSliderPos(self, slider, pos, move)
local delta, hotDistance
if self.orientation == 'vertical' then
delta = move.y
hotDistance = pos.y - slider:getY()
else
delta = move.x
hotDistance = pos.x - slider:getX()
end
if (delta > 0 and hotDistance + delta > self.hotDistance) or
(delta < 0 and hotDistance + delta < self.hotDistance) then
local range, pxrange, px, offset, center = calcValues(self)
local newvalue = self.value + delta * (range / (pxrange - px))
self:setValue(newvalue)
end
end
local function parseSliderPress(self, slider, pos, button)
if self.orientation == 'vertical' then
self.hotDistance = pos.y - slider:getY()
else
self.hotDistance = pos.x - slider:getX()
end
end
-- public functions
function UIScrollBar.create()
local scrollbar = UIScrollBar.internalCreate()
scrollbar:setFocusable(false)
scrollbar.value = 0
scrollbar.minimum = -999999
scrollbar.maximum = 999999
scrollbar.step = 1
scrollbar.orientation = 'vertical'
scrollbar.pixelsScroll = false
scrollbar.showValue = false
scrollbar.symbol = nil
scrollbar.mouseScroll = true
return scrollbar
end
function UIScrollBar:onSetup()
self.setupDone = true
local sliderButton = self:getChildById('sliderButton')
g_mouse.bindAutoPress(self:getChildById('decrementButton'), function() self:onDecrement() end, 300)
g_mouse.bindAutoPress(self:getChildById('incrementButton'), function() self:onIncrement() end, 300)
g_mouse.bindPressMove(sliderButton, function(mousePos, mouseMoved) parseSliderPos(self, sliderButton, mousePos, mouseMoved) end)
g_mouse.bindPress(sliderButton, function(mousePos, mouseButton) parseSliderPress(self, sliderButton, mousePos, mouseButton) end)
updateSlider(self)
end
function UIScrollBar:onStyleApply(styleName, styleNode)
for name,value in pairs(styleNode) do
if name == 'maximum' then
self:setMaximum(tonumber(value))
elseif name == 'minimum' then
self:setMinimum(tonumber(value))
elseif name == 'step' then
self:setStep(tonumber(value))
elseif name == 'orientation' then
self:setOrientation(value)
elseif name == 'value' then
self:setValue(value)
elseif name == 'pixels-scroll' then
self.pixelsScroll = true
elseif name == 'show-value' then
self.showValue = true
elseif name == 'symbol' then
self.symbol = value
elseif name == 'mouse-scroll' then
self.mouseScroll = value
end
end
end
function UIScrollBar:onDecrement()
if g_keyboard.isCtrlPressed() then
self:decrement(self.value)
elseif g_keyboard.isShiftPressed() then
self:decrement(10)
else
self:decrement()
end
end
function UIScrollBar:onIncrement()
if g_keyboard.isCtrlPressed() then
self:increment(self.maximum)
elseif g_keyboard.isShiftPressed() then
self:increment(10)
else
self:increment()
end
end
function UIScrollBar:decrement(count)
count = count or self.step
self:setValue(self.value - count)
end
function UIScrollBar:increment(count)
count = count or self.step
self:setValue(self.value + count)
end
function UIScrollBar:setMaximum(maximum)
if maximum == self.maximum then return end
self.maximum = maximum
if self.minimum > maximum then
self:setMinimum(maximum)
end
if self.value > maximum then
self:setValue(maximum)
else
updateSlider(self)
end
end
function UIScrollBar:setMinimum(minimum)
if minimum == self.minimum then return end
self.minimum = minimum
if self.maximum < minimum then
self:setMaximum(minimum)
end
if self.value < minimum then
self:setValue(minimum)
else
updateSlider(self)
end
end
function UIScrollBar:setRange(minimum, maximum)
self:setMinimum(minimum)
self:setMaximum(maximum)
end
function UIScrollBar:setValue(value)
value = math.max(math.min(value, self.maximum), self.minimum)
if self.value == value then return end
local delta = value - self.value
self.value = value
updateSlider(self)
if self.setupDone then
signalcall(self.onValueChange, self, math.round(value), delta)
end
end
function UIScrollBar:setMouseScroll(scroll)
self.mouseScroll = scroll
end
function UIScrollBar:setStep(step)
self.step = step
end
function UIScrollBar:setOrientation(orientation)
self.orientation = orientation
end
function UIScrollBar:setText(text)
local valueLabel = self:getChildById('valueLabel')
if valueLabel then
valueLabel:setText(text)
end
end
function UIScrollBar:onGeometryChange()
updateSlider(self)
end
function UIScrollBar:onMouseWheel(mousePos, mouseWheel)
if not self.mouseScroll or not self:isOn() or self.disableScroll then
return false
end
if mouseWheel == MouseWheelUp then
if self.orientation == 'vertical' then
if self.value <= self.minimum then return false end
self:decrement()
else
if self.value >= self.maximum then return false end
self:increment()
end
else
if self.orientation == 'vertical' then
if self.value >= self.maximum then return false end
self:increment()
else
if self.value <= self.minimum then return false end
self:decrement()
end
end
return true
end
function UIScrollBar:getMaximum() return self.maximum end
function UIScrollBar:getMinimum() return self.minimum end
function UIScrollBar:getValue() return math.round(self.value) end
function UIScrollBar:getStep() return self.step end
function UIScrollBar:getOrientation() return self.orientation end
function UIScrollBar:getShowValue() return self.showValue end
function UIScrollBar:getSymbol() return self.symbol end
function UIScrollBar:getMouseScroll() return self.mouseScroll end

View File

@@ -0,0 +1,191 @@
-- @docclass
UISpinBox = extends(UITextEdit, "UISpinBox")
function UISpinBox.create()
local spinbox = UISpinBox.internalCreate()
spinbox:setFocusable(false)
spinbox:setValidCharacters('0123456789')
spinbox.displayButtons = true
spinbox.minimum = 0
spinbox.maximum = 1
spinbox.value = 0
spinbox.step = 1
spinbox.firstchange = true
spinbox.mouseScroll = true
spinbox:setText("1")
spinbox:setValue(1)
return spinbox
end
function UISpinBox:onSetup()
g_mouse.bindAutoPress(self:getChildById('up'), function() self:up() end, 300)
g_mouse.bindAutoPress(self:getChildById('down'), function() self:down() end, 300)
end
function UISpinBox:onMouseWheel(mousePos, direction)
if not self.mouseScroll or self.disableScroll then
return false
end
if direction == MouseWheelUp then
self:up()
elseif direction == MouseWheelDown then
self:down()
end
return true
end
function UISpinBox:onKeyPress()
if self.firstchange then
self.firstchange = false
self:setText('')
end
return false
end
function UISpinBox:onTextChange(text, oldText)
if text:len() == 0 then
self:setValue(self.minimum)
return
end
local number = tonumber(text)
if not number then
self:setText(number)
return
else
if number < self.minimum then
self:setText(self.minimum)
return
elseif number > self.maximum then
self:setText(self.maximum)
return
end
end
self:setValue(number)
end
function UISpinBox:onValueChange(value)
-- nothing to do
end
function UISpinBox:onFocusChange(focused)
if not focused then
if self:getText():len() == 0 then
self:setText(self.minimum)
end
end
end
function UISpinBox:onStyleApply(styleName, styleNode)
for name, value in pairs(styleNode) do
if name == 'maximum' then
self.maximum = value
addEvent(function() self:setMaximum(value) end)
elseif name == 'minimum' then
self.minimum = value
addEvent(function() self:setMinimum(value) end)
elseif name == 'mouse-scroll' then
addEvent(function() self:setMouseScroll(value) end)
elseif name == 'buttons' then
addEvent(function()
if value then
self:showButtons()
else
self:hideButtons()
end
end)
end
end
end
function UISpinBox:showButtons()
self:getChildById('up'):show()
self:getChildById('down'):show()
self.displayButtons = true
end
function UISpinBox:hideButtons()
self:getChildById('up'):hide()
self:getChildById('down'):hide()
self.displayButtons = false
end
function UISpinBox:up()
self:setValue(self.value + self.step)
end
function UISpinBox:down()
self:setValue(self.value - self.step)
end
function UISpinBox:setValue(value, dontSignal)
if type(value) == "string" then
value = tonumber(value)
end
value = value or 0
value = math.max(math.min(self.maximum, value), self.minimum)
if value == self.value then return end
self.value = value
if self:getText():len() > 0 then
self:setText(value)
end
local upButton = self:getChildById('up')
local downButton = self:getChildById('down')
if upButton then
upButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.maximum)
end
if downButton then
downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum)
end
if not dontSignal then
signalcall(self.onValueChange, self, value)
end
end
function UISpinBox:getValue()
return self.value
end
function UISpinBox:setMinimum(minimum)
minimum = minimum or -9223372036854775808
self.minimum = minimum
if self.minimum > self.maximum then
self.maximum = self.minimum
end
if self.value < minimum then
self:setValue(minimum)
end
end
function UISpinBox:getMinimum()
return self.minimum
end
function UISpinBox:setMaximum(maximum)
maximum = maximum or 9223372036854775807
self.maximum = maximum
if self.value > maximum then
self:setValue(maximum)
end
end
function UISpinBox:getMaximum()
return self.maximum
end
function UISpinBox:setStep(step)
self.step = step or 1
end
function UISpinBox:setMouseScroll(mouseScroll)
self.mouseScroll = mouseScroll
end
function UISpinBox:getMouseScroll()
return self.mouseScroll
end

View File

@@ -0,0 +1,85 @@
-- @docclass
UISplitter = extends(UIWidget, "UISplitter")
function UISplitter.create()
local splitter = UISplitter.internalCreate()
splitter:setFocusable(false)
splitter.relativeMargin = 'bottom'
return splitter
end
function UISplitter:onHoverChange(hovered)
-- Check if margin can be changed
local margin = (self.vertical and self:getMarginBottom() or self:getMarginRight())
if hovered and (self:canUpdateMargin(margin + 1) ~= margin or self:canUpdateMargin(margin - 1) ~= margin) then
if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end
if self:getWidth() > self:getHeight() then
self.vertical = true
self.cursortype = 'vertical'
else
self.vertical = false
self.cursortype = 'horizontal'
end
self.hovering = true
g_mouse.pushCursor(self.cursortype)
if not self:isPressed() then
g_effects.fadeIn(self)
end
else
if not self:isPressed() and self.hovering then
g_mouse.popCursor(self.cursortype)
g_effects.fadeOut(self)
self.hovering = false
end
end
end
function UISplitter:onMouseMove(mousePos, mouseMoved)
if self:isPressed() then
--local currentmargin, newmargin, delta
if self.vertical then
local delta = mousePos.y - self:getY() - self:getHeight()/2
local newMargin = self:canUpdateMargin(self:getMarginBottom() - delta)
local currentMargin = self:getMarginBottom()
if newMargin ~= currentMargin then
self.newMargin = newMargin
if not self.event or self.event:isExecuted() then
self.event = addEvent(function()
self:setMarginBottom(self.newMargin)
end)
end
end
else
local delta = mousePos.x - self:getX() - self:getWidth()/2
local newMargin = self:canUpdateMargin(self:getMarginRight() - delta)
local currentMargin = self:getMarginRight()
if newMargin ~= currentMargin then
self.newMargin = newMargin
if not self.event or self.event:isExecuted() then
self.event = addEvent(function()
self:setMarginRight(self.newMargin)
end)
end
end
end
return true
end
end
function UISplitter:onMouseRelease(mousePos, mouseButton)
if not self:isHovered() then
g_mouse.popCursor(self.cursortype)
g_effects.fadeOut(self)
self.hovering = false
end
end
function UISplitter:onStyleApply(styleName, styleNode)
if styleNode['relative-margin'] then
self.relativeMargin = styleNode['relative-margin']
end
end
function UISplitter:canUpdateMargin(newMargin)
return newMargin
end

View File

@@ -0,0 +1,163 @@
-- @docclass
UITabBar = extends(UIWidget, "UITabBar")
-- private functions
local function onTabClick(tab)
tab.tabBar:selectTab(tab)
end
local function onTabMouseRelease(tab, mousePos, mouseButton)
if mouseButton == MouseRightButton and tab:containsPoint(mousePos) then
signalcall(tab.tabBar.onTabLeftClick, tab.tabBar, tab)
end
end
-- public functions
function UITabBar.create()
local tabbar = UITabBar.internalCreate()
tabbar:setFocusable(false)
tabbar.tabs = {}
return tabbar
end
function UITabBar:onSetup()
self.buttonsPanel = self:getChildById('buttonsPanel')
end
function UITabBar:setContentWidget(widget)
self.contentWidget = widget
if #self.tabs > 0 then
self.contentWidget:addChild(self.tabs[1].tabPanel)
end
end
function UITabBar:addTab(text, panel, icon)
if panel == nil then
panel = g_ui.createWidget(self:getStyleName() .. 'Panel')
panel:setId('tabPanel')
end
local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel)
panel.isTab = true
tab.tabPanel = panel
tab.tabBar = self
tab:setId('tab')
tab:setText(text)
tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight())
tab.onClick = onTabClick
tab.onMouseRelease = onTabMouseRelease
tab.onDestroy = function() tab.tabPanel:destroy() end
table.insert(self.tabs, tab)
if #self.tabs == 1 then
self:selectTab(tab)
end
local tabStyle = {}
tabStyle['icon-source'] = icon
tab:mergeStyle(tabStyle)
return tab
end
function UITabBar:addButton(text, func, icon)
local button = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel)
button:setText(text)
local style = {}
style['icon-source'] = icon
button:mergeStyle(style)
button.onClick = func
return button
end
function UITabBar:removeTab(tab)
local index = table.find(self.tabs, tab)
if index == nil then return end
if self.currentTab == tab then
self:selectPrevTab()
end
table.remove(self.tabs, index)
tab:destroy()
end
function UITabBar:getTab(text)
for k,tab in pairs(self.tabs) do
if tab:getText():lower() == text:lower() then
return tab
end
end
end
function UITabBar:selectTab(tab)
if self.currentTab == tab then return end
if self.contentWidget then
local selectedWidget = self.contentWidget:getLastChild()
if selectedWidget and selectedWidget.isTab then
self.contentWidget:removeChild(selectedWidget)
end
self.contentWidget:addChild(tab.tabPanel)
tab.tabPanel:fill('parent')
end
if self.currentTab then
self.currentTab:setChecked(false)
end
signalcall(self.onTabChange, self, tab)
self.currentTab = tab
tab:setChecked(true)
tab:setOn(false)
local parent = tab:getParent()
if parent then
parent:focusChild(tab, MouseFocusReason)
end
end
function UITabBar:selectNextTab()
if self.currentTab == nil then return end
local index = table.find(self.tabs, self.currentTab)
if index == nil then return end
local nextTab = self.tabs[index + 1] or self.tabs[1]
if not nextTab then return end
self:selectTab(nextTab)
end
function UITabBar:selectPrevTab()
if self.currentTab == nil then return end
local index = table.find(self.tabs, self.currentTab)
if index == nil then return end
local prevTab = self.tabs[index - 1] or self.tabs[#self.tabs]
if not prevTab then return end
self:selectTab(prevTab)
end
function UITabBar:getTabPanel(tab)
return tab.tabPanel
end
function UITabBar:getCurrentTabPanel()
if self.currentTab then
return self.currentTab.tabPanel
end
end
function UITabBar:getCurrentTab()
return self.currentTab
end
function UITabBar:getTabs()
return self.tabs
end
function UITabBar:getTabsPanel()
return table.collect(self.tabs, function(_,tab) return tab.tabPanel end)
end
function UITabBar:clearTabs()
while #self.tabs > 0 do
self:removeTab(self.tabs[#self.tabs])
end
end

View File

@@ -0,0 +1,432 @@
-- @docclass
--[[
TODO:
* Make table headers more robust.
* Get dynamic row heights working with text wrapping.
]]
TABLE_SORTING_ASC = 0
TABLE_SORTING_DESC = 1
UITable = extends(UIWidget, "UITable")
-- Initialize default values
function UITable.create()
local table = UITable.internalCreate()
table.headerRow = nil
table.headerColumns = {}
table.dataSpace = nil
table.rows = {}
table.rowBaseStyle = nil
table.columns = {}
table.columnWidth = {}
table.columBaseStyle = nil
table.headerRowBaseStyle = nil
table.headerColumnBaseStyle = nil
table.selectedRow = nil
table.defaultColumnWidth = 80
table.sortColumn = -1
table.sortType = TABLE_SORTING_ASC
table.autoSort = false
return table
end
-- Clear table values
function UITable:onDestroy()
for _,row in pairs(self.rows) do
row.onClick = nil
end
self.rows = {}
self.columns = {}
self.headerRow = nil
self.headerColumns = {}
self.columnWidth = {}
self.selectedRow = nil
if self.dataSpace then
self.dataSpace:destroyChildren()
self.dataSpace = nil
end
end
-- Detect if a header is already defined
function UITable:onSetup()
local header = self:getChildById('header')
if header then
self:setHeader(header)
end
end
-- Parse table related styles
function UITable:onStyleApply(styleName, styleNode)
for name, value in pairs(styleNode) do
if value ~= false then
if name == 'table-data' then
addEvent(function()
self:setTableData(self:getParent():getChildById(value))
end)
elseif name == 'column-style' then
addEvent(function()
self:setColumnStyle(value)
end)
elseif name == 'row-style' then
addEvent(function()
self:setRowStyle(value)
end)
elseif name == 'header-column-style' then
addEvent(function()
self:setHeaderColumnStyle(value)
end)
elseif name == 'header-row-style' then
addEvent(function()
self:setHeaderRowStyle(value)
end)
end
end
end
end
function UITable:setColumnWidth(width)
if self:hasHeader() then return end
self.columnWidth = width
end
function UITable:setDefaultColumnWidth(width)
self.defaultColumnWidth = width
end
-- Check if the table has a header
function UITable:hasHeader()
return self.headerRow ~= nil
end
-- Clear all rows
function UITable:clearData()
if not self.dataSpace then
return
end
self.dataSpace:destroyChildren()
self.selectedRow = nil
self.columns = {}
self.rows = {}
end
-- Set existing child as header
function UITable:setHeader(headerWidget)
self:removeHeader()
if self.dataSpace then
local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop()
self.dataSpace:applyStyle({ height = newHeight })
end
self.headerColumns = {}
self.columnWidth = {}
for colId, column in pairs(headerWidget:getChildren()) do
column.colId = colId
column.table = self
table.insert(self.columnWidth, column:getWidth())
table.insert(self.headerColumns, column)
end
self.headerRow = headerWidget
end
-- Create and add header from table data
function UITable:addHeader(data)
if not data or type(data) ~= 'table' then
g_logger.error('UITable:addHeaderRow - table columns must be provided in a table')
return
end
self:removeHeader()
-- build header columns
local columns = {}
for colId, column in pairs(data) do
local col = g_ui.createWidget(self.headerColumnBaseStyle)
col.colId = colId
col.table = self
for type, value in pairs(column) do
if type == 'width' then
col:setWidth(value)
elseif type == 'height' then
col:setHeight(value)
elseif type == 'text' then
col:setText(value)
elseif type == 'onClick' then
col.onClick = value
end
end
table.insert(columns, col)
end
-- create a new header
local headerRow = g_ui.createWidget(self.headerRowBaseStyle, self)
local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop()
self.dataSpace:applyStyle({ height = newHeight })
headerRow:setId('header')
self.headerColumns = {}
self.columnWidth = {}
for _, column in pairs(columns) do
headerRow:addChild(column)
table.insert(self.columnWidth, column:getWidth())
table.insert(self.headerColumns, column)
end
self.headerRow = headerRow
return headerRow
end
-- Remove header
function UITable:removeHeader()
if self:hasHeader() then
if self.dataSpace then
local newHeight = self.dataSpace:getHeight()+self.headerRow:getHeight()+self.dataSpace:getMarginTop()
self.dataSpace:applyStyle({ height = newHeight })
end
self.headerColumns = {}
self.columnWidth = {}
self.headerRow:destroy()
self.headerRow = nil
end
end
function UITable:addRow(data, height)
if not self.dataSpace then
g_logger.error('UITable:addRow - table data space has not been set, cannot add rows.')
return
end
if not data or type(data) ~= 'table' then
g_logger.error('UITable:addRow - table columns must be provided in a table.')
return
end
local row = g_ui.createWidget(self.rowBaseStyle)
row.table = self
if height then row:setHeight(height) end
local rowId = #self.rows + 1
row.rowId = rowId
row:setId('row'..rowId)
row:updateBackgroundColor()
self.columns[rowId] = {}
for colId, column in pairs(data) do
local col = g_ui.createWidget(self.columBaseStyle, row)
if column.width then
col:setWidth(column.width)
else
col:setWidth(self.columnWidth[colId] or self.defaultColumnWidth)
end
if column.height then
col:setHeight(column.height)
end
if column.text then
col:setText(column.text)
end
if column.sortvalue then
col.sortvalue = column.sortvalue
else
col.sortvalue = column.text or 0
end
table.insert(self.columns[rowId], col)
end
self.dataSpace:addChild(row)
table.insert(self.rows, row)
if self.autoSort then
self:sort()
end
return row
end
-- Update row indices and background color
function UITable:updateRows()
for rowId = 1, #self.rows do
local row = self.rows[rowId]
row.rowId = rowId
row:setId('row'..rowId)
row:updateBackgroundColor()
end
end
-- Removes the given row widget from the table
function UITable:removeRow(row)
if self.selectedRow == row then
self:selectRow(nil)
end
row.onClick = nil
row.table = nil
table.remove(self.columns, row.rowId)
table.remove(self.rows, row.rowId)
self.dataSpace:removeChild(row)
self:updateRows()
end
function UITable:toggleSorting(enabled)
self.autoSort = enabled
end
function UITable:setSorting(colId, sortType)
self.headerColumns[colId]:focus()
if sortType then
self.sortType = sortType
elseif self.sortColumn == colId then
if self.sortType == TABLE_SORTING_ASC then
self.sortType = TABLE_SORTING_DESC
else
self.sortType = TABLE_SORTING_ASC
end
else
self.sortType = TABLE_SORTING_ASC
end
self.sortColumn = colId
end
function UITable:sort()
if self.sortColumn <= 0 then
return
end
if self.sortType == TABLE_SORTING_ASC then
table.sort(self.rows, function(rowA, b)
return rowA:getChildByIndex(self.sortColumn).sortvalue < b:getChildByIndex(self.sortColumn).sortvalue
end)
else
table.sort(self.rows, function(rowA, b)
return rowA:getChildByIndex(self.sortColumn).sortvalue > b:getChildByIndex(self.sortColumn).sortvalue
end)
end
if self.dataSpace then
for _, child in pairs(self.dataSpace:getChildren()) do
self.dataSpace:removeChild(child)
end
end
self:updateRows()
self.columns = {}
for _, row in pairs(self.rows) do
if self.dataSpace then
self.dataSpace:addChild(row)
end
self.columns[row.rowId] = {}
for _, column in pairs(row:getChildren()) do
table.insert(self.columns[row.rowId], column)
end
end
end
function UITable:selectRow(selectedRow)
if selectedRow == self.selectedRow then return end
local previousSelectedRow = self.selectedRow
self.selectedRow = selectedRow
if previousSelectedRow then
previousSelectedRow:setChecked(false)
end
if selectedRow then
selectedRow:setChecked(true)
end
signalcall(self.onSelectionChange, self, selectedRow, previousSelectedRow)
end
function UITable:setTableData(tableData)
local headerHeight = 0
if self.headerRow then
headerHeight = self.headerRow:getHeight()
end
self.dataSpace = tableData
self.dataSpace:applyStyle({ height = self:getHeight()-headerHeight-self:getMarginTop() })
end
function UITable:setRowStyle(style, dontUpdate)
self.rowBaseStyle = style
if not dontUpdate then
for _, row in pairs(self.rows) do
row:setStyle(style)
end
end
end
function UITable:setColumnStyle(style, dontUpdate)
self.columBaseStyle = style
if not dontUpdate then
for _, columns in pairs(self.columns) do
for _, col in pairs(columns) do
col:setStyle(style)
end
end
end
end
function UITable:setHeaderRowStyle(style)
self.headerRowBaseStyle = style
if self.headerRow then
self.headerRow:setStyle(style)
end
end
function UITable:setHeaderColumnStyle(style)
self.headerColumnBaseStyle = style
for _, col in pairs(self.headerColumns) do
col:setStyle(style)
end
end
UITableRow = extends(UIWidget, "UITableRow")
function UITableRow:onFocusChange(focused)
if focused then
if self.table then self.table:selectRow(self) end
end
end
function UITableRow:onStyleApply(styleName, styleNode)
for name,value in pairs(styleNode) do
if name == 'even-background-color' then
self.evenBackgroundColor = value
elseif name == 'odd-background-color' then
self.oddBackgroundColor = value
end
end
end
function UITableRow:updateBackgroundColor()
self.backgroundColor = nil
local isEven = (self.rowId % 2 == 0)
if isEven and self.evenBackgroundColor then
self.backgroundColor = self.evenBackgroundColor
elseif not isEven and self.oddBackgroundColor then
self.backgroundColor = self.oddBackgroundColor
end
if self.backgroundColor then
self:mergeStyle({ ['background-color'] = self.backgroundColor })
end
end
UITableHeaderColumn = extends(UIButton, "UITableHeaderColumn")
function UITableHeaderColumn:onClick()
if self.table then
self.table:setSorting(self.colId)
self.table:sort()
end
end

View File

@@ -0,0 +1,78 @@
function UITextEdit:onStyleApply(styleName, styleNode)
for name,value in pairs(styleNode) do
if name == 'vertical-scrollbar' then
addEvent(function()
self:setVerticalScrollBar(self:getParent():getChildById(value))
end)
elseif name == 'horizontal-scrollbar' then
addEvent(function()
self:setHorizontalScrollBar(self:getParent():getChildById(value))
end)
end
end
end
function UITextEdit:onMouseWheel(mousePos, mouseWheel)
if self.verticalScrollBar and self:isMultiline() then
if mouseWheel == MouseWheelUp then
self.verticalScrollBar:decrement()
else
self.verticalScrollBar:increment()
end
return true
elseif self.horizontalScrollBar then
if mouseWheel == MouseWheelUp then
self.horizontalScrollBar:increment()
else
self.horizontalScrollBar:decrement()
end
return true
end
end
function UITextEdit:onTextAreaUpdate(virtualOffset, virtualSize, totalSize)
self:updateScrollBars()
end
function UITextEdit:setVerticalScrollBar(scrollbar)
self.verticalScrollBar = scrollbar
self.verticalScrollBar.onValueChange = function(scrollbar, value)
local virtualOffset = self:getTextVirtualOffset()
virtualOffset.y = value
self:setTextVirtualOffset(virtualOffset)
end
self:updateScrollBars()
end
function UITextEdit:setHorizontalScrollBar(scrollbar)
self.horizontalScrollBar = scrollbar
self.horizontalScrollBar.onValueChange = function(scrollbar, value)
local virtualOffset = self:getTextVirtualOffset()
virtualOffset.x = value
self:setTextVirtualOffset(virtualOffset)
end
self:updateScrollBars()
end
function UITextEdit:updateScrollBars()
local scrollSize = self:getTextTotalSize()
local scrollWidth = math.max(scrollSize.width - self:getTextVirtualSize().width, 0)
local scrollHeight = math.max(scrollSize.height - self:getTextVirtualSize().height, 0)
local scrollbar = self.verticalScrollBar
if scrollbar then
scrollbar:setMinimum(0)
scrollbar:setMaximum(scrollHeight)
scrollbar:setValue(self:getTextVirtualOffset().y)
end
local scrollbar = self.horizontalScrollBar
if scrollbar then
scrollbar:setMinimum(0)
scrollbar:setMaximum(scrollWidth)
scrollbar:setValue(self:getTextVirtualOffset().x)
end
end
-- todo: ontext change, focus to cursor

View File

@@ -0,0 +1,21 @@
-- @docclass UIWidget
function UIWidget:setMargin(...)
local params = {...}
if #params == 1 then
self:setMarginTop(params[1])
self:setMarginRight(params[1])
self:setMarginBottom(params[1])
self:setMarginLeft(params[1])
elseif #params == 2 then
self:setMarginTop(params[1])
self:setMarginRight(params[2])
self:setMarginBottom(params[1])
self:setMarginLeft(params[2])
elseif #params == 4 then
self:setMarginTop(params[1])
self:setMarginRight(params[2])
self:setMarginBottom(params[3])
self:setMarginLeft(params[4])
end
end

View File

@@ -0,0 +1,46 @@
-- @docclass
UIWindow = extends(UIWidget, "UIWindow")
function UIWindow.create()
local window = UIWindow.internalCreate()
window:setTextAlign(AlignTopCenter)
window:setDraggable(true)
window:setAutoFocusPolicy(AutoFocusFirst)
return window
end
function UIWindow:onKeyDown(keyCode, keyboardModifiers)
if keyboardModifiers == KeyboardNoModifier then
if keyCode == KeyEnter then
signalcall(self.onEnter, self)
elseif keyCode == KeyEscape then
signalcall(self.onEscape, self)
end
end
end
function UIWindow:onFocusChange(focused)
if focused then self:raise() end
end
function UIWindow:onDragEnter(mousePos)
if self.static then
return false
end
self:breakAnchors()
self.movingReference = { x = mousePos.x - self:getX(), y = mousePos.y - self:getY() }
return true
end
function UIWindow:onDragLeave(droppedWidget, mousePos)
-- TODO: auto detect and reconnect anchors
end
function UIWindow:onDragMove(mousePos, mouseMoved)
if self.static then
return
end
local pos = { x = mousePos.x - self.movingReference.x, y = mousePos.y - self.movingReference.y }
self:setPosition(pos)
self:bindRectToParent()
end

View File

@@ -0,0 +1,376 @@
-- @docfuncs @{
function print(...)
local msg = ""
local args = {...}
local appendSpace = #args > 1
for i,v in ipairs(args) do
msg = msg .. tostring(v)
if appendSpace and i < #args then
msg = msg .. ' '
end
end
g_logger.log(LogInfo, msg)
end
function pinfo(msg)
g_logger.log(LogInfo, msg)
end
function perror(msg)
g_logger.log(LogError, msg)
end
function pwarning(msg)
g_logger.log(LogWarning, msg)
end
function pdebug(msg)
g_logger.log(LogDebug, msg)
end
function fatal(msg)
g_logger.log(LogFatal, msg)
end
function exit()
g_app.exit()
end
function quit()
g_app.exit()
end
function connect(object, arg1, arg2, arg3)
local signalsAndSlots
local pushFront
if type(arg1) == 'string' then
signalsAndSlots = { [arg1] = arg2 }
pushFront = arg3
else
signalsAndSlots = arg1
pushFront = arg2
end
for signal,slot in pairs(signalsAndSlots) do
if not object[signal] then
local mt = getmetatable(object)
if mt and type(object) == 'userdata' then
object[signal] = function(...)
return signalcall(mt[signal], ...)
end
end
end
if not object[signal] then
object[signal] = slot
elseif type(object[signal]) == 'function' then
object[signal] = { object[signal] }
end
if type(slot) ~= 'function' then
perror(debug.traceback('unable to connect a non function value'))
end
if type(object[signal]) == 'table' then
if pushFront then
table.insert(object[signal], 1, slot)
else
table.insert(object[signal], #object[signal]+1, slot)
end
end
end
end
function disconnect(object, arg1, arg2)
local signalsAndSlots
if type(arg1) == 'string' then
if arg2 == nil then
object[arg1] = nil
return
end
signalsAndSlots = { [arg1] = arg2 }
elseif type(arg1) == 'table' then
signalsAndSlots = arg1
else
perror(debug.traceback('unable to disconnect'))
end
for signal,slot in pairs(signalsAndSlots) do
if not object[signal] then
elseif type(object[signal]) == 'function' then
if object[signal] == slot then
object[signal] = nil
end
elseif type(object[signal]) == 'table' then
for k,func in pairs(object[signal]) do
if func == slot then
table.remove(object[signal], k)
if #object[signal] == 1 then
object[signal] = object[signal][1]
end
break
end
end
end
end
end
function newclass(name)
if not name then
perror(debug.traceback('new class has no name.'))
end
local class = {}
function class.internalCreate()
local instance = {}
for k,v in pairs(class) do
instance[k] = v
end
return instance
end
class.create = class.internalCreate
class.__class = name
class.getClassName = function() return name end
return class
end
function extends(base, name)
if not name then
perror(debug.traceback('extended class has no name.'))
end
local derived = {}
function derived.internalCreate()
local instance = base.create()
for k,v in pairs(derived) do
instance[k] = v
end
return instance
end
derived.create = derived.internalCreate
derived.__class = name
derived.getClassName = function() return name end
return derived
end
function runinsandbox(func, ...)
if type(func) == 'string' then
func, err = loadfile(resolvepath(func, 2))
if not func then
error(err)
end
end
local env = { }
local oldenv = getfenv(0)
setmetatable(env, { __index = oldenv } )
setfenv(0, env)
func(...)
setfenv(0, oldenv)
return env
end
function loadasmodule(name, file)
file = file or resolvepath(name, 2)
if package.loaded[name] then
return package.loaded[name]
end
local env = runinsandbox(file)
package.loaded[name] = env
return env
end
local function module_loader(modname)
local module = g_modules.getModule(modname)
if not module then
return '\n\tno module \'' .. modname .. '\''
end
return function()
if not module:load() then
error('unable to load required module ' .. modname)
end
return module:getSandbox()
end
end
table.insert(package.loaders, 1, module_loader)
function import(table)
assert(type(table) == 'table')
local env = getfenv(2)
for k,v in pairs(table) do
env[k] = v
end
end
function export(what, key)
if key ~= nil then
_G[key] = what
else
for k,v in pairs(what) do
_G[k] = v
end
end
end
function unexport(key)
if type(key) == 'table' then
for _k,v in pairs(key) do
_G[v] = nil
end
else
_G[key] = nil
end
end
function getfsrcpath(depth)
depth = depth or 2
local info = debug.getinfo(1+depth, "Sn")
local path
if info.short_src then
path = info.short_src:match("(.*)/.*")
end
if not path then
path = '/'
elseif path:sub(0, 1) ~= '/' then
path = '/' .. path
end
return path
end
function resolvepath(filePath, depth)
if not filePath then return nil end
depth = depth or 1
if filePath then
if filePath:sub(0, 1) ~= '/' then
local basepath = getfsrcpath(depth+1)
if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
return basepath .. filePath
else
return filePath
end
else
local basepath = getfsrcpath(depth+1)
if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
return basepath
end
end
function toboolean(v)
if type(v) == 'string' then
v = v:trim():lower()
if v == '1' or v == 'true' then
return true
end
elseif type(v) == 'number' then
if v == 1 then
return true
end
elseif type(v) == 'boolean' then
return v
end
return false
end
function fromboolean(boolean)
if boolean then
return 'true'
else
return 'false'
end
end
function booleantonumber(boolean)
if boolean then
return 1
else
return 0
end
end
function numbertoboolean(number)
if number ~= 0 then
return true
else
return false
end
end
function protectedcall(func, ...)
local status, ret = pcall(func, ...)
if status then
return ret
end
perror(ret)
return false
end
function signalcall(param, ...)
if type(param) == 'function' then
local status, ret = pcall(param, ...)
if status then
return ret
else
perror(ret)
end
elseif type(param) == 'table' then
for k,v in pairs(param) do
local status, ret = pcall(v, ...)
if status then
if ret then return true end
else
perror(ret)
end
end
elseif param ~= nil then
error('attempt to call a non function value')
end
return false
end
function tr(s, ...)
return string.format(s, ...)
end
function getOppositeAnchor(anchor)
if anchor == AnchorLeft then
return AnchorRight
elseif anchor == AnchorRight then
return AnchorLeft
elseif anchor == AnchorTop then
return AnchorBottom
elseif anchor == AnchorBottom then
return AnchorTop
elseif anchor == AnchorVerticalCenter then
return AnchorHorizontalCenter
elseif anchor == AnchorHorizontalCenter then
return AnchorVerticalCenter
end
return anchor
end
function makesingleton(obj)
local singleton = {}
if obj.getClassName then
for key,value in pairs(_G[obj:getClassName()]) do
if type(value) == 'function' then
singleton[key] = function(...) return value(obj, ...) end
end
end
end
return singleton
end
function comma_value(amount)
local formatted = amount
while true do
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
if (k==0) then
break
end
end
return formatted
end
-- @}

View File

@@ -0,0 +1,22 @@
local CRASH_FILE = "exception.dmp"
function init()
if g_resources.fileExists(CRASH_FILE) then
local crashLog = g_resources.readFileContents(CRASH_FILE)
local clientLog = g_logger.getLastLog()
HTTP.post(Services.crash, {
version = APP_VERSION,
build = g_app.getVersion(),
os = g_app.getOs(),
platform = g_window.getPlatformType(),
crash = base64.encode(crashLog),
log = base64.encode(clientLog)
}, function(data, err)
if err then
return g_logger.error("Error while reporting crash report: " .. err)
end
g_resources.deleteFile(CRASH_FILE)
end)
end
end

View File

@@ -0,0 +1,8 @@
Module
name: crash_reporter
description: Sends crash log to remote server
author: otclient@otclient.ovh
website: otclient.ovh
reloadable: false
scripts: [ crash_reporter ]
@onLoad: init()

View File

@@ -0,0 +1,620 @@
bottomActionPanel1 = nil
bottomActionPanel2 = nil
bottomActionPanel3 = nil
leftActionPanel1 = nil
leftActionPanel2 = nil
leftActionPanel3 = nil
local settings = {}
local hotkeyAssignWindow
local actionButtonsInPanel = 50
ActionTypes = {
USE = 0,
USE_SELF = 1,
USE_TARGET = 2,
USE_WITH = 3,
EQUIP = 4
}
ActionColors = {
empty = '#00000033',
text = '#00000033',
itemUse = '#8888FF88',
itemUseSelf = '#00FF0088',
itemUseTarget = '#FF000088',
itemUseWith = '#F5B32588',
itemEquip = '#FFFFFF88'
}
function init()
local bottomPanel = modules.game_interface.getBottomActionPanel()
local leftPanel = modules.game_interface.getLeftActionPanel()
local rightPanel = modules.game_interface.getRightActionPanel()
-- bottom
bottomActionPanel1 = g_ui.loadUI('actionbar', bottomPanel)
bottomPanel:moveChildToIndex(bottomActionPanel1, 1)
bottomActionPanel2 = g_ui.loadUI('actionbar', bottomPanel)
bottomPanel:moveChildToIndex(bottomActionPanel2, 1)
bottomActionPanel3 = g_ui.loadUI('actionbar', bottomPanel)
bottomPanel:moveChildToIndex(bottomActionPanel3, 1)
-- left
leftActionPanel1 = g_ui.loadUI('sideactionbar', leftPanel)
leftPanel:moveChildToIndex(leftActionPanel1, 1)
leftActionPanel2 = g_ui.loadUI('sideactionbar', leftPanel)
leftPanel:moveChildToIndex(leftActionPanel2, 1)
leftActionPanel3 = g_ui.loadUI('sideactionbar', leftPanel)
leftPanel:moveChildToIndex(leftActionPanel3, 1)
-- right
rightActionPanel1 = g_ui.loadUI('sideactionbar', rightPanel)
rightPanel:moveChildToIndex(rightActionPanel1, 1)
rightActionPanel2 = g_ui.loadUI('sideactionbar', rightPanel)
rightPanel:moveChildToIndex(rightActionPanel2, 1)
rightActionPanel3 = g_ui.loadUI('sideactionbar', rightPanel)
rightPanel:moveChildToIndex(rightActionPanel3, 1)
connect(g_game, {
onGameStart = online,
onGameEnd = offline,
onSpellGroupCooldown = onSpellGroupCooldown,
onSpellCooldown = onSpellCooldown
})
if g_game.isOnline() then
online()
end
end
function terminate()
disconnect(g_game, {
onGameStart = online,
onGameEnd = offline,
onSpellGroupCooldown = onSpellGroupCooldown,
onSpellCooldown = onSpellCooldown
})
-- remove hotkeys, also saves config
local panels = {
bottomActionPanel1,
bottomActionPanel2,
bottomActionPanel3,
leftActionPanel1,
leftActionPanel2,
leftActionPanel3,
rightActionPanel1,
rightActionPanel2,
rightActionPanel3,
}
for i, panel in ipairs(panels) do
if panel.tabBar:getChildCount() > 0 then
offline()
break
end
end
bottomActionPanel1:destroy()
bottomActionPanel2:destroy()
bottomActionPanel3:destroy()
leftActionPanel1:destroy()
leftActionPanel2:destroy()
leftActionPanel3:destroy()
rightActionPanel1:destroy()
rightActionPanel2:destroy()
rightActionPanel3:destroy()
end
function show()
if not g_game.isOnline() then return end
bottomActionPanel1:setOn(g_settings.getBoolean("actionbarBottom1", false))
bottomActionPanel2:setOn(g_settings.getBoolean("actionbarBottom2", false))
bottomActionPanel3:setOn(g_settings.getBoolean("actionbarBottom3", false))
leftActionPanel1:setOn(g_settings.getBoolean("actionbarLeft1", false))
leftActionPanel2:setOn(g_settings.getBoolean("actionbarLeft2", false))
leftActionPanel3:setOn(g_settings.getBoolean("actionbarLeft3", false))
rightActionPanel1:setOn(g_settings.getBoolean("actionbarRight1", false))
rightActionPanel2:setOn(g_settings.getBoolean("actionbarRight2", false))
rightActionPanel3:setOn(g_settings.getBoolean("actionbarRight3", false))
end
function hide()
bottomActionPanel1:setOn(false)
bottomActionPanel2:setOn(false)
bottomActionPanel3:setOn(false)
leftActionPanel1:setOn(false)
leftActionPanel2:setOn(false)
leftActionPanel3:setOn(false)
rightActionPanel1:setOn(false)
rightActionPanel2:setOn(false)
rightActionPanel3:setOn(false)
end
function switchMode(newMode)
if newMode then
bottomActionPanel1:setImageColor('#ffffff88')
bottomActionPanel2:setImageColor('#ffffff88')
bottomActionPanel3:setImageColor('#ffffff88')
leftActionPanel1:setImageColor('#ffffff88')
leftActionPanel2:setImageColor('#ffffff88')
leftActionPanel3:setImageColor('#ffffff88')
rightActionPanel1:setImageColor('#ffffff88')
rightActionPanel2:setImageColor('#ffffff88')
rightActionPanel3:setImageColor('#ffffff88')
else
bottomActionPanel1:setImageColor('white')
bottomActionPanel2:setImageColor('white')
bottomActionPanel3:setImageColor('white')
leftActionPanel1:setImageColor('white')
leftActionPanel2:setImageColor('white')
leftActionPanel3:setImageColor('white')
rightActionPanel1:setImageColor('white')
rightActionPanel2:setImageColor('white')
rightActionPanel3:setImageColor('white')
end
end
function online()
load()
setupActionPanel(1, bottomActionPanel1, true)
setupActionPanel(2, bottomActionPanel2, true)
setupActionPanel(3, bottomActionPanel3, true)
setupActionPanel(4, leftActionPanel1, false)
setupActionPanel(5, leftActionPanel2, false)
setupActionPanel(6, leftActionPanel3, false)
setupActionPanel(7, rightActionPanel1, false)
setupActionPanel(8, rightActionPanel2, false)
setupActionPanel(9, rightActionPanel3, false)
show()
end
function refresh(reloaded)
offline(reloaded)
online()
end
function offline(reloaded)
hide()
if hotkeyAssignWindow then
hotkeyAssignWindow:destroy()
hotkeyAssignWindow = nil
end
local gameRootPanel = modules.game_interface.getRootPanel()
for index, panel in ipairs({bottomActionPanel1, bottomActionPanel2, bottomActionPanel3,
leftActionPanel1, leftActionPanel2, leftActionPanel3,
rightActionPanel1, rightActionPanel2, rightActionPanel3}) do
settings[tostring(index)] = {}
for i, child in ipairs(panel.tabBar:getChildren()) do
if child.config and child.config.item then
settings[tostring(index)][tostring(i)] = child.config
if type(child.config.hotkey) == 'string' and child.config.hotkey:len() > 0 then
g_keyboard.unbindKeyPress(child.config.hotkey, child.callback, gameRootPanel)
end
end
if child.cooldownEvent then
removeEvent(child.cooldownEvent)
end
end
panel.tabBar:destroyChildren()
end
if not reloaded then
save()
end
end
function setupActionPanel(index, panel, bottom)
local config = settings[tostring(index)] or {}
for i=1,actionButtonsInPanel do
local type = bottom and 'ActionButton' or 'SideActionButton'
local action = g_ui.createWidget(type, panel.tabBar)
action.config = config[tostring(i)] or {}
setupAction(action)
end
panel.nextButton.onClick = function()
panel.tabBar:moveChildToIndex(panel.tabBar:getFirstChild(), panel.tabBar:getChildCount())
end
panel.prevButton.onClick = function()
panel.tabBar:moveChildToIndex(panel.tabBar:getLastChild(), 1)
end
end
function setupAction(action)
local config = action.config
action.item:setShowCount(false)
action.onMouseRelease = actionOnMouseRelease
action.onTouchRelease = actionOnMouseRelease
action.callback = function(k, c, ticks)
local lockKeyboard = g_settings.getBoolean('actionbarLock', false)
local chatMode = not modules.game_walking.wsadWalking
if not lockKeyboard or not chatMode then
executeAction(action, ticks)
end
end
action.item.onItemChange = nil -- disable callbacks for setup
if config then
if type(config.text) == 'number' then
config.text = tostring(config.text)
end
if type(config.hotkey) == 'number' then
config.hotkey = tostring(config.hotkey)
end
if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
local gameRootPanel = modules.game_interface.getRootPanel()
g_keyboard.bindKeyPress(config.hotkey, action.callback, gameRootPanel)
local text = config.hotkey
-- formatting similar to cip Tibia 12
local values = {
{"Shift", "S"},
{"Ctrl", "C"},
{"+", ""},
{"PageUp", "PgUp"},
{"PageDown", "PgDown"},
{"Enter", "Return"},
{"Insert", "Ins"},
{"Delete", "Del"},
{"Escape", "Esc"}
}
for i, v in pairs(values) do
text = text:gsub(v[1], v[2])
end
if text:len() > 6 then
text = text:sub(text:len()-3,text:len())
text = "..."..text
end
action.hotkeyLabel:setText(text)
else
action.hotkeyLabel:setText("")
end
action.text:setImageSource("")
action.cooldownTill = 0
action.cooldownStart = 0
if type(config.text) == 'string' and config.text:len() > 0 then
action.text:setText(config.text)
action.item:setBorderColor(ActionColors.text)
action.item:setOn(true) -- removes background
action.item:setItemId(0)
if Spells then
local spell, profile = Spells.getSpellByWords(config.text:lower())
action.spell = spell
if action.spell and action.spell.icon and profile then
action.text:setImageSource(SpelllistSettings[profile].iconFile)
action.text:setImageClip(Spells.getImageClip(SpellIcons[action.spell.icon][1], profile))
action.text:setText("")
end
end
else
action.text:setText("")
action.spell = nil
if type(config.item) == 'number' and config.item > 100 then
action.item:setOn(true)
action.item:setItemId(config.item)
action.item:setItemCount(config.count or 1)
setupActionType(action, config.actionType)
else
action.item:setItemId(0)
action.item:setOn(false)
action.item:setBorderColor(ActionColors.empty)
end
end
end
action.item.onItemChange = actionOnItemChange
end
function setupActionType(action, actionType)
local item = action.item:getItem()
if action.item:getItem():isMultiUse() then
if not actionType or actionType <= ActionTypes.USE then
actionType = ActionTypes.USE_WITH
end
elseif g_game.getClientVersion() >= 910 then
if actionType ~= ActionTypes.USE and actionType ~= ActionTypes.EQUIP then
actionType = ActionTypes.USE
end
else
actionType = ActionTypes.USE
end
action.config.actionType = actionType
if action.config.actionType == ActionTypes.USE then
action.item:setBorderColor(ActionColors.itemUse)
elseif action.config.actionType == ActionTypes.USE_SELF then
action.item:setBorderColor(ActionColors.itemUseSelf)
elseif action.config.actionType == ActionTypes.USE_TARGET then
action.item:setBorderColor(ActionColors.itemUseTarget)
elseif action.config.actionType == ActionTypes.USE_WITH then
action.item:setBorderColor(ActionColors.itemUseWith)
elseif action.config.actionType == ActionTypes.EQUIP then
action.item:setBorderColor(ActionColors.itemEquip)
end
end
function updateAction(action, newConfig)
local config = action.config
if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
local gameRootPanel = modules.game_interface.getRootPanel()
g_keyboard.unbindKeyPress(config.hotkey, action.callback, gameRootPanel)
end
for key, val in pairs(newConfig) do
action.config[key] = val
end
setupAction(action)
end
function actionOnMouseRelease(action, mousePosition, mouseButton)
if mouseButton == MouseTouch then return end
if mouseButton == MouseRightButton or not action.item:isOn() then
local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
if action.item:getItemId() > 0 then
if action.item:getItem():isMultiUse() then
menu:addOption(tr('Use on yourself'), function() return setupActionType(action, ActionTypes.USE_SELF) end)
menu:addOption(tr('Use on target'), function() return setupActionType(action, ActionTypes.USE_TARGET) end)
menu:addOption(tr('With crosshair'), function() return setupActionType(action, ActionTypes.USE_WITH) end)
end
if g_game.getClientVersion() >= 910 then
if not action.item:getItem():isMultiUse() then
menu:addOption(tr('Use'), function() return setupActionType(action, ActionTypes.USE) end)
end
menu:addOption(tr('Equip'), function() return setupActionType(action, ActionTypes.EQUIP) end)
end
else
menu:addOption(tr('Select item'), function() return modules.game_itemselector.show(action.item) end)
end
menu:addSeparator()
menu:addOption(tr('Set text'), function()
modules.client_textedit.singlelineEditor(action.config.text or "", function(newText)
updateAction(action, {text=newText, item=0})
end)
end)
menu:addOption(tr('Set hotkey'), function()
if hotkeyAssignWindow then
hotkeyAssignWindow:destroy()
end
local assignWindow = g_ui.createWidget('ActionAssignWindow', rootWidget)
assignWindow:grabKeyboard()
assignWindow.comboPreview.keyCombo = ''
assignWindow.onKeyDown = function(assignWindow, keyCode, keyboardModifiers)
local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
assignWindow.comboPreview:setText(tr('Current action hotkey: %s', keyCombo))
assignWindow.comboPreview.keyCombo = keyCombo
assignWindow.comboPreview:resizeToText()
return true
end
assignWindow.onDestroy = function(widget)
if widget == hotkeyAssignWindow then
hotkeyAssignWindow = nil
end
end
assignWindow.addButton.onClick = function()
local text = tostring(assignWindow.comboPreview.keyCombo)
updateAction(action, {hotkey=text})
assignWindow:destroy()
end
hotkeyAssignWindow = assignWindow
end)
menu:addSeparator()
menu:addOption(tr('Clear'), function()
updateAction(action, {hotkey="", text="", item=0, count=1})
end)
menu:display(mousePosition)
return true
elseif mouseButton == MouseLeftButton or mouseButton == MouseTouch2 or mouseButton == MouseTouch3 then
action.callback()
return true
end
return false
end
function actionOnItemChange(widget)
updateAction(widget:getParent(), {text="", item=widget:getItemId(), count=widget:getItemCountOrSubType()})
end
function onSpellCooldown(iconId, duration)
for index, panel in ipairs({bottomActionPanel1, bottomActionPanel2, bottomActionPanel3,
leftActionPanel1, leftActionPanel2, leftActionPanel3,
rightActionPanel1, rightActionPanel2, rightActionPanel3}) do
for i, child in ipairs(panel.tabBar:getChildren()) do
if child.spell and child.spell.id == iconId then
startCooldown(child, duration)
end
end
end
end
function onSpellGroupCooldown(groupId, duration)
for index, panel in ipairs({bottomActionPanel1, bottomActionPanel2, bottomActionPanel3,
leftActionPanel1, leftActionPanel2, leftActionPanel3,
rightActionPanel1, rightActionPanel2, rightActionPanel3}) do
for i, child in ipairs(panel.tabBar:getChildren()) do
if child.spell and child.spell.group then
for group, dur in pairs(child.spell.group) do
if groupId == group then
startCooldown(child, duration)
end
end
end
end
end
end
function startCooldown(action, duration)
if type(action.cooldownTill) == 'number' and action.cooldownTill > g_clock.millis() + duration then
return -- already has cooldown with greater duration
end
action.cooldownStart = g_clock.millis()
action.cooldownTill = g_clock.millis() + duration
updateCooldown(action)
end
function updateCooldown(action)
if not action or not action.cooldownTill then return end
local timeleft = action.cooldownTill - g_clock.millis()
if timeleft <= 30 then
action.cooldown:setPercent(100)
action.cooldownEvent = nil
action.cooldown:setText("")
return
end
local duration = action.cooldownTill - action.cooldownStart
local formattedText
if timeleft > 60000 then
formattedText = math.floor(timeleft / 60000) .. "m"
else
formattedText = timeleft/1000
formattedText = math.floor(formattedText * 10) / 10
formattedText = math.floor(formattedText) .. "." .. math.floor(formattedText * 10) % 10
end
action.cooldown:setText(formattedText)
action.cooldown:setPercent(100 - math.floor(100 * timeleft / duration))
action.cooldownEvent = scheduleEvent(function() updateCooldown(action) end, 30)
end
function executeAction(action, ticks)
if not action.config then return end
if type(ticks) ~= 'number' then ticks = 0 end
local actionDelay = 100
if ticks == 0 then
actionDelay = 200 -- for first use
elseif action.actionDelayTo ~= nil and g_clock.millis() < action.actionDelayTo then
return
end
local actionType = action.config.actionType
if type(action.config.text) == 'string' and action.config.text:len() > 0 then
if g_app.isMobile() then -- turn to direction of targer
local target = g_game.getAttackingCreature()
if target then
local pos = g_game.getLocalPlayer():getPosition()
local tpos = target:getPosition()
if pos and tpos then
local offx = tpos.x - pos.x
local offy = tpos.y - pos.y
if offy < 0 and offx <= 0 and math.abs(offx) < math.abs(offy) then
g_game.turn(Directions.North)
elseif offy > 0 and offx >= 0 and math.abs(offx) < math.abs(offy) then
g_game.turn(Directions.South)
elseif offx < 0 and offy <= 0 and math.abs(offx) > math.abs(offy) then
g_game.turn(Directions.West)
elseif offx > 0 and offy >= 0 and math.abs(offx) > math.abs(offy) then
g_game.turn(Directions.East)
end
end
end
end
if modules.game_interface.isChatVisible() then
modules.game_console.sendMessage(action.config.text)
else
g_game.talk(action.config.text)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.item:getItemId() > 0 then
if actionType == ActionTypes.USE then
if g_game.getClientVersion() < 780 then
local item = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
if item then
g_game.use(item)
end
else
g_game.useInventoryItem(action.item:getItemId())
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif actionType == ActionTypes.USE_SELF then
if g_game.getClientVersion() < 780 then
local item = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
if item then
g_game.useWith(item, g_game.getLocalPlayer())
end
else
g_game.useInventoryItemWith(action.item:getItemId(), g_game.getLocalPlayer(), action.item:getItemSubType() or -1)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif actionType == ActionTypes.USE_TARGET then
local attackingCreature = g_game.getAttackingCreature()
if not attackingCreature then
local item = Item.create(action.item:getItemId())
if g_game.getClientVersion() < 780 then
local tmpItem = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
if not tmpItem then return end
item = tmpItem
end
modules.game_interface.startUseWith(item, action.item:getItemSubType() or - 1)
return
end
if not attackingCreature:getTile() then return end
if g_game.getClientVersion() < 780 then
local item = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
if item then
g_game.useWith(item, attackingCreature, action.item:getItemSubType() or -1)
end
else
g_game.useInventoryItemWith(action.item:getItemId(), attackingCreature, action.item:getItemSubType() or -1)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif actionType == ActionTypes.USE_WITH then
local item = Item.create(action.item:getItemId())
if g_game.getClientVersion() < 780 then
local tmpItem = g_game.findPlayerItem(action.item:getItemId(), action.item:getItemSubType() or -1)
if not tmpItem then return true end
item = tmpItem
end
modules.game_interface.startUseWith(item, action.item:getItemSubType() or - 1)
elseif actionType == ActionTypes.EQUIP then
if g_game.getClientVersion() >= 910 then
local item = Item.create(action.item:getItemId())
g_game.equipItem(item)
action.actionDelayTo = g_clock.millis() + actionDelay
end
end
end
end
function save()
local settingsFile = modules.client_profiles.getSettingsFilePath("actionbar.json")
local status, result = pcall(function() return json.encode(settings, 2) end)
if not status then
return onError(
"Error while saving top bar settings. Data won't be saved. Details: " ..
result)
end
if result:len() > 100 * 1024 * 1024 then
return onError(
"Something went wrong, file is above 100MB, won't be saved")
end
g_resources.writeFileContents(settingsFile, result)
end
function load()
local settingsFile = modules.client_profiles.getSettingsFilePath("actionbar.json")
if g_resources.fileExists(settingsFile) then
local status, result = pcall(function()
return json.decode(g_resources.readFileContents(settingsFile))
end)
if not status then
return onError(
"Error while reading top bar settings file. To fix this problem you can delete storage.json. Details: " ..
result)
end
settings = result
else
settings = {}
end
end

View File

@@ -0,0 +1,9 @@
Module
name: game_actionbar
description: Action bar
author: otclient@otclient.ovh
website: otclient.ovh
sandboxed: true
scripts: [ actionbar ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,141 @@
ActionButton < Panel
font: cipsoftFont
width: 40
padding: 1 1 1 1
margin-left: 1
draggable: true
Item
id: item
anchors.fill: parent
&selectable: true
&editable: false
virtual: true
border-width: 1
border-color: #00000000
$!on:
image-source: /images/game/actionbarslot
Label
id: text
anchors.fill: parent
text-auto-resize: true
text-wrap: true
phantom: true
text-align: center
font: verdana-9px
Label
id: hotkeyLabel
anchors.top: parent.top
anchors.right: parent.right
margin: 2 3 3 3
text-auto-resize: true
text-wrap: false
phantom: true
font: cipsoftFont
color: white
background: #292A2A
UIProgressRect
id: cooldown
background: #585858AA
percent: 100
focusable: false
phantom: true
anchors.fill: parent
margin: 1 1 1 1
font: verdana-11px-rounded
color: white
Panel
id: actionBar
focusable: false
image-source: /images/ui/panel_side
image-border: 4
margin-top: -1
$on:
height: 40
visible: true
$!on:
height: 0
visible: false
TabButton
id: prevButton
icon: /images/game/console/leftarrow
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-left: 1
margin-top: 1
margin-bottom: 2
Panel
id: tabBar
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: prev.right
anchors.right: next.left
margin-right: 3
clipping: true
layout: horizontalBox
TabButton
id: nextButton
icon: /images/game/console/rightarrow
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-right: 1
margin-top: 1
margin-bottom: 2
ActionAssignWindow < MainWindow
id: assignWindow
!text: tr('Button Assign')
size: 360 150
@onEscape: self:destroy()
Label
!text: tr('Please, press the key you wish to use for action')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-auto-resize: true
text-align: left
Label
id: comboPreview
!text: tr('Current action hotkey: %s', 'none')
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prev.bottom
margin-top: 10
text-auto-resize: true
HorizontalSeparator
id: separator
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 10
Button
id: addButton
!text: tr('Add')
width: 64
anchors.right: next.left
anchors.bottom: parent.bottom
margin-right: 10
Button
id: cancelButton
!text: tr('Cancel')
width: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
@onClick: self:getParent():destroy()

View File

@@ -0,0 +1,145 @@
SideActionButton < Panel
font: cipsoftFont
height: 40
padding: 1 1 1 1
$first:
margin-top: -4
$!first:
margin-top: -1
Item
id: item
anchors.fill: parent
&selectable: true
&editable: false
virtual: true
border-width: 1
border-color: #00000000
$!on:
image-source: /images/game/actionbarslot
Label
id: text
anchors.fill: parent
text-auto-resize: true
text-wrap: true
phantom: true
text-align: center
font: verdana-9px
Label
id: hotkeyLabel
anchors.top: parent.top
anchors.right: parent.right
margin: 2 3 3 3
text-auto-resize: true
text-wrap: false
phantom: true
font: cipsoftFont
color: white
background: #292A2A
UIProgressRect
id: cooldown
background: #585858AA
percent: 100
focusable: false
phantom: true
anchors.fill: parent
margin: 1 1 1 1
font: verdana-11px-rounded
color: white
Panel
id: actionBar
focusable: false
image-source: /images/ui/panel_side
image-border: 4
margin-top: -2
$on:
width: 40
visible: true
$!on:
width: 0
visible: false
TabButton
id: prevButton
icon: /images/game/console/uparrow
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
margin-top: -1
margin-left: 1
margin-right: 1
Panel
id: tabBar
anchors.top: prev.bottom
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: next.top
margin-top: 3
clipping: true
layout: verticalBox
TabButton
id: nextButton
icon: /images/game/console/downarrow
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
margin-right: 1
margin-left: 1
margin-bottom: 2
ActionAssignWindow < MainWindow
id: assignWindow
!text: tr('Button Assign')
size: 360 150
@onEscape: self:destroy()
Label
!text: tr('Please, press the key you wish to use for action')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-auto-resize: true
text-align: left
Label
id: comboPreview
!text: tr('Current action hotkey: %s', 'none')
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: prev.bottom
margin-top: 10
text-auto-resize: true
HorizontalSeparator
id: separator
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 10
Button
id: addButton
!text: tr('Add')
width: 64
anchors.right: next.left
anchors.bottom: parent.bottom
margin-right: 10
Button
id: cancelButton
!text: tr('Cancel')
width: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
@onClick: self:getParent():destroy()

Some files were not shown because too many files have changed in this diff Show More