Version 0.95 BETA

This commit is contained in:
OTCv8
2019-10-02 03:38:52 +02:00
parent 9219c78f15
commit 5220a3bdd2
501 changed files with 38097 additions and 2 deletions

113
modules/client/client.lua Normal file
View File

@@ -0,0 +1,113 @@
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 })
g_window.setMinimumSize({ width = 800, height = 600 })
if g_sounds ~= nil then
--g_sounds.preload(musicFilename)
end
-- initialize in fullscreen mode on mobile devices
if g_window.getPlatformType() == "X11-EGL" then
g_window.setFullscreen(true)
else
-- window size
local size = { width = 800, 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')
-- poll resize events
g_window.poll()
g_keyboard.bindKeyDown('Ctrl+Shift+R', reloadScripts)
g_keyboard.bindKeyDown('Ctrl+Shift+[', function() g_extras.setTestMode((g_extras.getTestMode() - 1) % 10) end)
g_keyboard.bindKeyDown('Ctrl+Shift+]', function() g_extras.setTestMode((g_extras.getTestMode() + 1) % 10) end)
-- 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 })
-- 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

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_options
- client_entergame
- client_terminal
- client_stats
- client_news
- client_feedback
- client_updater

View File

@@ -0,0 +1,50 @@
-- private variables
local background
local clientVersionLabel
-- public functions
function init()
background = g_ui.displayUI('background')
background:lower()
clientVersionLabel = background:getChildById('clientVersionLabel')
clientVersionLabel:setText(g_app.getName() .. ' ' .. g_app.getVersion() .. '\nMade by:\n' .. g_app.getAuthor() .. "\notclient@otclient.ovh")
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,10 @@
Module
name: client_background
description: Handles the background of the login screen
author: edubart
website: https://github.com/edubart/otclient
sandboxed: true
scripts: [ background ]
dependencies: [ client_topmenu ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,24 @@
Panel
id: background
image-source: /images/background
image-smooth: true
image-fixed-ratio: true
anchors.top: topMenu.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
margin-top: 1
focusable: false
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,373 @@
CharacterList = { }
-- private variables
local charactersWindow
local loadBox
local characterList
local errorBox
local waitingWindow
local updateWaitEvent
local resendWaitEvent
local loginEvent
-- private functions
local function tryLogin(charInfo, tries)
tries = tries or 1
if tries > 50 then
return
end
if g_game.isOnline() then
if tries == 1 then
g_game.safeLogout()
end
loginEvent = scheduleEvent(function() tryLogin(charInfo, tries+1) end, 100)
return
end
CharacterList.hide()
-- proxies for not http login users
if charInfo.worldHost == "0.0.0.0" and g_proxy then
g_proxy.clear()
-- g_proxy.addProxy(localPort, proxyHost, proxyPort, proxyPriority)
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.158.184.57", 7162, 0)
g_proxy.addProxy(tonumber(charInfo.worldPort), "54.39.190.20", 7162, 0)
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.83.226.109", 7162, 0)
g_proxy.addProxy(tonumber(charInfo.worldPort), "35.247.201.100", 443, 0)
end
g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey)
g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort)
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...'))
connect(loadBox, { onCancel = function()
loadBox = nil
g_game.cancelLogin()
CharacterList.show()
end })
-- save last used character
g_settings.set('last-used-character', charInfo.characterName)
g_settings.set('last-used-world', charInfo.worldName)
end
local function updateWait(timeStart, timeEnd)
if waitingWindow then
local time = g_clock.seconds()
if time <= timeEnd then
local percent = ((time - timeStart) / (timeEnd - timeStart)) * 100
local timeStr = string.format("%.0f", timeEnd - time)
local progressBar = waitingWindow:getChildById('progressBar')
progressBar:setPercent(percent)
local label = waitingWindow:getChildById('timeLabel')
label:setText(tr('Trying to reconnect in %s seconds.', timeStr))
updateWaitEvent = scheduleEvent(function() updateWait(timeStart, timeEnd) end, 1000 * progressBar:getPercentPixels() / 100 * (timeEnd - timeStart))
return true
end
end
if updateWaitEvent then
updateWaitEvent:cancel()
updateWaitEvent = nil
end
end
local function resendWait()
if waitingWindow then
waitingWindow:destroy()
waitingWindow = nil
if updateWaitEvent then
updateWaitEvent:cancel()
updateWaitEvent = nil
end
if charactersWindow then
local selected = characterList:getFocusedChild()
if selected then
local charInfo = { worldHost = selected.worldHost,
worldPort = selected.worldPort,
worldName = selected.worldName,
characterName = selected.characterName }
tryLogin(charInfo)
end
end
end
end
local function onLoginWait(message, time)
CharacterList.destroyLoadBox()
waitingWindow = g_ui.displayUI('waitinglist')
local label = waitingWindow:getChildById('infoLabel')
label:setText(message)
updateWaitEvent = scheduleEvent(function() updateWait(g_clock.seconds(), g_clock.seconds() + time) end, 0)
resendWaitEvent = scheduleEvent(resendWait, time * 1000)
end
function onGameLoginError(message)
CharacterList.destroyLoadBox()
errorBox = displayErrorBox(tr("Login Error"), message)
errorBox.onOk = function()
errorBox = nil
CharacterList.showAgain()
end
end
function onGameLoginToken(unknown)
CharacterList.destroyLoadBox()
-- TODO: make it possible to enter a new token here / prompt token
errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.')
errorBox.onOk = function()
errorBox = nil
EnterGame.show()
end
end
function onGameConnectionError(message, code)
CharacterList.destroyLoadBox()
local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message)
errorBox = displayErrorBox(tr("Connection Error"), text)
errorBox.onOk = function()
errorBox = nil
CharacterList.showAgain()
end
end
function onGameUpdateNeeded(signature)
CharacterList.destroyLoadBox()
errorBox = displayErrorBox(tr("Update needed"), tr('Enter with your account again to update your client.'))
errorBox.onOk = function()
errorBox = nil
CharacterList.showAgain()
end
end
-- public functions
function CharacterList.init()
connect(g_game, { onLoginError = onGameLoginError })
connect(g_game, { onLoginToken = onGameLoginToken })
connect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
connect(g_game, { onConnectionError = onGameConnectionError })
connect(g_game, { onGameStart = CharacterList.destroyLoadBox })
connect(g_game, { onLoginWait = onLoginWait })
connect(g_game, { onGameEnd = CharacterList.showAgain })
if G.characters then
CharacterList.create(G.characters, G.characterAccount)
end
end
function CharacterList.terminate()
disconnect(g_game, { onLoginError = onGameLoginError })
disconnect(g_game, { onLoginToken = onGameLoginToken })
disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
disconnect(g_game, { onConnectionError = onGameConnectionError })
disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox })
disconnect(g_game, { onLoginWait = onLoginWait })
disconnect(g_game, { onGameEnd = CharacterList.showAgain })
if charactersWindow then
characterList = nil
charactersWindow:destroy()
charactersWindow = nil
end
if loadBox then
g_game.cancelLogin()
loadBox:destroy()
loadBox = nil
end
if waitingWindow then
waitingWindow:destroy()
waitingWindow = nil
end
if updateWaitEvent then
removeEvent(updateWaitEvent)
updateWaitEvent = nil
end
if resendWaitEvent then
removeEvent(resendWaitEvent)
resendWaitEvent = nil
end
if loginEvent then
removeEvent(loginEvent)
loginEvent = nil
end
CharacterList = nil
end
function CharacterList.create(characters, account, otui)
if not otui then otui = 'characterlist' end
if charactersWindow then
charactersWindow:destroy()
end
charactersWindow = g_ui.displayUI(otui)
characterList = charactersWindow:getChildById('characters')
-- characters
G.characters = characters
G.characterAccount = account
characterList:destroyChildren()
local accountStatusLabel = charactersWindow:getChildById('accountStatusLabel')
local focusLabel
for i,characterInfo in ipairs(characters) do
local widget = g_ui.createWidget('CharacterWidget', characterList)
for key,value in pairs(characterInfo) do
local subWidget = widget:getChildById(key)
if subWidget then
if key == 'outfit' then -- it's an exception
subWidget:setOutfit(value)
else
local text = value
if subWidget.baseText and subWidget.baseTranslate then
text = tr(subWidget.baseText, text)
elseif subWidget.baseText then
text = string.format(subWidget.baseText, text)
end
subWidget:setText(text)
end
end
end
-- these are used by login
widget.characterName = characterInfo.name
widget.worldName = characterInfo.worldName
widget.worldHost = characterInfo.worldIp
widget.worldPort = characterInfo.worldPort
connect(widget, { onDoubleClick = function () CharacterList.doLogin() return true end } )
if i == 1 or (g_settings.get('last-used-character') == widget.characterName and g_settings.get('last-used-world') == widget.worldName) then
focusLabel = widget
end
end
if focusLabel then
characterList:focusChild(focusLabel, KeyboardFocusReason)
addEvent(function() characterList:ensureChildVisible(focusLabel) end)
end
-- account
local status = ''
if account.status == AccountStatus.Frozen then
status = tr(' (Frozen)')
elseif account.status == AccountStatus.Suspended then
status = tr(' (Suspended)')
end
if account.subStatus == SubscriptionStatus.Free then
accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status))
elseif account.subStatus == SubscriptionStatus.Premium then
if account.premDays == 0 or account.premDays == 65535 then
accountStatusLabel:setText(('%s%s'):format(tr('Gratis Premium Account'), status))
else
accountStatusLabel:setText(('%s%s'):format(tr('Premium Account (%s) days left', account.premDays), status))
end
end
if account.premDays > 0 and account.premDays <= 7 then
accountStatusLabel:setOn(true)
else
accountStatusLabel:setOn(false)
end
end
function CharacterList.destroy()
CharacterList.hide(true)
if charactersWindow then
characterList = nil
charactersWindow:destroy()
charactersWindow = nil
end
end
function CharacterList.show()
if loadBox or errorBox or not charactersWindow then return end
charactersWindow:show()
charactersWindow:raise()
charactersWindow:focus()
end
function CharacterList.hide(showLogin)
showLogin = showLogin or false
charactersWindow:hide()
if showLogin and EnterGame and not g_game.isOnline() then
EnterGame.show()
end
end
function CharacterList.showAgain()
if characterList and characterList:hasChildren() then
CharacterList.show()
end
end
function CharacterList.isVisible()
if charactersWindow and charactersWindow:isVisible() then
return true
end
return false
end
function CharacterList.doLogin()
local selected = characterList:getFocusedChild()
if selected then
local charInfo = { worldHost = selected.worldHost,
worldPort = selected.worldPort,
worldName = selected.worldName,
characterName = selected.characterName }
charactersWindow:hide()
if loginEvent then
removeEvent(loginEvent)
loginEvent = nil
end
tryLogin(charInfo)
else
displayErrorBox(tr('Error'), tr('You must select a character to login!'))
end
end
function CharacterList.destroyLoadBox()
if loadBox then
loadBox:destroy()
loadBox = nil
end
end
function CharacterList.cancelWait()
if waitingWindow then
waitingWindow:destroy()
waitingWindow = nil
end
if updateWaitEvent then
removeEvent(updateWaitEvent)
updateWaitEvent = nil
end
if resendWaitEvent then
removeEvent(resendWaitEvent)
resendWaitEvent = nil
end
CharacterList.destroyLoadBox()
CharacterList.showAgain()
end

View File

@@ -0,0 +1,134 @@
CharacterWidget < UIWidget
height: 14
background-color: alpha
&updateOnStates: |
function(self)
local children = self:getChildren()
for i=1,#children do
children[i]:setOn(self:isFocused())
end
end
@onFocusChange: self:updateOnStates()
@onSetup: self:updateOnStates()
$focus:
background-color: #ffffff22
Label
id: name
color: #bbbbbb
anchors.top: parent.top
anchors.left: parent.left
font: verdana-11px-monochrome
text-auto-resize: true
background-color: alpha
text-offset: 2 0
$on:
color: #ffffff
Label
id: worldName
color: #bbbbbb
anchors.top: parent.top
anchors.right: parent.right
margin-right: 5
font: verdana-11px-monochrome
text-auto-resize: true
background-color: alpha
&baseText: '(%s)'
$on:
color: #ffffff
StaticMainWindow
id: charactersWindow
!text: tr('Character List')
visible: false
@onEnter: CharacterList.doLogin()
@onEscape: CharacterList.hide(true)
@onSetup: |
g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self)
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
if g_game.getFeature(GamePreviewState) then
self:setSize({width = 350, height = 400})
else
self:setSize({width = 250, height = 248})
end
TextList
id: characters
background-color: #565656
anchors.top: parent.top
anchors.left: parent.left
anchors.right: characterListScrollBar.left
anchors.bottom: accountStatusCaption.top
margin-bottom: 5
padding: 1
focusable: false
vertical-scrollbar: characterListScrollBar
auto-focus: first
VerticalScrollBar
id: characterListScrollBar
anchors.top: parent.top
anchors.bottom: accountStatusCaption.top
anchors.right: parent.right
margin-bottom: 5
step: 14
pixels-scroll: true
Label
id: accountStatusCaption
!text: tr('Account Status') .. ':'
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 1
Label
id: accountStatusLabel
!text: tr('Free Account')
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: separator.top
margin-bottom: 5
text-auto-resize: true
$on:
color: #FF0000
HorizontalSeparator
id: separator
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 10
//CheckBox
// id: charAutoLoginBox
// !text: tr('Auto login')
// !tooltip: tr('Auto login selected character on next charlist load')
// anchors.left: parent.left
// anchors.right: parent.right
// anchors.bottom: next.top
// margin-bottom: 6
// margin-left: 18
// margin-right: 18
Button
id: buttonOk
!text: tr('Ok')
width: 64
anchors.right: next.left
anchors.bottom: parent.bottom
margin-right: 10
@onClick: CharacterList.doLogin()
Button
id: buttonCancel
!text: tr('Cancel')
width: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
@onClick: CharacterList.hide(true)

View File

@@ -0,0 +1,495 @@
EnterGame = { }
-- private variables
local loadBox
local enterGame
local enterGameButton
local clientBox
local protocolLogin
local server = nil
local versionsFound = false
local newLogin = nil
local newLoginUrl = nil
local newLoginEvent
local customServerSelectorPanel
local serverSelectorPanel
local serverSelector
local clientVersionSelector
local serverHostTextEdit
local rememberPasswordBox
local protos = {"740", "760", "772", "800", "810", "854", "860", "1090", "1096", "1099"}
-- private functions
local function onProtocolError(protocol, message, errorCode)
if errorCode then
return EnterGame.onError(message)
end
return EnterGame.onLoginError(message)
end
local function onSessionKey(protocol, sessionKey)
G.sessionKey = sessionKey
end
local function onCharacterList(protocol, characters, account, otui)
if rememberPasswordBox:isChecked() then
local account = g_crypt.encrypt(G.account)
local password = g_crypt.encrypt(G.password)
g_settings.set('account', account)
g_settings.set('password', password)
else
EnterGame.clearAccountFields()
end
for _, characterInfo in pairs(characters) do
if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then
characterInfo.worldName = characterInfo.worldName .. ', Preview'
end
end
if loadBox then
loadBox:destroy()
loadBox = nil
end
CharacterList.create(characters, account, otui)
CharacterList.show()
g_settings.save()
end
local function onUpdateNeeded(protocol, signature)
return EnterGame.onError(tr('Your client needs updating, try redownloading it.'))
end
local function parseFeatures(features)
for feature_id, value in pairs(features) do
if value == "1" or value == "true" or value == true then
g_game.enableFeature(feature_id)
else
g_game.disableFeature(feature_id)
end
end
end
local function validateThings(things)
local incorrectThings = ""
if things ~= nil then
local thingsNode = {}
for thingtype, thingdata in pairs(things) do
thingsNode[thingtype] = thingdata[1]
if not g_resources.fileExists("/data/things/" .. thingdata[1]) then
correctThings = false
incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n"
end
local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower()
if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then
if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version
incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n"
end
end
end
g_settings.setNode("things", thingsNode)
else
g_settings.setNode("things", {})
end
return incorrectThings
end
local function onHTTPResult(data, err)
if err then
return EnterGame.onError(err)
end
if data['error'] and #data['error'] > 0 then
return EnterGame.onLoginError(data['error'])
end
local characters = data["characters"]
local account = data["account"]
local session = data["session"]
local version = data["version"]
local things = data["things"]
local customProtocol = data["customProtocol"]
local features = data["features"]
local settings = data["settings"]
local rsa = data["rsa"]
local proxies = data["proxies"]
local incorrectThings = validateThings(things)
if #incorrectThings > 0 then
g_logger.info(incorrectThings)
if Updater then
return Updater.updateThings(things, incorrectThings)
else
return EnterGame.onError(incorrectThings)
end
end
-- custom protocol
g_game.setCustomProtocolVersion(0)
if customProtocol ~= nil then
customProtocol = tonumber(customProtocol)
if customProtocol ~= nil and customProtocol > 0 then
g_game.setCustomProtocolVersion(customProtocol)
end
end
-- force player settings
if settings ~= nil then
for option, value in pairs(settings) do
modules.client_options.setOption(option, value, true)
end
end
-- version
G.clientVersion = version
g_game.setClientVersion(version)
g_game.setProtocolVersion(g_game.getClientProtocolVersion(version))
g_game.setCustomOs(-1) -- disable
if rsa ~= nil then
g_game.setRsa(rsa)
end
if features ~= nil then
parseFeatures(features)
end
if session ~= nil and session:len() > 0 then
onSessionKey(nil, session)
end
-- proxies
if g_proxy then
g_proxy.clear()
if proxies then
for i, proxy in ipairs(proxies) do
g_proxy.addProxy(tonumber(proxy["localPort"]), proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"]))
end
end
end
onCharacterList(nil, characters, account, nil)
end
-- public functions
function EnterGame.init()
enterGame = g_ui.displayUI('entergame')
newLogin = g_ui.displayUI('entergame_new')
serverSelectorPanel = enterGame:getChildById('serverSelectorPanel')
customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel')
serverSelector = serverSelectorPanel:getChildById('serverSelector')
rememberPasswordBox = enterGame:getChildById('rememberPasswordBox')
serverHostTextEdit = customServerSelectorPanel:getChildById('serverHostTextEdit')
clientVersionSelector = customServerSelectorPanel:getChildById('clientVersionSelector')
if Servers ~= nil then
for name,server in pairs(Servers) do
serverSelector:addOption(name)
end
end
if serverSelector:getOptionsCount() == 0 or ALLOW_CUSTOM_SERVERS then
serverSelector:addOption(tr("Another"))
end
for i,proto in pairs(protos) do
clientVersionSelector:addOption(proto)
end
if serverSelector:getOptionsCount() == 1 then
enterGame:setHeight(enterGame:getHeight() - serverSelectorPanel:getHeight())
serverSelectorPanel:setOn(false)
end
local account = g_crypt.decrypt(g_settings.get('account'))
local password = g_crypt.decrypt(g_settings.get('password'))
local server = g_settings.get('server')
local host = g_settings.get('host')
local clientVersion = g_settings.get('client-version')
local hdSprites = g_settings.getBoolean('hdSprites', false)
if serverSelector:isOption(server) then
serverSelector:setCurrentOption(server, false)
if Servers == nil or Servers[server] == nil then
serverHostTextEdit:setText(host)
end
clientVersionSelector:setOption(clientVersion)
else
server = ""
host = ""
end
enterGame:getChildById('accountPasswordTextEdit'):setText(password)
enterGame:getChildById('accountNameTextEdit'):setText(account)
rememberPasswordBox:setChecked(#account > 0)
if enterGame.hdSprites then
enterGame.hdSprites:setChecked(hdSprites)
end
g_keyboard.bindKeyDown('Ctrl+G', EnterGame.openWindow)
if g_game.isOnline() then
return EnterGame.hide()
end
scheduleEvent(function()
EnterGame.show()
end, 100)
end
function EnterGame.terminate()
g_keyboard.unbindKeyDown('Ctrl+G')
removeEvent(newLoginEvent)
enterGame:destroy()
if newLogin then
newLogin:destroy()
end
if loadBox then
loadBox:destroy()
loadBox = nil
end
if protocolLogin then
protocolLogin:cancelLogin()
protocolLogin = nil
end
EnterGame = nil
end
function EnterGame.show()
if Updater and Updater.isVisible() or g_game.isOnline() then
return EnterGame.hide()
end
enterGame:show()
enterGame:raise()
enterGame:focus()
enterGame:getChildById('accountNameTextEdit'):focus()
EnterGame.checkNewLogin()
end
function EnterGame.hide()
enterGame:hide()
newLogin:hide()
end
function EnterGame.openWindow()
if g_game.isOnline() then
CharacterList.show()
elseif not g_game.isLogging() and not CharacterList.isVisible() then
EnterGame.show()
end
end
function EnterGame.clearAccountFields()
enterGame:getChildById('accountNameTextEdit'):clearText()
enterGame:getChildById('accountPasswordTextEdit'):clearText()
--enterGame:getChildById('authenticatorTokenTextEdit'):clearText()
enterGame:getChildById('accountNameTextEdit'):focus()
g_settings.remove('account')
g_settings.remove('password')
end
function EnterGame.hideNewLogin()
newLogin:hide()
newLoginUrl = nil
end
function EnterGame.checkNewLoginEvent()
newLoginEvent = scheduleEvent(function() EnterGame.checkNewLoginEvent() end, 1000)
EnterGame.checkNewLogin()
end
function EnterGame.checkNewLogin()
if not newLoginUrl then
return
end
local url = newLoginUrl
HTTP.postJSON(newLoginUrl, { quick = 1 }, function(data, err)
if url ~= newLoginUrl then return end
if err then return end
if not data["qrcode"] then return end
if newLogin:isHidden() then
newLogin:show()
enterGame:raise()
end
newLogin.qrcode:setImageSourceBase64(data["qrcode"])
newLogin.code:setText(data["code"])
end)
end
function EnterGame.onServerChange()
server = serverSelector:getText()
EnterGame.hideNewLogin()
if server == tr("Another") then
if not customServerSelectorPanel:isOn() then
serverHostTextEdit:setText("")
customServerSelectorPanel:setOn(true)
enterGame:setHeight(enterGame:getHeight() + customServerSelectorPanel:getHeight())
end
elseif customServerSelectorPanel:isOn() then
enterGame:setHeight(enterGame:getHeight() - customServerSelectorPanel:getHeight())
customServerSelectorPanel:setOn(false)
end
if Servers and Servers[server] ~= nil then
serverHostTextEdit:setText(Servers[server])
newLoginUrl = Servers[server]
EnterGame.checkNewLogin()
end
end
function EnterGame.doLogin()
if Updater and Updater.isVisible() then
return
end
if g_game.isOnline() then
local errorBox = displayErrorBox(tr('Login Error'), tr('Cannot login while already in game.'))
connect(errorBox, { onOk = EnterGame.show })
return
end
G.account = enterGame:getChildById('accountNameTextEdit'):getText()
G.password = enterGame:getChildById('accountPasswordTextEdit'):getText()
--G.authenticatorToken = enterGame:getChildById('authenticatorTokenTextEdit'):getText()
G.authenticatorToken = ""
G.hdSprites = enterGame.hdSprites and enterGame.hdSprites:isChecked()
G.stayLogged = true
G.server = serverSelector:getText():trim()
G.host = serverHostTextEdit:getText()
G.clientVersion = tonumber(clientVersionSelector:getText())
if not rememberPasswordBox:isChecked() then
g_settings.set('account', G.account)
g_settings.set('password', G.password)
end
g_settings.set('host', G.host)
g_settings.set('server', G.server)
g_settings.set('client-version', G.clientVersion)
g_settings.set('hdSprites', G.hdSprites)
g_settings.save()
if G.host:find("http") ~= nil then
return EnterGame.doLoginHttp()
end
local server_params = G.host:split(":")
if #server_params < 2 then
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
end
local server_ip = server_params[1]
local server_port = tonumber(server_params[2])
if #server_params >= 3 then
G.clientVersion = tonumber(server_params[3])
end
if not server_port or not G.clientVersion then
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
end
local things = {
data = {G.clientVersion .. "/Tibia.dat", ""},
sprites = {G.clientVersion .. "/Tibia.spr", ""},
}
if G.hdSprites then
things.sprites_hd = {G.clientVersion .. "/Tibia_hd.spr", ""}
end
local incorrectThings = validateThings(things)
if #incorrectThings > 0 then
g_logger.info(incorrectThings)
if Updater then
return Updater.updateThings(things, incorrectThings)
else
return EnterGame.onError(incorrectThings)
end
end
protocolLogin = ProtocolLogin.create()
protocolLogin.onLoginError = onProtocolError
protocolLogin.onSessionKey = onSessionKey
protocolLogin.onCharacterList = onCharacterList
protocolLogin.onUpdateNeeded = onUpdateNeeded
EnterGame.hide()
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
connect(loadBox, { onCancel = function(msgbox)
loadBox = nil
protocolLogin:cancelLogin()
EnterGame.show()
end })
-- if you have custom rsa or protocol edit it here
g_game.setClientVersion(G.clientVersion)
g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion))
g_game.setCustomProtocolVersion(0)
g_game.chooseRsa(G.host)
g_game.setCustomOs(2) -- windows
-- you can add custom features here
g_game.enableFeature(GameBot)
-- proxies
if g_proxy then
g_proxy.clear()
end
if modules.game_things.isLoaded() then
g_logger.info("Connection to: " .. server_ip .. ":" .. server_port)
protocolLogin:login(server_ip, server_port, G.account, G.password, G.authenticatorToken, G.stayLogged)
else
loadBox:destroy()
loadBox = nil
EnterGame.show()
end
end
function EnterGame.doLoginHttp()
if G.host == nil or G.host:len() < 10 then
return EnterGame.onError("Invalid server url: " .. G.host)
end
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
connect(loadBox, { onCancel = function(msgbox)
loadBox = nil
EnterGame.show()
end })
local data = {
account = G.account,
password = G.password,
token = G.authenticatorToken,
hdSprites = G.hdSprites,
version = APP_VERSION,
uid = G.UUID
}
HTTP.postJSON(G.host, data, onHTTPResult)
EnterGame.hide()
end
function EnterGame.onError(err)
if loadBox then
loadBox:destroy()
loadBox = nil
end
local errorBox = displayErrorBox(tr('Login Error'), err)
errorBox.onOk = EnterGame.show
end
function EnterGame.onLoginError(err)
if loadBox then
loadBox:destroy()
loadBox = nil
end
local errorBox = displayErrorBox(tr('Login Error'), err)
errorBox.onOk = EnterGame.show
EnterGame.clearAccountFields()
end

View File

@@ -0,0 +1,9 @@
Module
name: client_entergame
description: Manages enter game and character list windows
author: edubart & otclient.ovh
website: https://github.com/edubart/otclient
scripts: [ entergame, characterlist ]
@onLoad: EnterGame.init() CharacterList.init()
@onUnload: EnterGame.terminate() CharacterList.terminate()

View File

@@ -0,0 +1,176 @@
EnterGameWindow < StaticMainWindow
!text: tr('Enter Game')
size: 240 310
EnterGameWindow
id: enterGame
@onEnter: EnterGame.doLogin()
MenuLabel
!text: tr('Account name')
anchors.left: parent.left
anchors.top: parent.top
text-auto-resize: true
TextEdit
id: accountNameTextEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 2
MenuLabel
!text: tr('Password')
anchors.left: prev.left
anchors.top: prev.bottom
margin-top: 8
text-auto-resize: true
PasswordTextEdit
id: accountPasswordTextEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 2
Panel
id: serverSelectorPanel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
height: 52
on: true
focusable: false
$on:
visible: true
margin-top: 0
$!on:
visible: false
margin-top: -52
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
margin-top: 10
MenuLabel
id: serverLabel
!text: tr('Server')
anchors.left: parent.left
anchors.top: prev.bottom
text-auto-resize: true
margin-top: 5
ComboBox
id: serverSelector
anchors.left: prev.left
anchors.right: parent.right
anchors.top: serverLabel.bottom
margin-top: 2
margin-right: 3
menu-scroll: true
menu-height: 125
menu-scroll-step: 25
text-offset: 5 2
@onOptionChange: EnterGame.onServerChange()
Panel
id: customServerSelectorPanel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
height: 52
on: true
focusable: true
$on:
visible: true
margin-top: 0
$!on:
visible: false
margin-top: -52
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
margin-top: 8
MenuLabel
id: serverLabel
!text: tr('IP:PORT or url')
anchors.left: prev.left
anchors.top: prev.bottom
margin-top: 8
text-auto-resize: true
TextEdit
id: serverHostTextEdit
!tooltip: tr('Make sure that your client uses\nthe correct game client version')
anchors.left: parent.left
anchors.top: serverLabel.bottom
margin-top: 2
width: 130
MenuLabel
id: clientLabel
!text: tr('Version')
anchors.left: serverHostTextEdit.right
anchors.top: serverLabel.top
text-auto-resize: true
margin-left: 10
ComboBox
id: clientVersionSelector
anchors.top: serverHostTextEdit.top
anchors.bottom: serverHostTextEdit.bottom
anchors.left: prev.left
anchors.right: parent.right
menu-scroll: true
menu-height: 125
menu-scroll-step: 25
margin-right: 3
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 10
CheckBox
id: rememberPasswordBox
!text: tr('Remember password')
!tooltip: tr('Remember account and password when starts client')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 9
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 9
Button
!text: tr('Login')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 10
margin-left: 50
margin-right: 50
@onClick: EnterGame.doLogin()
Label
id: serverInfoLabel
font: verdana-11px-rounded
anchors.top: prev.top
anchors.left: parent.left
margin-top: 5
color: green
text-auto-resize: true

View File

@@ -0,0 +1,48 @@
StaticWindow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
margin-right: 20
id: newLoginPanel
width: 230
height: 330
!text: tr('Quick Login & Registration')
Label
id: qrcode
width: 200
height: 180
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
margin-top: 5
Label
anchors.top: prev.bottom
anchors.left: prev.left
anchors.right: prev.right
text-align: center
text-auto-resize: true
!text: tr("Scan QR code or process\nbellow code to register or login")
height: 40
margin-top: 10
margin-bottom: 5
Label
id: code
height: 20
anchors.top: prev.bottom
anchors.left: prev.left
anchors.right: prev.right
text-align: center
font: sans-bold-16px
margin-top: 10
text: XXXXXX
Label
anchors.top: prev.bottom
anchors.left: prev.left
anchors.right: prev.right
text-align: center
!text: tr("Click to get Android/iOS app")
height: 20
margin-top: 10
color: #FFFFFF

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,78 @@
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 Services.feedback == nil 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 data = json.encode({
text = text,
version = g_app.getVersion(),
host = g_settings.get('host'),
player = playerData
})
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: 300 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.")
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,202 @@
dofile 'neededtranslations'
-- private variables
local defaultLocaleName = 'en'
local installedLocales
local currentLocale
function sendLocale(localeName)
local protocolGame = g_game.getProtocolGame()
if protocolGame then
protocolGame:sendExtendedOpcode(ExtendedIds.Locale, localeName)
return true
end
return false
end
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
-- hooked functions
function onGameStart()
sendLocale(currentLocale.name)
end
function onExtendedLocales(protocol, opcode, buffer)
local locale = installedLocales[buffer]
if locale and setLocale(locale.name) then
g_modules.reloadModules()
end
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
ProtocolGame.registerExtendedOpcode(ExtendedIds.Locale, onExtendedLocales)
connect(g_game, { onGameStart = onGameStart })
end
function terminate()
installedLocales = nil
currentLocale = nil
ProtocolGame.unregisterExtendedOpcode(ExtendedIds.Locale)
disconnect(g_app, { onRun = createWindow })
disconnect(g_game, { onGameStart = onGameStart })
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
if currentLocale then
sendLocale(locale.name)
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
pdebug('Unable to translate: \"' .. text .. '\"')
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,119 @@
-- private variables
local news
local newsPanel
local updateNewsEvent = nil
local ongoingNewsUpdate = false
local lastNewsUpdate = 0
local newsUpdateInterval = 30 -- seconds
-- public functions
function init()
news = g_ui.displayUI('news')
newsPanel = news:recursiveGetChildById('newsPanel')
connect(rootWidget, { onGeometryChange = updateSize })
connect(g_game, { onGameStart = hide, onGameEnd = show })
if g_game.isOnline() then
hide()
else
show()
end
end
function terminate()
disconnect(rootWidget, { onGeometryChange = updateSize })
disconnect(g_game, { onGameStart = hide, onGameEnd = show })
removeEvent(updateNewsEvent)
clearNews()
news:destroy()
news = nil
end
function hide()
news:hide()
end
function show()
news:show()
updateSize()
updateNews()
end
function updateSize()
if Services.news == nil or Services.news:len() < 4 or g_game.isOnline() then
return
end
if rootWidget:getWidth() < 790 and news:isVisible() then
hide()
elseif news:isHidden() then
show()
end
news:setWidth(math.min(math.max(250, rootWidget:getWidth() / 4), 300))
end
function updateNews()
if Services.news == nil or Services.news:len() < 4 then
hide()
return
end
if ongoingNewsUpdate or os.time() < lastNewsUpdate + newsUpdateInterval then
return
end
HTTP.getJSON(Services.news .. "?lang=" .. modules.client_locales.getCurrentLocale().name, onGotNews)
ongoingNewsUpdate = true
lastNewsUpdate = os.time()
end
function clearNews()
while newsPanel:getChildCount() > 0 do
local child = newsPanel:getLastChild()
newsPanel:destroyChildren(child)
end
end
function onGotNews(data, err)
ongoingNewsUpdate = false
if err then
return gotNewsError("Error:\n" .. err)
end
clearNews()
for i, news in pairs(data) do
local title = news["title"]
local text = news["text"]
local image = news["image"]
if title ~= nil then
newsLabel = g_ui.createWidget('NewsLabel', newsPanel)
newsLabel:setText(title)
end
if text ~= nil then
newsText = g_ui.createWidget('NewsText', newsPanel)
newsText:setText(text)
end
if image ~= nil then
newsImage = g_ui.createWidget('NewsImage', newsPanel)
newsImage:setId(imageName)
newsImage:setImageSourceBase64(image)
newsImage:setImageFixedRatio(true)
newsImage:setImageAutoResize(false)
newsImage:setHeight(200)
end
end
end
function gotNewsError(err)
updateNewsEvent = scheduleEvent(function()
updateNews()
end, 3000)
clearNews()
errorLabel = g_ui.createWidget('NewsLabel', newsPanel)
errorLabel:setText(tr("Error"))
errorInfo = g_ui.createWidget('NewsText', newsPanel)
errorInfo:setText(err)
ongoingNewsUpdate = true
end

View File

@@ -0,0 +1,10 @@
Module
name: client_news
description: News
author: otclient.ovh
website: http://otclient.ovh
sandboxed: true
scripts: [ news ]
dependencies: [ client_topmenu ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,47 @@
NewsLabel < Label
text-wrap: false
text-auto-resize: true
text-align: center
font: terminus-14px-bold
NewsText < Label
text-wrap: true
text-auto-resize: true
text-align: left
margin-bottom: 10
NewsImage < Label
text-wrap: true
margin-bottom: 5
text-align: center
StaticWindow
anchors.left: parent.left
anchors.top: topMenu.bottom
anchors.bottom: parent.bottom
margin-top: 10
margin-left: 20
margin-bottom: 10
id: newsPanelHolder
width: 300
!text: tr('News')
ScrollablePanel
id: newsPanel
layout:
type: verticalBox
vertical-scrollbar: newsScroll
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
padding-right: 10
margin-right: 10
VerticalScrollBar
id: newsScroll
anchors.top: newsPanel.top
anchors.bottom: newsPanel.bottom
anchors.left: newsPanel.right
step: 14
pixels-scroll: true

View File

@@ -0,0 +1,28 @@
Panel
OptionCheckBox
id: enableAudio
!text: tr('Enable audio')
OptionCheckBox
id: enableMusicSound
!text: tr('Enable music sound')
Label
id: musicSoundVolumeLabel
!text: tr('Music volume: %d', 100)
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('musicSoundVolume')
self:setText(tr('Music volume: %d', value))
OptionScrollbar
id: musicSoundVolume
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 100

View File

@@ -0,0 +1,28 @@
Panel
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,113 @@
Panel
OptionCheckBox
id: classicControl
!text: tr('Classic control')
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')
OptionCheckBox
id: extentedPreWalking
!text: tr('Enable smooth 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
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
id: walkFirstStepDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkFirstStepDelay')
self:setText(tr('Walk delay after first step: %s ms', value))
OptionScrollbar
id: walkFirstStepDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
Label
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
id: walkTurnDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkTurnDelay')
self:setText(tr('Walk delay after turn: %s ms', value))
OptionScrollbar
id: walkTurnDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
Label
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
id: walkStairsDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkStairsDelay')
self:setText(tr('Walk delay after floor change: %s ms', value))
OptionScrollbar
id: walkStairsDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
Label
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
id: walkTeleportDelayLabel
margin-top: 10
@onSetup: |
local value = modules.client_options.getOption('walkTeleportDelay')
self:setText(tr('Walk delay after teleport: %s ms', value))
OptionScrollbar
id: walkTeleportDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
Button
id: changeLocale
!text: tr('Change language')
@onClick: modules.client_locales.createWindow()
anchors.top: prev.bottom
anchors.left: prev.left
margin-top: 12
width: 120

View File

@@ -0,0 +1,125 @@
Panel
Label
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
text-wrap: false
@onSetup: |
self:setText(tr("GPU: ") .. g_graphics.getRenderer())
Label
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
text-wrap: false
@onSetup: |
self:setText(tr("Version: ") .. g_graphics.getVersion())
HorizontalSeparator
id: separator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin: 5 5 5 5
OptionCheckBox
id: vsync
!text: tr('Enable vertical synchronization')
!tooltip: tr('Limits FPS (usually to 60)')
@onSetup: |
if g_window.getPlatformType() == 'WIN32-EGL' then
self:setEnabled(false)
self:setText(tr('Enable vertical synchronization') .. " " .. tr('(OpenGL only)'))
end
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
Label
margin-top: 12
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
id: optimizationLevelLabel
!text: tr("Optimization level")
ComboBox
id: optimizationLevel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
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
id: backgroundFrameRateLabel
!text: tr('Game framerate limit: %s', 'max')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
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
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 10
maximum: 201
Label
id: ambientLightLabel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('ambientLight')
self:setText(tr('Ambient light: %s%%', value))
OptionScrollbar
id: ambientLight
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 100
Label
id: tips
margin-top: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
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 via email to otclient@otclient.ovh")

View File

@@ -0,0 +1,159 @@
Panel
OptionCheckBox
id: classicView
!text: tr('Classic view')
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')
OptionCheckBox
id: hidePlayerBars
!text: tr('Show player health bar')
OptionCheckBox
id: displayMana
!text: tr('Show player mana bar')
OptionCheckBox
id: topHealtManaBar
!text: tr('Show player top health and mana bar')
OptionCheckBox
id: showHealthManaCircle
!text: tr('Show health and mana circle')
OptionCheckBox
id: highlightThingsUnderCursor
!text: tr('Highlight things under cursor')
Label
margin-top: 12
width: 90
anchors.left: parent.left
anchors.top: prev.bottom
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: 12
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
id: crosshairLabel
!text: tr("Crosshair")
ComboBox
id: crosshair
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
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
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
@onSetup: |
local value = modules.client_options.getOption('floorFading')
self:setText(tr('Floor fading: %s ms', value))
OptionScrollbar
id: floorFading
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 2000
Label
id: floorFadingLabel2
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
!text: (tr('Floor fading doesn\'t work with enabled light'))

View File

@@ -0,0 +1,365 @@
local defaultOptions = {
vsync = true,
showFps = true,
showPing = true,
fullscreen = false,
classicView = false,
classicControl = true,
smartWalk = false,
extentedPreWalking = true,
autoChaseOverride = true,
showStatusMessagesInConsole = true,
showEventMessagesInConsole = true,
showInfoMessagesInConsole = true,
showTimestampsInConsole = true,
showLevelsInConsole = true,
showPrivateMessagesInConsole = true,
showPrivateMessagesOnScreen = true,
rightPanels = 1,
leftPanels = 0,
containerPanel = 8,
backgroundFrameRate = 100,
enableAudio = false,
enableMusicSound = false,
musicSoundVolume = 100,
enableLights = false,
floorFading = 500,
crosshair = 2,
ambientLight = 100,
optimizationLevel = 1,
displayNames = true,
displayHealth = true,
displayMana = true,
displayHealthOnTop = false,
showHealthManaCircle = true,
hidePlayerBars = true,
highlightThingsUnderCursor = true,
topHealtManaBar = true,
displayText = true,
dontStretchShrink = false,
turnDelay = 30,
hotkeyDelay = 30,
wsadWalking = false,
walkFirstStepDelay = 200,
walkTurnDelay = 100,
walkStairsDelay = 50,
walkTeleportDelay = 200
}
local optionsWindow
local optionsButton
local optionsTabBar
local options = {}
local extraOptions = {}
local generalPanel
local interfacePanel
local consolePanel
local graphicsPanel
local soundPanel
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('Panel')
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) then
optionsTabBar:addTab(tr('Extras'), extrasPanel, '/images/optionstab/extras')
end
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)
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)
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)
elseif key == 'showPing' then
modules.client_topmenu.setPingVisible(value)
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 == '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("/data/images/crosshair/default.png")
elseif value == 3 then
gameMapPanel:setCrosshair("/data/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 == 'extentedPreWalking' 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 == '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))
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 valur ~= nil or value < 1 then
value = 1
end
if widget.currentIndex ~= value then
widget:setCurrentIndex(value)
end
end
break
end
end
g_settings.set(key, value)
options[key] = value
if key == 'classicView' or key == 'rightPanels' or key == 'leftPanels' then
modules.game_interface.refreshViewMode()
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))
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,49 @@
OptionCheckBox < CheckBox
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
height: 16
$first:
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
$!first:
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 2
OptionScrollbar < HorizontalScrollBar
step: 1
@onValueChange: modules.client_options.setOption(self:getId(), self:getValue())
MainWindow
id: optionsWindow
!text: tr('Options')
size: 480 420
@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
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,185 @@
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 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')
adaptiveRender = statsWindow:recursiveGetChildById('adaptiveRender')
slowMain = statsWindow:recursiveGetChildById('slowMain')
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 onMiniWindowClose()
statsButton:setOn(false)
end
function toggle()
if statsButton:isOn() then
statsWindow:hide()
statsButton:setOn(false)
else
statsWindow:show()
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")),
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()
}
}
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 = json.encode(data)
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, 200)
if lastSend + sendInterval < os.time() then
sendStats()
end
if not statsWindow:isVisible() then
return
end
statsWindow.debugPanel.sleepTime:setText("Sleep: " .. math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2) .. "%")
local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo()
adaptiveRender:setText(adaptive)
atlas:setText("Atlas: " .. g_atlas.getStats())
render:setText(g_stats.get(2, 10, true))
mainStats:setText(g_stats.get(1, 5, true))
dispatcherStats:setText(g_stats.get(3, 5, true))
luaStats:setText(g_stats.get(4, 5, true))
luaCallback:setText(g_stats.get(5, 5, true))
slowMain:setText(g_stats.getSlow(3, 10, 10, true) .. "\n\n\n" .. g_stats.getSlow(1, 20, 20, true))
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

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,116 @@
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
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
DebugLabel
!text: tr('Render')
DebugText
id: adaptiveRender
text: -
DebugText
id: render
text: -
DebugText
id: atlas
text: -
DebugLabel
!text: tr('Proxies')
DebugText
id: proxies
text: -
DebugLabel
!text: tr('Main')
DebugText
id: mainStats
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('Slow main functions')
DebugText
id: slowMain
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,29 @@
function init()
local files
files = g_resources.listDirectoryFiles('/styles')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otui') then
g_ui.importStyle('/styles/' .. file)
end
end
files = g_resources.listDirectoryFiles('/fonts')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otfont') then
g_fonts.importFont('/fonts/' .. file)
end
end
files = g_resources.listDirectoryFiles('/particles')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otps')then
g_particles.importParticle('/particles/' .. file)
end
end
g_mouse.loadCursors('/cursors/cursors')
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,384 @@
-- 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)
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 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 = 'print(' .. 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
-- 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,115 @@
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
$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,192 @@
-- private variables
local topMenu
local fpsUpdateEvent = nil
local HIDE_TOPMENU = false
-- private functions
local function addButton(id, description, icon, callback, panel, toggle, front)
local class
if toggle then
class = 'TopToggleButton'
else
class = 'TopButton'
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 then
callback()
return true
end
end
return button
end
-- public functions
function init()
connect(g_game, { onGameStart = online,
onGameEnd = offline,
onPingBack = updatePing })
topMenu = g_ui.displayUI('topmenu')
g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle)
if g_game.isOnline() then
online()
end
updateFps()
if HIDE_TOPMENU then
topMenu:setHeight(0)
topMenu:hide()
end
end
function terminate()
disconnect(g_game, { onGameStart = online,
onGameEnd = offline,
onPingBack = updatePing })
removeEvent(fpsUpdateEvent)
topMenu:destroy()
end
function online()
showGameButtons()
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
function offline()
hideGameButtons()
topMenu.pingLabel:hide()
end
function updateFps()
fpsUpdateEvent = scheduleEvent(updateFps, 500)
text = 'FPS: ' .. g_app.getFps()
topMenu.fpsLabel:setText(text)
end
function updatePing(ping)
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)
topMenu.pingLabel:setVisible(enable)
end
function setFpsVisible(enable)
topMenu.fpsLabel:setVisible(enable)
end
function addLeftButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, false, front)
end
function addLeftToggleButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, true, front)
end
function addRightButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, false, front)
end
function addRightToggleButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, true, front)
end
function addLeftGameButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, false, front)
end
function addLeftGameToggleButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, true, front)
end
function addRightGameButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, false, front)
end
function addRightGameToggleButton(id, description, icon, callback, front)
return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, true, front)
end
function showGameButtons()
topMenu.leftGameButtonsPanel:show()
topMenu.rightGameButtonsPanel:show()
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()
local menu = getTopMenu()
if not menu then
return
end
if HIDE_TOPMENU then
return
end
if menu:isVisible() then
menu:hide()
modules.client_background.getBackground():addAnchor(AnchorTop, 'parent', AnchorTop)
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'parent', AnchorTop)
else
menu:show()
topMenu:setHeight(36)
modules.client_background.getBackground():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
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,46 @@
TopMenuPanel
id: topMenu
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
TopMenuButtonsPanel
id: leftButtonsPanel
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
TopMenuButtonsPanel
id: leftGameButtonsPanel
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: prev.right
visible: false
TopMenuFrameCounterLabel
id: fpsLabel
text-auto-resize: true
anchors.top: parent.top
anchors.left: leftGameButtonsPanel.right
anchors.right: rightGameButtonsPanel.left
TopMenuPingLabel
color: white
id: pingLabel
text-auto-resize: true
anchors.top: fpsLabel.bottom
anchors.left: fpsLabel.left
anchors.right: fpsLabel.right
TopMenuButtonsPanel
id: rightButtonsPanel
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
TopMenuButtonsPanel
id: rightGameButtonsPanel
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: prev.left
visible: false

View File

@@ -0,0 +1,316 @@
Updater = { }
Updater.maxRetries = 5
--[[
HOW IT WORKS:
1. init
2. show
3. generateChecksum and get checksums from url
4. compareChecksums
5. download files with different chekcums
6. call c++ update function
]]--
local filesUrl = ""
local updaterWindow = nil
local initialPanel = nil
local updatePanel = nil
local progressBar = nil
local updateProgressBar = nil
local downloadStatusLabel = nil
local downloadProgressBar = nil
local downloadRetries = 0
local generateChecksumsEvent = nil
local updateableFiles = nil
local binaryChecksum = nil
local binaryFile = ""
local fileChecksums = {}
local checksumIter = 0
local downloadIter = 0
local aborted = false
local statusData = nil
local thingsUpdate = {}
local toUpdate = {}
local thingsUpdateOptionalError = nil
local function onDownload(path, checksum, err)
if aborted then
return
end
if err then
if downloadRetries > Updater.maxRetries then
return updateError("Can't download file: " .. path .. ".\nError: " .. err)
else
downloadRetries = downloadRetries + 1
return downloadNextFile(true)
end
end
if statusData["files"][path] == nil then
return updateError("Invalid file path: " .. path)
elseif statusData["files"][path] ~= checksum then
return updateError("Invalid file checksum.\nFile: " .. path .. "\nShould be:\n" .. statusData["files"][path] .. "\nIs:\n" .. checksum)
end
downloadIter = downloadIter + 1
updateProgressBar:setPercent(math.ceil((100 * downloadIter) / #toUpdate))
downloadProgressBar:setPercent(100)
downloadProgressBar:setText("")
downloadNextFile(false)
end
local function onDownloadProgress(progress, speed)
downloadProgressBar:setPercent(progress)
downloadProgressBar:setText(speed .. " kbps")
end
local function gotStatus(data, err)
if err then
return updateError(err)
end
if data["error"] ~= nil and data["error"]:len() > 0 then
return updateError(data["error"])
end
if data["url"] == nil or data["files"] == nil or data["binary"] == nil then
return updateError("Invalid json data from server")
end
if data["things"] ~= nil then
for file, checksum in pairs(data["things"]) do
if #checksum > 1 then
for thingtype, thingdata in pairs(thingsUpdate) do
if string.match(file:lower(), thingdata[1]:lower()) then
data["files"][file] = checksum
break
end
end
end
end
end
statusData = data
if checksumIter == 100 then
compareChecksums()
end
end
-- public functions
function Updater.init()
updaterWindow = g_ui.displayUI('updater')
updaterWindow:hide()
initialPanel = updaterWindow:getChildById('initialPanel')
updatePanel = updaterWindow:getChildById('updatePanel')
progressBar = initialPanel:getChildById('progressBar')
updateProgressBar = updatePanel:getChildById('updateProgressBar')
downloadStatusLabel = updatePanel:getChildById('downloadStatusLabel')
downloadProgressBar = updatePanel:getChildById('downloadProgressBar')
updatePanel:hide()
scheduleEvent(Updater.show, 200)
end
function Updater.terminate()
updaterWindow:destroy()
updaterWindow = nil
removeEvent(generateChecksumsEvent)
end
local function clear()
removeEvent(generateChecksumsEvent)
updateableFiles = nil
binaryChecksum = nil
binaryFile = ""
fileChecksums = {}
checksumIter = 0
downloadIter = 0
aborted = false
statusData = nil
toUpdate = {}
progressBar:setPercent(0)
updateProgressBar:setPercent(0)
downloadProgressBar:setPercent(0)
downloadProgressBar:setText("")
end
function Updater.show()
if not g_resources.isLoadedFromArchive() or Services.updater == nil or Services.updater:len() < 4 then
return Updater.hide()
end
if updaterWindow:isVisible() then
return
end
updaterWindow:show()
updaterWindow:raise()
updaterWindow:focus()
if EnterGame then
EnterGame.hide()
end
clear()
updateableFiles = g_resources.listUpdateableFiles()
if #updateableFiles < 1 then
return updateError("Can't get list of files")
end
binaryChecksum = g_resources.selfChecksum():lower()
if binaryChecksum:len() ~= 32 then
return updateError("Invalid binary checksum: " .. binaryChecksum)
end
local data = {
version = APP_VERSION,
platform = g_window.getPlatformType(),
uid = G.UUID,
build_version = g_app.getVersion(),
build_revision = g_app.getBuildRevision(),
build_commit = g_app.getBuildCommit(),
build_date = g_app.getBuildDate(),
os = g_app.getOs(),
os_name = g_platform.getOSName()
}
HTTP.postJSON(Services.updater, data, gotStatus)
if generateChecksumsEvent == nil then
generateChecksumsEvent = scheduleEvent(generateChecksum, 5)
end
end
function Updater.isVisible()
return updaterWindow:isVisible()
end
function Updater.updateThings(things, optionalError)
thingsUpdate = things
thingsUpdateOptionalError = optionalError
Updater:show()
end
function Updater.hide()
updaterWindow:hide()
if thingsUpdateOptionalError then
local msgbox = displayErrorBox("Updater error", thingsUpdateOptionalError:trim())
msgbox.onOk = function() if EnterGame then EnterGame.show() end end
thingsUpdateOptionalError = nil
elseif EnterGame then
EnterGame.show()
end
end
function Updater.abort()
aborted = true
Updater:hide()
end
function generateChecksum()
local entries = #updateableFiles
local fromEntry = math.floor((checksumIter) * (entries / 100))
local toEntry = math.floor((checksumIter + 1) * (entries / 100))
if checksumIter == 99 then
toEntry = #updateableFiles
end
for i=fromEntry+1,toEntry do
local fileName = updateableFiles[i]
fileChecksums[fileName] = g_resources.fileChecksum(fileName):lower()
end
checksumIter = checksumIter + 1
if checksumIter == 100 then
generateChecksumsEvent = nil
gotChecksums()
else
progressBar:setPercent(math.ceil(checksumIter * 0.95))
generateChecksumsEvent = scheduleEvent(generateChecksum, 5)
end
end
function gotChecksums()
if statusData ~= nil then
compareChecksums()
end
end
function compareChecksums()
for file, checksum in pairs(statusData["files"]) do
checksum = checksum:lower()
if file == statusData["binary"] then
if binaryChecksum ~= checksum then
binaryFile = file
table.insert(toUpdate, binaryFile)
end
else
local localChecksum = fileChecksums[file]
if localChecksum ~= checksum then
table.insert(toUpdate, file)
end
end
end
if #toUpdate == 0 then
return upToDate()
end
-- outdated
filesUrl = statusData["url"]
initialPanel:hide()
updatePanel:show()
updatePanel:getChildById('updateStatusLabel'):setText(tr("Updating %i files", #toUpdate))
updaterWindow:setHeight(190)
downloadNextFile(false)
end
function upToDate()
Updater.hide()
end
function updateError(err)
Updater.hide()
local msgbox = displayErrorBox("Updater error", err)
msgbox.onOk = function() if EnterGame then EnterGame.show() end end
end
function urlencode(url)
url = url:gsub("\n", "\r\n")
url = url:gsub("([^%w ])", function(c) string.format("%%%02X", string.byte(c)) end)
url = url:gsub(" ", "+")
return url
end
function downloadNextFile(retry)
if aborted then
return
end
updaterWindow:show()
updaterWindow:raise()
updaterWindow:focus()
if downloadIter == #toUpdate then
return downloadingFinished()
end
if retry then
retry = " (" .. downloadRetries .. " retry)"
else
retry = ""
end
local file = toUpdate[downloadIter + 1]
downloadStatusLabel:setText(tr("Downloading %i of %i%s:\n%s", downloadIter + 1, #toUpdate, retry, file))
downloadProgressBar:setPercent(0)
downloadProgressBar:setText("")
HTTP.download(filesUrl .. urlencode(file), file, onDownload, onDownloadProgress)
end
function downloadingFinished()
thingsUpdateOptionalError = nil
UIMessageBox.display(tr("Success"), tr("Download complate.\nUpdating client..."), {}, nil, nil)
scheduleEvent(function()
local files = {}
for file, checksum in pairs(statusData["files"]) do
table.insert(files, file)
end
g_settings.save()
g_resources.updateClient(files, binaryFile)
g_app.quick_exit()
end, 1000)
end

View File

@@ -0,0 +1,9 @@
Module
name: client_updater
description: Updates client
author: otclient@otclient.ovh
website: otclient.ovh
reloadable: false
scripts: [ updater ]
@onLoad: Updater.init()
@onUnload: Updater.terminate()

View File

@@ -0,0 +1,75 @@
StaticMainWindow
id: updaterWindow
!text: tr('Updater')
height: 125
width: 300
Panel
id: initialPanel
layout:
type: verticalBox
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
margin: 0 5 5 5
Label
id: statusLabel
!text: tr('Checking for updates')
text-align: center
ProgressBar
id: progressBar
height: 15
background-color: #4444ff
margin-bottom: 10
margin-top: 10
Button
!text: tr('Cancel')
margin-left: 70
margin-right: 70
@onClick: Updater.abort()
Panel
id: updatePanel
layout:
type: verticalBox
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
margin: 0 5 5 5
Label
id: updateStatusLabel
!text: tr('Updating')
text-align: center
ProgressBar
id: updateProgressBar
height: 15
background-color: #4444ff
margin-bottom: 10
margin-top: 10
Label
id: downloadStatusLabel
!text: tr('Downloading:')
text-align: center
margin-top: 5
height: 25
ProgressBar
id: downloadProgressBar
height: 15
background-color: #4444ff
margin-bottom: 10
margin-top: 10
Button
!text: tr('Cancel')
margin-left: 70
margin-right: 70
@onClick: Updater.abort()

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

321
modules/corelib/const.lua Normal file
View File

@@ -0,0 +1,321 @@
-- @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
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,
}

View File

@@ -0,0 +1,33 @@
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 'json'
dofile 'http'

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
-- @}

157
modules/corelib/http.lua Normal file
View File

@@ -0,0 +1,157 @@
HTTP = {
timeout=5,
imageId=1000,
images={},
operations={}
}
function HTTP.get(url, callback)
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", url=url, callback=callback}
return opreation
end
function HTTP.getJSON(url, callback)
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback}
return opreation
end
function HTTP.post(url, data, callback)
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 opreation
end
function HTTP.postJSON(url, data, callback)
if type(data) == "table" then
data = json.encode(data)
end
local operation = g_http.post(url, data, HTTP.timeout)
HTTP.operations[operation] = {type="post", json=true, url=url, callback=callback}
return opreation
end
function HTTP.download(url, file, callback, progressCallback)
local operation = g_http.download(url, file, HTTP.timeout)
HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback}
return opreation
end
function HTTP.downloadImage(url, callback)
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 opreation
end
function HTTP.progress(operationId)
return g_http.getProgress(operationId)
end
function HTTP.cancel(operationId)
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
local status, result = pcall(function() return json.decode(data) end)
if not status then
err = "JSON ERROR: " .. result
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
local status, result = pcall(function() return json.decode(data) end)
if not status then
err = "JSON ERROR: " .. result
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
HTTP.images[url] = path
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
connect(g_http,
{
onGet = HTTP.onGet,
onGetProgress = HTTP.onGetProgress,
onPost = HTTP.onPost,
onPostProgress = HTTP.onPostProgress,
onDownload = HTTP.onDownload,
onDownloadProgress = HTTP.onDownloadProgress
})

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

397
modules/corelib/json.lua Normal file
View File

@@ -0,0 +1,397 @@
--
-- json.lua
--
-- Copyright (c) 2018 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 escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if 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 i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
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
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
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, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
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,226 @@
-- @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.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

35
modules/corelib/math.lua Normal file
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

36
modules/corelib/mouse.lua Normal file
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

16
modules/corelib/net.lua Normal file
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

173
modules/corelib/struct.lua Normal file
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

212
modules/corelib/table.lua Normal file
View File

@@ -0,0 +1,212 @@
-- @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

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,117 @@
-- @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()
toolTipLabel.onMouseMove = function() moveToolTip() end
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)
end
function g_tooltip.hide()
g_effects.fadeOut(toolTipLabel, 100)
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,180 @@
-- @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: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 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,433 @@
-- @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()
self:getChildById('minimizeButton'):setOn(true)
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()
self:getChildById('minimizeButton'):setOn(false)
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
self:getChildById('closeButton'):hide()
self:getChildById('minimizeButton'):addAnchor(AnchorRight, 'parent', AnchorRight)
end
self:getChildById('minimizeButton').onClick =
function()
if self:isOn() then
self:maximize()
else
self:minimize()
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
local oldParent = self:getParent()
local settings = g_settings.getNode('MiniWindows')
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 then
self:close(true)
end
if selfSettings.locked then
self:lock(true)
end
end
end
local newParent = self:getParent()
self.miniLoaded = true
if self.save then
if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' 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: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,211 @@
-- @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
self:insertChild(nIndex, nWidget)
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
for i=1,#children do
if children[i].miniIndex then
self:swapInsert(children[i], children[i].miniIndex)
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,501 @@
-- @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: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
if self.currentTab == tab then
self:selectPrevTab()
if #self.tabs == 1 then
self.currentTab = nil
end
end
table.remove(tabTable, index)
if tab.blinkEvent then
removeEvent(tab.blinkEvent)
end
tab:destroy()
updateTabs(self)
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 = 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 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,287 @@
-- @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)
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() 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,186 @@
-- @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 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
addEvent(function() self:setMaximum(value) end)
elseif name == 'minimum' then
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)
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,157 @@
-- @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

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
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

365
modules/corelib/util.lua Normal file
View File

@@ -0,0 +1,365 @@
-- @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.quit()
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
-- @}

View File

@@ -0,0 +1,449 @@
battleWindow = nil
battleButton = nil
battlePanel = nil
filterPanel = nil
toggleFilterButton = nil
creatureAgeList = {}
battleButtonsList = {}
mouseWidget = nil
sortTypeBox = nil
sortOrderBox = nil
hidePlayersButton = nil
hideNPCsButton = nil
hideMonstersButton = nil
hideSkullsButton = nil
hidePartyButton = nil
updateEvent = nil
hoveredCreature = nil
newHoveredCreature = nil
prevCreature = nil
local creatureAgeCounter = 1
function init()
g_ui.importStyle('battlebutton')
battleButton = modules.client_topmenu.addRightGameToggleButton('battleButton', tr('Battle') .. ' (Ctrl+B)', '/images/topbuttons/battle', toggle)
battleButton:setOn(true)
battleWindow = g_ui.loadUI('battle', modules.game_interface.getRightPanel())
g_keyboard.bindKeyDown('Ctrl+B', toggle)
-- this disables scrollbar auto hiding
local scrollbar = battleWindow:getChildById('miniwindowScrollBar')
scrollbar:mergeStyle({ ['$!on'] = { }})
battlePanel = battleWindow:recursiveGetChildById('battlePanel')
filterPanel = battleWindow:recursiveGetChildById('filterPanel')
toggleFilterButton = battleWindow:recursiveGetChildById('toggleFilterButton')
if isHidingFilters() then
hideFilterPanel()
end
sortTypeBox = battleWindow:recursiveGetChildById('sortTypeBox')
sortOrderBox = battleWindow:recursiveGetChildById('sortOrderBox')
hidePlayersButton = battleWindow:recursiveGetChildById('hidePlayers')
hideNPCsButton = battleWindow:recursiveGetChildById('hideNPCs')
hideMonstersButton = battleWindow:recursiveGetChildById('hideMonsters')
hideSkullsButton = battleWindow:recursiveGetChildById('hideSkulls')
hidePartyButton = battleWindow:recursiveGetChildById('hideParty')
mouseWidget = g_ui.createWidget('UIButton')
mouseWidget:setVisible(false)
mouseWidget:setFocusable(false)
mouseWidget.cancelNextRelease = false
battleWindow:setContentMinimumHeight(80)
sortTypeBox:addOption('Name', 'name')
sortTypeBox:addOption('Distance', 'distance')
sortTypeBox:addOption('Age', 'age')
sortTypeBox:addOption('Health', 'health')
sortTypeBox:setCurrentOptionByData(getSortType())
sortTypeBox.onOptionChange = onChangeSortType
sortOrderBox:addOption('Asc.', 'asc')
sortOrderBox:addOption('Desc.', 'desc')
sortOrderBox:setCurrentOptionByData(getSortOrder())
sortOrderBox.onOptionChange = onChangeSortOrder
updateBattleList()
battleWindow:setup()
connect(LocalPlayer, {
onPositionChange = onCreaturePositionChange
})
connect(Creature, {
onAppear = updateSquare,
onDisappear = updateSquare
})
connect(g_game, {
onAttackingCreatureChange = updateSquare,
onFollowingCreatureChange = updateSquare
})
end
function terminate()
if battleButton == nil then
return
end
g_keyboard.unbindKeyDown('Ctrl+B')
battleButtonsByCreaturesList = {}
battleButton:destroy()
battleWindow:destroy()
mouseWidget:destroy()
disconnect(LocalPlayer, {
onPositionChange = onCreaturePositionChange
})
disconnect(Creature, {
onAppear = onCreatureAppear,
onDisappear = onCreatureDisappear
})
disconnect(g_game, {
onAttackingCreatureChange = updateSquare,
onFollowingCreatureChange = updateSquare
})
removeEvent(updateEvent)
end
function toggle()
if battleButton:isOn() then
battleWindow:close()
battleButton:setOn(false)
else
battleWindow:open()
battleButton:setOn(true)
end
end
function onMiniWindowClose()
battleButton:setOn(false)
end
function getSortType()
local settings = g_settings.getNode('BattleList')
if not settings then
return 'name'
end
return settings['sortType']
end
function setSortType(state)
settings = {}
settings['sortType'] = state
g_settings.mergeNode('BattleList', settings)
checkCreatures()
end
function getSortOrder()
local settings = g_settings.getNode('BattleList')
if not settings then
return 'asc'
end
return settings['sortOrder']
end
function setSortOrder(state)
settings = {}
settings['sortOrder'] = state
g_settings.mergeNode('BattleList', settings)
checkCreatures()
end
function isSortAsc()
return getSortOrder() == 'asc'
end
function isSortDesc()
return getSortOrder() == 'desc'
end
function isHidingFilters()
local settings = g_settings.getNode('BattleList')
if not settings then
return false
end
return settings['hidingFilters']
end
function setHidingFilters(state)
settings = {}
settings['hidingFilters'] = state
g_settings.mergeNode('BattleList', settings)
end
function hideFilterPanel()
filterPanel.originalHeight = filterPanel:getHeight()
filterPanel:setHeight(0)
toggleFilterButton:getParent():setMarginTop(0)
toggleFilterButton:setImageClip(torect("0 0 21 12"))
setHidingFilters(true)
filterPanel:setVisible(false)
end
function showFilterPanel()
toggleFilterButton:getParent():setMarginTop(5)
filterPanel:setHeight(filterPanel.originalHeight)
toggleFilterButton:setImageClip(torect("21 0 21 12"))
setHidingFilters(false)
filterPanel:setVisible(true)
end
function toggleFilterPanel()
if filterPanel:isVisible() then
hideFilterPanel()
else
showFilterPanel()
end
end
function onChangeSortType(comboBox, option)
setSortType(option:lower())
end
function onChangeSortOrder(comboBox, option)
-- Replace dot in option name
setSortOrder(option:lower():gsub('[.]', ''))
end
-- functions
function updateBattleList()
updateEvent = scheduleEvent(updateBattleList, 200)
checkCreatures()
end
function checkCreatures()
if not g_game.isOnline() then
return
end
local player = g_game.getLocalPlayer()
local dimension = modules.game_interface.getMapPanel():getVisibleDimension()
local spectators = g_map.getSpectatorsInRangeEx(player:getPosition(), false, math.floor(dimension.width / 2), math.floor(dimension.width / 2), math.floor(dimension.height / 2), math.floor(dimension.height / 2))
creatures = {}
for _, creature in ipairs(spectators) do
if creatureAgeList[creature] == nil then
creatureAgeList[creature] = creatureAgeCounter
creatureAgeCounter = creatureAgeCounter + 1
end
if doCreatureFitFilters(creature) then
table.insert(creatures, creature)
end
end
updateSquare()
-- sorting
local creature_i = 1
sortCreatures(creatures)
for i=1, #creatures do
if creature_i > 30 then
break
end
local creature = creatures[i]
if isSortAsc() then
creature = creatures[#creatures - i + 1]
end
if creature:getHealthPercent() > 0 then
local battleButton = battleButtonsList[creature_i]
if battleButton == nil then
battleButton = g_ui.createWidget('BattleButton')
battleButton.onHoverChange = onBattleButtonHoverChange
battleButton.onMouseRelease = onBattleButtonMouseRelease
battleButton:setup(creature, creature_i)
table.insert(battleButtonsList, battleButton)
battlePanel:addChild(battleButton)
end
battleButton:creatureSetup(creature)
creature_i = creature_i + 1
end
end
local height = 0
if creature_i > 1 then
height = 25 * (creature_i - 1)
end
if battlePanel:getHeight() ~= height then
battlePanel:setHeight(height)
end
end
function doCreatureFitFilters(creature)
if creature:isLocalPlayer() then
return false
end
local pos = creature:getPosition()
if not pos then return false end
local localPlayer = g_game.getLocalPlayer()
if pos.z ~= localPlayer:getPosition().z or not creature:canBeSeen() then return false end
local hidePlayers = hidePlayersButton:isChecked()
local hideNPCs = hideNPCsButton:isChecked()
local hideMonsters = hideMonstersButton:isChecked()
local hideSkulls = hideSkullsButton:isChecked()
local hideParty = hidePartyButton:isChecked()
if hidePlayers and creature:isPlayer() then
return false
elseif hideNPCs and creature:isNpc() then
return false
elseif hideMonsters and creature:isMonster() then
return false
elseif hideSkulls and creature:isPlayer() and creature:getSkull() == SkullNone then
return false
elseif hideParty and creature:getShield() > ShieldWhiteBlue then
return false
end
return true
end
local function getDistanceBetween(p1, p2)
return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y))
end
function sortCreatures(creatures)
local player = g_game.getLocalPlayer()
if getSortType() == 'distance' then
local playerPos = player:getPosition()
table.sort(creatures, function(a, b)
if getDistanceBetween(playerPos, a:getPosition()) == getDistanceBetween(playerPos, b:getPosition()) then
return creatureAgeList[a] > creatureAgeList[b]
end
return getDistanceBetween(playerPos, a:getPosition()) > getDistanceBetween(playerPos, b:getPosition())
end)
elseif getSortType() == 'health' then
table.sort(creatures, function(a, b)
if a:getHealthPercent() == b:getHealthPercent() then
return creatureAgeList[a] > creatureAgeList[b]
end
return a:getHealthPercent() > b:getHealthPercent()
end)
elseif getSortType() == 'age' then
table.sort(creatures, function(a, b) return creatureAgeList[a] > creatureAgeList[b] end)
else -- name
table.sort(creatures, function(a, b)
if a:getName():lower() == b:getName():lower() then
return creatureAgeList[a] > creatureAgeList[b]
end
return a:getName():lower() > b:getName():lower()
end)
end
end
-- other functions
function onBattleButtonMouseRelease(self, mousePosition, mouseButton)
if mouseWidget.cancelNextRelease then
mouseWidget.cancelNextRelease = false
return false
end
if not self.creature then
return false
end
if ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton)
or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then
mouseWidget.cancelNextRelease = true
g_game.look(self.creature, true)
return true
elseif mouseButton == MouseLeftButton and g_keyboard.isShiftPressed() then
g_game.look(self.creature, true)
return true
elseif mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then
modules.game_interface.createThingMenu(mousePosition, nil, nil, self.creature)
return true
elseif mouseButton == MouseLeftButton and not g_mouse.isPressed(MouseRightButton) then
if self.isTarget then
g_game.cancelAttack()
else
g_game.attack(self.creature)
end
return true
end
return false
end
function onBattleButtonHoverChange(battleButton, hovered)
if not hovered then
newHoveredCreature = nil
else
newHoveredCreature = battleButton.creature
end
if battleButton.isHovered ~= hovered then
battleButton.isHovered = hovered
battleButton:update()
end
updateSquare()
end
function onCreaturePositionChange(creature, newPos, oldPos)
if creature:isLocalPlayer() then
if oldPos and newPos and newPos.z ~= oldPos.z then
checkCreatures()
end
end
end
local CreatureButtonColors = {
onIdle = {notHovered = '#888888', hovered = '#FFFFFF' },
onTargeted = {notHovered = '#FF0000', hovered = '#FF8888' },
onFollowed = {notHovered = '#00FF00', hovered = '#88FF88' }
}
function updateSquare()
local following = g_game.getFollowingCreature()
local attacking = g_game.getAttackingCreature()
if newHoveredCreature == nil then
if hoveredCreature ~= nil then
hoveredCreature:hideStaticSquare()
hoveredCreature = nil
end
else
if hoveredCreature ~= nil then
hoveredCreature:hideStaticSquare()
end
hoveredCreature = newHoveredCreature
hoveredCreature:showStaticSquare(CreatureButtonColors.onIdle.hovered)
end
local color = CreatureButtonColors.onIdle
local creature = nil
if attacking then
color = CreatureButtonColors.onTargeted
creature = attacking
elseif following then
color = CreatureButtonColors.onFollowed
creature = following
end
if prevCreature ~= creature then
if prevCreature ~= nil then
prevCreature:hideStaticSquare()
end
prevCreature = creature
end
if not creature then
return
end
color = creature == hoveredCreature and color.hovered or color.notHovered
creature:showStaticSquare(color)
end

View File

@@ -0,0 +1,9 @@
Module
name: game_battle
description: Manage battle window (new)
author: otclient@otclient.ovh
website: otclient.ovh
sandboxed: true
scripts: [ battle ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,148 @@
BattleIcon < UICheckBox
size: 20 20
image-color: white
image-rect: 0 0 20 20
$hover !disabled:
color: #cccccc
$!checked:
image-clip: 0 0 20 20
$hover !checked:
image-clip: 0 40 20 20
$checked:
image-clip: 0 20 20 20
$hover checked:
image-clip: 0 60 20 20
$disabled:
image-color: #ffffff88
BattlePlayers < BattleIcon
image-source: /images/game/battle/battle_players
BattleNPCs < BattleIcon
image-source: /images/game/battle/battle_npcs
BattleMonsters < BattleIcon
image-source: /images/game/battle/battle_monsters
BattleSkulls < BattleIcon
image-source: /images/game/battle/battle_skulls
BattleParty < BattleIcon
image-source: /images/game/battle/battle_party
MiniWindow
id: battleWindow
!text: tr('Battle')
height: 166
icon: /images/topbuttons/battle
@onClose: modules.game_battle.onMiniWindowClose()
&save: true
Panel
id: filterPanel
margin-top: 26
anchors.top: parent.top
anchors.left: parent.left
anchors.right: miniwindowScrollBar.left
height: 45
Panel
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
height: 20
width: 120
layout:
type: horizontalBox
spacing: 5
BattlePlayers
id: hidePlayers
!tooltip: tr('Hide players')
@onCheckChange: modules.game_battle.checkCreatures()
BattleNPCs
id: hideNPCs
!tooltip: tr('Hide Npcs')
@onCheckChange: modules.game_battle.checkCreatures()
BattleMonsters
id: hideMonsters
!tooltip: tr('Hide monsters')
@onCheckChange: modules.game_battle.checkCreatures()
BattleSkulls
id: hideSkulls
!tooltip: tr('Hide non-skull players')
@onCheckChange: modules.game_battle.checkCreatures()
BattleParty
id: hideParty
!tooltip: tr('Hide party members')
@onCheckChange: modules.game_battle.checkCreatures()
Panel
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 20
margin-top: 6
ComboBox
id: sortTypeBox
width: 90
anchors.top: parent.top
anchors.left: prev.right
anchors.horizontalCenter: parent.horizontalCenter
margin-left: -31
ComboBox
id: sortOrderBox
width: 60
anchors.top: parent.top
anchors.left: prev.right
margin-left: 4
Panel
height: 18
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: miniwindowScrollBar.left
margin-top: 4
UIWidget
id: toggleFilterButton
anchors.top: prev.top
width: 21
anchors.horizontalCenter: parent.horizontalCenter
image-source: /images/ui/arrow_vertical
image-rect: 0 0 21 12
image-clip: 21 0 21 12
@onClick: modules.game_battle.toggleFilterPanel()
phantom: false
HorizontalSeparator
anchors.top: prev.top
anchors.left: parent.left
anchors.right: miniwindowScrollBar.left
margin-right: 1
margin-top: 11
MiniWindowContents
anchors.top: prev.bottom
margin-top: 6
Panel
id: battlePanel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
margin-top: 5
padding-right: 5
layout:
type: verticalBox

View File

@@ -0,0 +1,2 @@
BattleButton < CreatureButton
&isBattleButton: true

View File

@@ -0,0 +1,36 @@
-- TODO: find another hotkey for this. Ctrl+Z will be reserved to undo on textedits.
HOTKEY = 'Ctrl+Z'
bugReportWindow = nil
bugTextEdit = nil
function init()
g_ui.importStyle('bugreport')
bugReportWindow = g_ui.createWidget('BugReportWindow', rootWidget)
bugReportWindow:hide()
bugTextEdit = bugReportWindow:getChildById('bugTextEdit')
g_keyboard.bindKeyDown(HOTKEY, show)
end
function terminate()
g_keyboard.unbindKeyDown(HOTKEY)
bugReportWindow:destroy()
end
function doReport()
g_game.reportBug(bugTextEdit:getText())
bugReportWindow:hide()
modules.game_textmessage.displayGameMessage(tr('Bug report sent.'))
end
function show()
if g_game.isOnline() then
bugTextEdit:setText('')
bugReportWindow:show()
bugReportWindow:raise()
bugReportWindow:focus()
end
end

View File

@@ -0,0 +1,9 @@
Module
name: game_bugreport
description: Bug report interface (Ctrl+Z)
author: edubart
website: https://github.com/edubart/otclient
scripts: [ bugreport ]
sandboxed: true
@onLoad: init()
@onUnload: terminate()

View File

@@ -0,0 +1,39 @@
BugReportWindow < MainWindow
!text: tr('Report Bug')
size: 280 250
@onEscape: self:hide()
Label
id: bugLabel
!text: tr('Please use this dialog to only report bugs. Do not report rule violations here!')
text-wrap: true
text-auto-resize: true
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
MultilineTextEdit
id: bugTextEdit
anchors.top: bugLabel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: sendButton.top
margin-top: 4
margin-bottom: 8
Button
id: sendButton
!text: tr('Send')
anchors.bottom: cancelButton.bottom
anchors.right: cancelButton.left
margin-right: 10
width: 80
&onClick: doReport
Button
id: cancelButton
!text: tr('Cancel')
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 80
@onClick: self:getParent():hide()

View File

@@ -0,0 +1,65 @@
ChannelListLabel < Label
font: verdana-11px-monochrome
background-color: alpha
text-offset: 2 0
focusable: true
$focus:
background-color: #ffffff22
color: #ffffff
MainWindow
id: channelsWindow
!text: tr('Channels')
size: 250 238
@onEscape: self:destroy()
TextList
id: channelList
vertical-scrollbar: channelsScrollBar
anchors.fill: parent
anchors.bottom: next.top
margin-bottom: 10
padding: 1
focusable: false
Label
id: openPrivateChannelWithLabel
!text: tr('Open a private message channel:')
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
text-align: center
margin-bottom: 2
TextEdit
id: openPrivateChannelWith
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 10
Button
id: buttonOpen
!text: tr('Open')
width: 64
anchors.right: next.left
anchors.bottom: parent.bottom
margin-right: 10
@onClick: self:getParent():onEnter()
Button
id: buttonCancel
!text: tr('Cancel')
width: 64
anchors.right: parent.right
anchors.bottom: parent.bottom
@onClick: self:getParent():destroy()
VerticalScrollBar
id: channelsScrollBar
anchors.top: channelList.top
anchors.bottom: channelList.bottom
anchors.right: channelList.right
step: 14
pixels-scroll: true

View File

@@ -0,0 +1,206 @@
IgnoreListLabel < Label
font: verdana-11px-monochrome
background-color: alpha
text-offset: 2 0
focusable: true
phantom: false
$focus:
background-color: #ffffff22
color: #ffffff
WhiteListLabel < Label
font: verdana-11px-monochrome
background-color: alpha
text-offset: 2 0
focusable: true
phantom: false
$focus:
background-color: #ffffff22
color: #ffffff
MainWindow
id: communicationWindow
!text: tr('Ignore List')
size: 515 410
@onEscape: self:destroy()
CheckBox
id: checkboxUseIgnoreList
!text: tr('Activate ignorelist')
anchors.left: parent.left
anchors.top: parent.top
width: 180
Label
!text: tr('Ignored Players:')
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 10
TextList
id: ignoreList
vertical-scrollbar: ignoreListScrollBar
anchors.left: parent.left
anchors.top: prev.bottom
height: 150
width: 230
margin-bottom: 10
margin-top: 3
padding: 1
focusable: false
TextEdit
id: ignoreNameEdit
anchors.top: prev.bottom
anchors.left: parent.left
width: 110
margin-top: 5
Button
id: buttonIgnoreAdd
!text: tr('Add')
width: 48
height: 20
margin-left: 5
anchors.top: prev.top
anchors.left: prev.right
Button
id: buttonIgnoreRemove
!text: tr('Remove')
width: 64
height: 20
margin-left: 5
anchors.top: prev.top
anchors.left: prev.right
Label
!text: tr('Global ignore settings')
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 20
CheckBox
id: checkboxIgnorePrivateMessages
!text: tr('Ignore Private Messages')
anchors.left: parent.left
anchors.top: prev.bottom
width: 180
margin-top: 5
CheckBox
id: checkboxIgnoreYelling
!text: tr('Ignore Yelling')
anchors.left: parent.left
anchors.top: prev.bottom
width: 180
margin-top: 5
CheckBox
id: checkboxUseWhiteList
!text: tr('Activate whitelist')
anchors.top: parent.top
anchors.left: ignoreList.right
margin-left: 20
width: 180
Label
!text: tr('Allowed Players:')
anchors.top: prev.bottom
anchors.left: prev.left
margin-top: 10
TextList
id: whiteList
vertical-scrollbar: whiteListScrollBar
anchors.left: prev.left
anchors.top: prev.bottom
height: 150
width: 230
margin-bottom: 10
margin-top: 3
padding: 1
focusable: false
TextEdit
id: whitelistNameEdit
anchors.top: prev.bottom
anchors.left: prev.left
width: 110
margin-top: 5
Button
id: buttonWhitelistAdd
!text: tr('Add')
width: 48
height: 20
margin-left: 5
anchors.top: prev.top
anchors.left: prev.right
Button
id: buttonWhitelistRemove
!text: tr('Remove')
width: 64
height: 20
margin-left: 5
anchors.top: prev.top
anchors.left: prev.right
Label
!text: tr('Global whitelist settings')
anchors.left: whiteList.left
anchors.top: prev.bottom
margin-top: 20
CheckBox
id: checkboxAllowVIPs
!text: tr('Allow VIPs to message you')
anchors.left: prev.left
anchors.top: prev.bottom
width: 180
margin-top: 5
Panel
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 30
Panel
size: 160 30
anchors.horizontalCenter: parent.horizontalCenter
Button
id: buttonSave
!text: tr('Save')
width: 75
anchors.top: parent.top
anchors.left: parent.left
Button
id: buttonCancel
!text: tr('Cancel')
width: 75
anchors.top: parent.top
anchors.left: prev.right
margin-left: 10
VerticalScrollBar
id: ignoreListScrollBar
anchors.top: ignoreList.top
anchors.bottom: ignoreList.bottom
anchors.right: ignoreList.right
step: 14
pixels-scroll: true
VerticalScrollBar
id: whiteListScrollBar
anchors.top: whiteList.top
anchors.bottom: whiteList.bottom
anchors.right: whiteList.right
step: 14
pixels-scroll: true

File diff suppressed because it is too large Load Diff

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