This commit is contained in:
OTCv8
2020-06-09 18:19:20 +02:00
parent 76d6f2ce7d
commit 541e189d3f
154 changed files with 2540 additions and 1221 deletions

View File

@@ -57,14 +57,17 @@ function init()
connect(g_game, { onGameStart = onGameStart,
onGameEnd = onGameEnd })
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
if not Updater then
if g_resources.getLayout() == "mobile" then
g_window.setMinimumSize({ width = 640, height = 360 })
else
g_window.setMinimumSize({ width = 800, height = 640 })
end
-- window size
local size = { width = 1024, height = 600 }
size = g_settings.getSize('window-size', size)
@@ -87,12 +90,7 @@ function init()
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

View File

@@ -14,10 +14,11 @@ Module
- client_locales
- client_topmenu
- client_background
- client_textedit
- client_options
- client_entergame
- client_entergamev2
- client_terminal
- client_stats
- client_feedback
- client_updater
- client_mobile

View File

@@ -8,9 +8,8 @@ function init()
background:lower()
clientVersionLabel = background:getChildById('clientVersionLabel')
clientVersionLabel:setText(g_app.getName() .. ' ' .. g_app.getVersion() .. '\nMade by:\n' .. g_app.getAuthor() .. "\notclient@otclient.ovh")
clientVersionLabel:setText('OTClientV8 ' .. 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

View File

@@ -5,6 +5,5 @@ Module
website: https://github.com/edubart/otclient
sandboxed: true
scripts: [ background ]
dependencies: [ client_topmenu ]
@onLoad: init()
@onUnload: terminate()

View File

@@ -1,7 +1,11 @@
Background
UIWidget
id: background
anchors.fill: parent
focusable: false
image-source: /images/background
image-smooth: true
image-fixed-ratio: true
margin-top: 1
UILabel
id: clientVersionLabel

View File

@@ -45,12 +45,14 @@ StaticMainWindow
id: charactersWindow
!text: tr('Character List')
visible: false
size: 350 400
$mobile:
size: 350 280
@onEnter: CharacterList.doLogin()
@onEscape: CharacterList.hide(true)
@onSetup: |
g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self)
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
self:setSize({width = 350, height = 400})
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
TextList
id: characters

View File

@@ -17,6 +17,8 @@ local serverHostTextEdit
local rememberPasswordBox
local protos = {"740", "760", "772", "792", "800", "810", "854", "860", "870", "961", "1077", "1090", "1096", "1098", "1099", "1100"}
local checkedByUpdater = {}
-- private functions
local function onProtocolError(protocol, message, errorCode)
if errorCode then
@@ -129,11 +131,7 @@ local function onHTTPResult(data, err)
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
return EnterGame.onError(incorrectThings)
end
-- custom protocol
@@ -264,9 +262,6 @@ end
function EnterGame.show()
if not enterGame then return end
if Updater and Updater.isVisible() or g_game.isOnline() then
return EnterGame.hide()
end
enterGame:show()
enterGame:raise()
enterGame:focus()
@@ -313,9 +308,6 @@ function EnterGame.onServerChange()
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 })
@@ -339,23 +331,20 @@ function EnterGame.doLogin()
g_settings.set('client-version', G.clientVersion)
g_settings.save()
if G.host:find("ws://") ~= nil or G.host:find("wss://") ~= nil then
return EnterGame.doLoginWs()
end
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])
local server_port = 7171
if #server_params >= 2 then
server_port = tonumber(server_params[2])
end
if #server_params >= 3 then
G.clientVersion = tonumber(server_params[3])
end
if not server_port or not G.clientVersion then
if type(server_ip) ~= 'string' or server_ip:len() <= 3 or not server_port or not G.clientVersion then
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
end
@@ -367,8 +356,12 @@ function EnterGame.doLogin()
local incorrectThings = validateThings(things)
if #incorrectThings > 0 then
g_logger.error(incorrectThings)
if Updater then
return Updater.updateThings(things, incorrectThings)
if Updater and not checkedByUpdater[G.clientVersion] then
checkedByUpdater[G.clientVersion] = true
return Updater.check({
version = G.clientVersion,
host = G.host
})
else
return EnterGame.onError(incorrectThings)
end

View File

@@ -6,4 +6,7 @@ Module
scripts: [ entergame, characterlist ]
@onLoad: EnterGame.init() CharacterList.init()
@onUnload: EnterGame.terminate() CharacterList.terminate()
load-later:
- game_things
- game_features

View File

@@ -4,19 +4,7 @@ dofile 'neededtranslations'
local defaultLocaleName = 'en'
local installedLocales
local currentLocale
function sendLocale(localeName)
if not g_game.getFeature(GameExtendedOpcode) then
return
end
local protocolGame = g_game.getProtocolGame()
if protocolGame then
protocolGame:sendExtendedOpcode(ExtendedIds.Locale, localeName)
return true
end
return false
end
local missingTranslations = {}
function createWindow()
localesWindow = g_ui.displayUI('locales')
@@ -51,18 +39,6 @@ function selectFirstLocale(name)
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 = {}
@@ -76,18 +52,13 @@ function init()
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 })
--disconnect(g_app, { onRun = createWindow })
end
function generateNewTranslationTable(localename)
@@ -154,9 +125,6 @@ function setLocale(name)
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
@@ -194,7 +162,10 @@ function _G.tr(text, ...)
if not translation then
if translation == nil then
if currentLocale.name ~= defaultLocaleName then
pdebug('Unable to translate: \"' .. text .. '\"')
if not missingTranslations[text] then
pdebug('Unable to translate: \"' .. text .. '\"')
missingTranslations[text] = true
end
end
end
translation = text

View File

@@ -0,0 +1,84 @@
local overlay
local touchStart = 0
local updateCursorEvent = nil
local zoomInButton
local zoomOutButton
-- public functions
function init()
if not g_app.isMobile() then return end
overlay = g_ui.displayUI('mobile')
overlay:raise()
zoomInButton = modules.client_topmenu.addLeftButton('zoomInButton', 'Zoom In', '/images/topbuttons/zoomin', function() g_app.scaleUp() end)
zoomOutButton = modules.client_topmenu.addLeftButton('zoomOutButton', 'Zoom Out', '/images/topbuttons/zoomout', function() g_app.scaleDown() end)
scheduleEvent(function()
g_app.scale(5.0)
end, 10)
connect(overlay, {
onMousePress = onMousePress,
onMouseRelease = onMouseRelease,
onTouchPress = onMousePress,
onTouchRelease = onMouseRelease,
onMouseMove = onMouseMove
})
end
function terminate()
if not g_app.isMobile() then return end
disconnect(overlay, {
onMousePress = onMousePress,
onMouseRelease = onMouseRelease,
onTouchPress = onMousePress,
onTouchRelease = onMouseRelease,
onMouseMove = onMouseMove
})
zoomInButton:destroy()
zoomOutButton:destroy()
overlay:destroy()
overlay = nil
end
function hide()
overlay:hide()
end
function show()
overlay:show()
end
function onMouseMove(widget, pos, offset)
end
function onMousePress(widget, pos, button)
overlay:raise()
if button == 4 then -- touch
overlay:raise()
overlay.cursor:show()
overlay.cursor:setPosition({x=pos.x - 32, y = pos.y - 32})
touchStart = g_clock.millis()
updateCursor()
else
overlay.cursor:hide()
removeEvent(updateCursorEvent)
end
end
function onMouseRelease(widget, pos, button)
overlay.cursor:hide()
removeEvent(updateCursorEvent)
end
function updateCursor()
removeEvent(updateCursorEvent)
local percent = 100 - math.max(0, math.min(100, (g_clock.millis() - touchStart) / 5)) -- 500 ms
overlay.cursor:setPercent(percent)
if percent > 0 then
overlay.cursor:setOpacity(0.5)
updateCursorEvent = scheduleEvent(updateCursor, 10)
else
overlay.cursor:setOpacity(0.8)
end
end

View File

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

View File

@@ -0,0 +1,15 @@
UIWidget
anchors.fill: parent
focusable: false
phantom: true
UIProgressRect
id: cursor
size: 64 64
background: #FF5858
percent: 100
visible: false
x: 0
y: 0
focusable: false
phantom: true

View File

@@ -1,4 +1,4 @@
Panel
OptionPanel
OptionCheckBox
id: enableAudio
!text: tr('Enable audio')
@@ -10,9 +10,6 @@ Panel
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')
@@ -20,9 +17,6 @@ Panel
OptionScrollbar
id: musicSoundVolume
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 100
@@ -30,9 +24,6 @@ Panel
Label
id: botSoundVolumeLabel
!text: tr('Bot sound 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('botSoundVolume')
@@ -40,9 +31,6 @@ Panel
OptionScrollbar
id: botSoundVolume
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 100

View File

@@ -1,4 +1,4 @@
Panel
OptionPanel
OptionCheckBox
id: showInfoMessagesInConsole
!text: tr('Show info messages in console')

View File

@@ -1,8 +1,11 @@
Panel
OptionPanel
OptionCheckBox
id: classicControl
!text: tr('Classic control')
$mobile:
visible: false
OptionCheckBox
id: autoChaseOverride
!text: tr('Allow auto chase override')
@@ -15,6 +18,8 @@ Panel
id: wsadWalking
!text: tr('Enable WSAD walking')
!tooltip: tr('Disable chat and allow walk using WSAD keys')
$mobile:
visible: false
OptionCheckBox
id: dash
@@ -27,9 +32,6 @@ Panel
!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: hotkeyDelayLabel
margin-top: 10
!tooltip: tr('Give you some time to make a turn while walking if you press many keys simultaneously')
@@ -39,114 +41,107 @@ Panel
OptionScrollbar
id: hotkeyDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 5
maximum: 50
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))
$mobile:
visible: false
OptionScrollbar
id: walkFirstStepDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 50
maximum: 300
$mobile:
visible: false
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))
$mobile:
visible: false
OptionScrollbar
id: walkTurnDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
Label
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
id: walkCtrlTurnDelayLabel
margin-top: 10
$mobile:
visible: false
@onSetup: |
local value = modules.client_options.getOption('walkTurnDelay')
self:setText(tr('Walk delay after ctrl turn: %s ms', value))
OptionScrollbar
id: walkCtrlTurnDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
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))
$mobile:
visible: false
OptionScrollbar
id: walkStairsDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
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))
$mobile:
visible: false
OptionScrollbar
id: walkTeleportDelay
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
margin-top: 3
minimum: 0
maximum: 300
$mobile:
visible: false
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
Panel
height: 30
margin-top: 10
Button
id: changeLocale
!text: tr('Change language')
@onClick: modules.client_locales.createWindow()
anchors.left: parent.left
anchors.top: parent.top
width: 150

View File

@@ -1,36 +1,22 @@
Panel
OptionPanel
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
@@ -47,17 +33,11 @@ Panel
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
@@ -70,12 +50,13 @@ Panel
self:addOption("High")
self:addOption("Maximum")
Label
!text: tr('High/Maximum optimization level may cause visual defects.')
margin-top: 5
Label
id: backgroundFrameRateLabel
!text: tr('Game framerate limit: %s', 'max')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 12
@onSetup: |
local value = modules.client_options.getOption('backgroundFrameRate')
@@ -87,18 +68,12 @@ Panel
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')
@@ -106,9 +81,6 @@ Panel
OptionScrollbar
id: ambientLight
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
minimum: 0
maximum: 100
@@ -116,10 +88,9 @@ Panel
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")
!text: tr("If you have FPS issues:\n- Use OpenGL version (_gl)\n- Disable vertical synchronization\n- Set higher optimization level\n- Lower screen resolution\nOr report it on forum: http://otclient.net")
$mobile:
visible: false

View File

@@ -1,20 +1,18 @@
Panel
OptionPanel
Label
width: 130
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
id: layoutLabel
!text: tr("Layout (change requries client restart)")
$mobile:
visible: false
ComboBox
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 3
margin-right: 2
margin-left: 2
$mobile:
visible: false
@onOptionChange: modules.client_options.setOption(self:getId(), self:getCurrentOption().text)
@onSetup: |
self:addOption("Default")
@@ -28,11 +26,17 @@ Panel
id: classicView
!text: tr('Classic view')
margin-top: 5
$mobile:
visible: false
OptionCheckBox
id: cacheMap
!text: tr('Cache map (for non-classic view)')
$mobile:
visible: false
OptionCheckBox
id: actionBar1
!text: tr("Show first action bar")
@@ -57,6 +61,8 @@ Panel
OptionCheckBox
id: displayHealthOnTop
!text: tr('Display creature health bars above texts')
$mobile:
visible: false
OptionCheckBox
id: hidePlayerBars
@@ -65,6 +71,8 @@ Panel
OptionCheckBox
id: displayMana
!text: tr('Show player mana bar')
$mobile:
visible: false
OptionCheckBox
id: topHealtManaBar
@@ -73,93 +81,92 @@ Panel
OptionCheckBox
id: showHealthManaCircle
!text: tr('Show health and mana circle')
$mobile:
visible: false
OptionCheckBox
id: highlightThingsUnderCursor
!text: tr('Highlight things under cursor')
Label
margin-top: 5
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
Panel
height: 40
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")
Label
width: 90
anchors.left: parent.left
anchors.top: parent.top
id: leftPanelsLabel
!text: tr("Left panels")
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")
Label
width: 90
anchors.left: prev.right
anchors.top: prev.top
id: rightPanelsLabel
!text: tr("Right panels")
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
width: 130
anchors.left: prev.right
anchors.top: prev.top
id: backpackPanelLabel
!text: tr("Container's panel")
!tooltip: tr("Open new containers in selected panel")
ComboBox
id: leftPanels
anchors.left: leftPanelsLabel.left
anchors.right: leftPanelsLabel.right
anchors.top: leftPanelsLabel.bottom
margin-top: 3
margin-right: 20
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("0")
self:addOption("1")
self:addOption("2")
self:addOption("3")
self:addOption("4")
ComboBox
id: rightPanels
anchors.left: rightPanelsLabel.left
anchors.right: rightPanelsLabel.right
anchors.top: rightPanelsLabel.bottom
margin-top: 3
margin-right: 20
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("1")
self:addOption("2")
self:addOption("3")
self:addOption("4")
ComboBox
id: containerPanel
anchors.left: backpackPanelLabel.left
anchors.right: backpackPanelLabel.right
anchors.top: backpackPanelLabel.bottom
margin-top: 3
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
@onSetup: |
self:addOption("1st left panel")
self:addOption("2nd left panel")
self:addOption("3rd left panel")
self:addOption("4th left panel")
self:addOption("1st right panel")
self:addOption("2nd right panel")
self:addOption("3rd right panel")
self:addOption("4th right panel")
Label
margin-top: 3
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
@@ -171,9 +178,6 @@ Panel
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')
@@ -181,17 +185,11 @@ Panel
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

@@ -4,9 +4,9 @@ local defaultOptions = {
showFps = true,
showPing = true,
fullscreen = false,
classicView = true,
classicView = not g_app.isMobile(),
cacheMap = false,
classicControl = true,
classicControl = not g_app.isMobile(),
smartWalk = false,
dash = false,
autoChaseOverride = true,
@@ -18,9 +18,9 @@ local defaultOptions = {
showPrivateMessagesInConsole = true,
showPrivateMessagesOnScreen = true,
rightPanels = 1,
leftPanels = 2,
leftPanels = g_app.isMobile() and 1 or 2,
containerPanel = 8,
backgroundFrameRate = 100,
backgroundFrameRate = 60,
enableAudio = true,
enableMusicSound = false,
musicSoundVolume = 100,
@@ -42,10 +42,7 @@ local defaultOptions = {
dontStretchShrink = false,
turnDelay = 30,
hotkeyDelay = 30,
ignoreServerDirection = true,
realDirection = false,
wsadWalking = false,
walkFirstStepDelay = 200,
walkTurnDelay = 100,
@@ -104,20 +101,23 @@ function init()
audioPanel = g_ui.loadUI('audio')
optionsTabBar:addTab(tr('Audio'), audioPanel, '/images/optionstab/audio')
extrasPanel = g_ui.createWidget('Panel')
extrasPanel = g_ui.createWidget('OptionPanel')
for _, v in ipairs(g_extras.getAll()) do
local extrasButton = g_ui.createWidget('OptionCheckBox')
extrasButton:setId(v)
extrasButton:setText(g_extras.getDescription(v))
extrasPanel:addChild(extrasButton)
end
if not g_game.getFeature(GameNoDebug) then
if not g_game.getFeature(GameNoDebug) and not g_app.isMobile() 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)
if g_app.isMobile() then
audioButton:hide()
end
addEvent(function() setup() end)
connect(g_game, { onGameStart = online,
@@ -316,10 +316,6 @@ function setOption(key, value, force)
if modules.game_console and modules.game_console.consoleToggleChat:isChecked() ~= value then
modules.game_console.consoleToggleChat:setChecked(value)
end
--elseif key == 'ignoreServerDirection' then
-- g_game.ignoreServerDirection(value)
--elseif key == 'realDirection' then
-- g_game.showRealDirection(value)
elseif key == 'hotkeyDelay' then
generalPanel:getChildById('hotkeyDelayLabel'):setText(tr('Hotkey delay: %s ms', value))
elseif key == 'walkFirstStepDelay' then

View File

@@ -2,25 +2,23 @@ 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())
OptionPanel < Panel
layout:
type: verticalBox
MainWindow
id: optionsWindow
!text: tr('Options')
size: 480 460
size: 490 500
$mobile:
size: 490 360
@onEnter: modules.client_options.hide()
@onEscape: modules.client_options.hide()

View File

@@ -57,7 +57,7 @@ function terminate()
removeEvent(monitorEvent)
end
function onMiniWindowClose()
function onClose()
statsButton:setOn(false)
end
@@ -67,6 +67,8 @@ function toggle()
statsButton:setOn(false)
else
statsWindow:show()
statsWindow:raise()
statsWindow:focus()
statsButton:setOn(true)
end
end
@@ -173,7 +175,7 @@ function update()
return
end
statsWindow.debugPanel.sleepTime:setText("Sleep: " .. math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2) .. "%, Packets: " .. g_game.getRecivedPacketsCount() .. " , " .. (g_game.getRecivedPacketsSize() / 1024) .. " KB")
statsWindow.debugPanel.sleepTime:setText("GFPS: " .. g_app.getGraphicsFps() .. " PFPS: " .. g_app.getProcessingFps() .. " Packets: " .. g_game.getRecivedPacketsCount() .. " , " .. (g_game.getRecivedPacketsSize() / 1024) .. " KB")
statsWindow.debugPanel.luaRamUsage:setText("Ram usage by lua: " .. gcinfo() .. " kb")
local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo()
adaptiveRender:setText(adaptive)

View File

@@ -24,6 +24,12 @@ MainWindow
margin: 0 0 0 0
padding: 25 3 3 3
opacity: 0.9
@onEnter: modules.client_stats.toggle()
@onEscape: modules.client_stats.toggle()
$mobile:
size: 550 300
ScrollablePanel
id: debugPanel
@@ -42,17 +48,6 @@ MainWindow
id: luaRamUsage
text: -
DebugLabel
!text: tr('Render')
DebugText
id: adaptiveRender
text: -
DebugText
id: render
text: -
DebugText
id: atlas
text: -
@@ -71,6 +66,17 @@ MainWindow
id: mainStats
text: -
DebugLabel
!text: tr('Render')
DebugText
id: adaptiveRender
text: -
DebugText
id: render
text: -
DebugLabel
!text: tr('Dispatcher')

View File

@@ -34,7 +34,7 @@ function init()
loaded_files = {}
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otfont') then
g_ui.importFont('/layouts/' .. layout .. '/fonts/' .. file)
g_fonts.importFont('/layouts/' .. layout .. '/fonts/' .. file)
loaded_files[file] = true
end
end
@@ -47,24 +47,6 @@ function init()
end
end
if layout:len() > 0 then
files = g_resources.listDirectoryFiles('/layouts/' .. layout .. '/particles')
loaded_files = {}
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otps') then
g_ui.importParticle('/layouts/' .. layout .. '/particles/' .. file)
loaded_files[file] = true
end
end
end
files = g_resources.listDirectoryFiles('/data/particles')
for _,file in pairs(files) do
if g_resources.isFileType(file, 'otps') and not loaded_files[file] then
g_particles.importParticle('/data/particles/' .. file)
end
end
g_mouse.loadCursors('/data/cursors/cursors')
if layout:len() > 0 and g_resources.directoryExists('/layouts/' .. layout .. '/cursors/cursors') then
g_mouse.loadCursors('/layouts/' .. layout .. '/cursors/cursors')

View File

@@ -313,6 +313,13 @@ function addLine(text, color)
table.insert(cachedLines, {text=text, color=color})
end
function terminalPrint(value)
if type(value) == "table" then
return print(json.encode(value, 2))
end
print(tostring(value))
end
function executeCommand(command)
if command == nil or #string.gsub(command, '\n', '') == 0 then return end
@@ -337,7 +344,7 @@ function executeCommand(command)
-- detect and convert commands with simple syntax
local realCommand
if string.sub(command, 1, 1) == '=' then
realCommand = 'print(tostring(' .. string.sub(command,2) .. '))'
realCommand = 'modules.client_terminal.terminalPrint(' .. string.sub(command,2) .. ')'
else
realCommand = command
end

View File

@@ -94,6 +94,7 @@ UIWindow
border-width-left: 0
border-width-top: 0
multiline: false
text-auto-submit: true
$on:
border-width-left: 1

View File

@@ -42,7 +42,7 @@ function show(text, options, callback) -- callback = function(newText)
elseif type(text) == 'nil' then
text = ''
elseif type(text) ~= 'string' then
return error("Invalid text type for game_textedit: " .. type(text))
return error("Invalid text type for client_textedit: " .. type(text))
end
if type(options) == 'function' then
local tmp = callback
@@ -113,20 +113,21 @@ function show(text, options, callback) -- callback = function(newText)
window.text:setCursorPos(-1)
end
end
if type(options.range) == 'table' or (type(options.validation) == 'string' and options.validation:len() > 0) then
window.buttons.ok:disable()
window.text.onTextChange = function(widget, text)
if validate(text) then
window.buttons.ok:enable()
else
window.buttons.ok:disable()
end
end
end
window.text:setText(text)
window.text:setCursorPos(-1)
window.text.onTextChange = function(widget, text)
if validate(text) then
window.buttons.ok:enable()
if g_app.isMobile() then
doneFunc()
end
else
window.buttons.ok:disable()
end
end
if type(options.width) == 'number' then
window:setWidth(options.width)
end
@@ -134,6 +135,14 @@ function show(text, options, callback) -- callback = function(newText)
activeWindow = window
activeWindow:raise()
activeWindow:focus()
if g_app.isMobile() then
window.text:focus()
local flags = 0
if options.multiline then
flags = 1
end
g_window.showTextEditor(window:getText(), window.description:getText(), window.text:getText(), flags)
end
return activeWindow
end

View File

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

View File

@@ -27,7 +27,6 @@ TextEditWindow < MainWindow
Label
id: description
text-align: center
text: description
margin-bottom: 5
visible: false
text-wrap: true
@@ -48,10 +47,14 @@ SinglelineTextEditWindow < TextEditWindow
MultilineTextEditWindow < TextEditWindow
width: 600
$mobile:
width: 500
Panel
id: textPanel
height: 400
$mobile:
height: 300
MultilineTextEdit
id: text

View File

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

@@ -1,75 +0,0 @@
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()

176
modules/corelib/base64.lua Normal file
View File

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

View File

@@ -28,6 +28,7 @@ Module
dofile 'outputmessage'
dofile 'orderedtable'
dofile 'base64'
dofile 'json'
dofile 'http'

View File

@@ -108,6 +108,7 @@ function HTTP.cancel(operationId)
if not g_http or not g_http.cancel then
return
end
HTTP.operations[operationId] = nil
return g_http.cancel(operationId)
end

View File

@@ -249,7 +249,7 @@ function table.encodeStringPairList(t)
end
function table.decodeStringPairList(l)
local ret = {}
local ret = {}
local r = regexMatch(l, "(?:^|\\n)([^:^\n]{1,20}):?(.*)(?:$|\\n)")
local multiline = ""
local multilineKey = ""

View File

@@ -46,11 +46,11 @@ function UIPopupMenu:onGeometryChange(oldRect, newRect)
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
local newy = ymax - newRect.height
if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end
end
if newRect.x + newRect.width > xmax then
local newx = newRect.x - newRect.width
local newx = xmax - newRect.width
if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end
end
self:bindRectToParent()

View File

@@ -29,6 +29,9 @@ local function calcValues(self)
end
local px = math.max(proportion * pxrange, 6)
if g_app.isMobile() then
px = math.max(proportion * pxrange, 24)
end
px = px - px % 2 + 1
local offset = 0

View File

@@ -38,7 +38,7 @@ function exit()
end
function quit()
g_app.quit()
g_app.exit()
end
function connect(object, arg1, arg2, arg3)

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ ActionColors = {
}
function init()
local bottomPanel = modules.game_interface.getBottomPanel()
local bottomPanel = modules.game_interface.getActionPanel()
actionPanel1 = g_ui.loadUI('actionbar', bottomPanel)
bottomPanel:moveChildToIndex(actionPanel1, 1)
actionPanel2 = g_ui.loadUI('actionbar', bottomPanel)
@@ -167,6 +167,7 @@ function setupAction(action)
action.text:setText(config.text)
action:setBorderColor(ActionColors.text)
action.item:setOn(true) -- removes background
action.item:setItemId(0)
if Spells then
local spell, profile = Spells.getSpellByWords(config.text:lower())
action.spell = spell
@@ -225,7 +226,7 @@ end
function updateAction(action, newConfig)
local config = action.config
if newConfig.hotkey and type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
if type(config.hotkey) == 'string' and config.hotkey:len() > 0 then
local gameRootPanel = modules.game_interface.getRootPanel()
g_keyboard.unbindKeyPress(config.hotkey, action.callback, gameRootPanel)
end
@@ -236,7 +237,7 @@ function updateAction(action, newConfig)
end
function actionOnMouseRelease(action, mousePosition, mouseButton)
if mouseButton == MouseRightButton then
if mouseButton == MouseRightButton or not action.item:isOn() then
local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
if action.item:getItemId() > 0 then
@@ -256,7 +257,7 @@ function actionOnMouseRelease(action, mousePosition, mouseButton)
end
menu:addSeparator()
menu:addOption(tr('Set text'), function()
modules.game_textedit.singlelineEditor(action.config.text or "", function(newText)
modules.client_textedit.singlelineEditor(action.config.text or "", function(newText)
updateAction(action, {text=newText, item=0})
end)
end)
@@ -362,7 +363,31 @@ function executeAction(action, ticks)
local actionType = action.config.actionType
if type(action.config.text) == 'string' and action.config.text:len() > 0 then
modules.game_console.sendMessage(action.config.text)
if g_app.isMobile() then -- turn to direction of targer
local target = g_game.getAttackingCreature()
if target then
local pos = g_game.getLocalPlayer():getPosition()
local tpos = target:getPosition()
if pos and tpos then
local offx = tpos.x - pos.x
local offy = tpos.y - pos.y
if offy < 0 and offx <= 0 and math.abs(offx) < math.abs(offy) then
g_game.turn(Directions.North)
elseif offy > 0 and offx >= 0 and math.abs(offx) < math.abs(offy) then
g_game.turn(Directions.South)
elseif offx < 0 and offy <= 0 and math.abs(offx) > math.abs(offy) then
g_game.turn(Directions.West)
elseif offx > 0 and offy >= 0 and math.abs(offx) > math.abs(offy) then
g_game.turn(Directions.East)
end
end
end
end
if modules.game_interface.isChatVisible() then
modules.game_console.sendMessage(action.config.text)
else
g_game.talk(action.config.text)
end
action.actionDelayTo = g_clock.millis() + actionDelay
elseif action.item:getItemId() > 0 then
if actionType == ActionTypes.USE then

View File

@@ -55,17 +55,11 @@ ActionButton < Panel
Panel
id: actionBar
anchors.left: parent.left
anchors.right: parent.right
image-source: /images/ui/panel_map
focusable: false
image-source: /images/ui/panel_side
image-border: 4
margin-top: -1
$first:
anchors.top: parent.top
$!first:
anchors.top: prev.bottom
$on:
height: 40
visible: true
@@ -78,9 +72,12 @@ Panel
id: prevButton
icon: /images/game/console/leftarrow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-left: 1
margin-top: 1
margin-bottom: 2
Panel
id: tabBar
anchors.top: parent.top
@@ -89,15 +86,17 @@ Panel
anchors.right: next.left
margin-right: 3
margin-top: 2
clipping: true
clipping: true
TabButton
id: nextButton
icon: /images/game/console/rightarrow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.top: parent.top
anchors.bottom: parent.bottom
margin-right: 1
margin-top: 1
margin-bottom: 2
ActionAssignWindow < MainWindow

View File

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

View File

@@ -1,70 +0,0 @@
TargetBot.Creature.attack = function(params, targets, isLooting) -- params {config, creature, danger, priority}
if player:isWalking() then
lastWalk = now
end
local config = params.config
local creature = params.creature
if g_game.getAttackingCreature() ~= creature then
g_game.attack(creature)
end
if not isLooting then -- walk only when not looting
TargetBot.Creature.walk(creature, config, targets)
end
-- attacks
local mana = player:getMana()
if config.useGroupAttack and config.groupAttackSpell:len() > 1 and mana > config.minManaGroup then
local creatures = g_map.getSpectatorsInRange(player:getPosition(), false, config.groupAttackRadius, config.groupAttackRadius)
local playersAround = false
local monsters = 0
for _, creature in ipairs(creatures) do
if not creature:isLocalPlayer() and creature:isPlayer() then
playersAround = true
elseif creature:isMonster() then
monsters = monsters + 1
end
end
if monsters >= config.groupAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then
if TargetBot.sayAttackSpell(config.groupAttackSpell, config.groupAttackDelay) then
return
end
end
end
if config.useSpellAttack and config.attackSpell:len() > 1 and mana > config.minMana then
if TargetBot.sayAttackSpell(config.attackSpell, config.attackSpellDelay) then
return
end
end
if config.useRuneAttack and config.attackRune > 100 then
if TargetBot.useAttackItem(config.attackRune, 0, creature, config.attackRuneDelay) then
return
end
end
end
TargetBot.Creature.walk = function(creature, config, targets)
-- luring
if config.lure and not (config.chase and creature:getHealthPercent() < 30) then
local monsters = 0
if targets < config.lureCount then
local path = findPath(player:getPosition(), creature:getPosition(), 5, {ignoreNonPathable=true, precision=2})
if path then
return TargetBot.walkTo(creature:getPosition(), 10, {marginMin=5, marginMax=6, ignoreNonPathable=true})
end
end
end
local currentDistance = findPath(player:getPosition(), creature:getPosition(), 10, {ignoreCreatures=true, ignoreNonPathable=true, ignoreCost=true})
if config.chase and (creature:getHealthPercent() < 30 or not config.keepDistance) then
if #currentDistance > 1 then
return TargetBot.walkTo(creature:getPosition(), 10, {ignoreNonPathable=true, precision=1})
end
elseif config.keepDistance then
if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then
return TargetBot.walkTo(creature:getPosition(), 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1})
end
end
end

View File

@@ -21,7 +21,7 @@ end
local actionRetries = 0
local prevActionResult = true
cavebotMacro = macro(20, function()
if TargetBot and TargetBot.isActive() then
if TargetBot and TargetBot.isActive() and not TargetBot.isCaveBotActionAllowed() then
return -- target bot or looting is working, wait
end

View File

@@ -1,5 +1,5 @@
-- main tab
VERSION = "1.1"
VERSION = "1.2"
UI.Label("Config version: " .. VERSION)
@@ -14,7 +14,7 @@ UI.Button("Discord", function()
end)
UI.Button("Forum", function()
g_platform.openUrl("https://otland.net/forums/otclient.494/")
g_platform.openUrl("https://otclient.net/")
end)
UI.Button("Help & Tutorials", function()

View File

@@ -0,0 +1,112 @@
TargetBot.Creature.attack = function(params, targets, isLooting) -- params {config, creature, danger, priority}
if player:isWalking() then
lastWalk = now
end
local config = params.config
local creature = params.creature
if g_game.getAttackingCreature() ~= creature then
g_game.attack(creature)
end
if not isLooting then -- walk only when not looting
TargetBot.Creature.walk(creature, config, targets)
end
-- attacks
local mana = player:getMana()
if config.useGroupAttack and config.groupAttackSpell:len() > 1 and mana > config.minManaGroup then
local creatures = g_map.getSpectatorsInRange(player:getPosition(), false, config.groupAttackRadius, config.groupAttackRadius)
local playersAround = false
local monsters = 0
for _, creature in ipairs(creatures) do
if not creature:isLocalPlayer() and creature:isPlayer() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) then
playersAround = true
elseif creature:isMonster() then
monsters = monsters + 1
end
end
if monsters >= config.groupAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then
if TargetBot.sayAttackSpell(config.groupAttackSpell, config.groupAttackDelay) then
return
end
end
end
if config.useGroupAttackRune and config.groupAttackRune > 100 then
local creatures = g_map.getSpectatorsInRange(creature:getPosition(), false, config.groupRuneAttackRadius, config.groupRuneAttackRadius)
local playersAround = false
local monsters = 0
for _, creature in ipairs(creatures) do
if not creature:isLocalPlayer() and creature:isPlayer() and (not config.groupAttackIgnoreParty or creature:getShield() <= 2) then
playersAround = true
elseif creature:isMonster() then
monsters = monsters + 1
end
end
if monsters >= config.groupRuneAttackTargets and (not playersAround or config.groupAttackIgnorePlayers) then
if TargetBot.useAttackItem(config.groupAttackRune, 0, creature, config.groupRuneAttackDelay) then
return
end
end
end
if config.useSpellAttack and config.attackSpell:len() > 1 and mana > config.minMana then
if TargetBot.sayAttackSpell(config.attackSpell, config.attackSpellDelay) then
return
end
end
if config.useRuneAttack and config.attackRune > 100 then
if TargetBot.useAttackItem(config.attackRune, 0, creature, config.attackRuneDelay) then
return
end
end
end
TargetBot.Creature.walk = function(creature, config, targets)
local cpos = creature:getPosition()
local pos = player:getPosition()
-- luring
if (config.lure or config.lureCavebot) and not (config.chase and creature:getHealthPercent() < 30) then
local monsters = 0
if targets < config.lureCount then
if config.lureCavebot then
return TargetBot.allowCaveBot(200)
else
local path = findPath(pos, cpos, 5, {ignoreNonPathable=true, precision=2})
if path then
return TargetBot.walkTo(cpos, 10, {marginMin=5, marginMax=6, ignoreNonPathable=true})
end
end
end
end
local currentDistance = findPath(pos, cpos, 10, {ignoreCreatures=true, ignoreNonPathable=true, ignoreCost=true})
if config.chase and (creature:getHealthPercent() < 30 or not config.keepDistance) then
if #currentDistance > 1 then
return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, precision=1})
end
elseif config.keepDistance then
if #currentDistance ~= config.keepDistanceRange and #currentDistance ~= config.keepDistanceRange + 1 then
return TargetBot.walkTo(cpos, 10, {ignoreNonPathable=true, marginMin=config.keepDistanceRange, marginMax=config.keepDistanceRange + 1})
end
end
if config.avoidAttacks then
local diffx = cpos.x - pos.x
local diffy = cpos.y - pos.y
local candidates = {}
if math.abs(diffx) == 1 and diffy == 0 then
candidates = {{x=pos.x, y=pos.y-1, z=pos.z}, {x=pos.x, y=pos.y+1, z=pos.z}}
elseif diffx == 0 and math.abs(diffy) == 1 then
candidates = {{x=pos.x-1, y=pos.y, z=pos.z}, {x=pos.x+1, y=pos.y, z=pos.z}}
end
for _, candidate in ipairs(candidates) do
local tile = g_map.getTile(candidate)
if tile and tile:isWalkable() then
return TargetBot.walkTo(candidate, 2, {ignoreNonPathable=true})
end
end
end
end

View File

@@ -82,18 +82,24 @@ TargetBot.Creature.edit = function(config, callback) -- callback = function(newC
addScrollBar("groupAttackRadius", "Radius of group attack spell", 1, 7, 1)
addScrollBar("groupAttackDelay", "Group attack spell delay", 200, 60000, 5000)
addScrollBar("runeAttackDelay", "Rune attack delay", 200, 5000, 2000)
addScrollBar("groupRuneAttackTargets", "Min. targets for group rune attack", 1, 10, 2)
addScrollBar("groupRuneAttackRadius", "Radius of group rune attack", 1, 7, 1)
addScrollBar("groupRuneAttackDelay", "Group rune attack delay", 200, 60000, 5000)
addCheckBox("chase", "Chase", true)
addCheckBox("keepDistance", "Keep Distance", false)
addCheckBox("lure", "Lure", false)
-- addCheckBox("avoidAttacks", "Avoid attacks", true)
addCheckBox("lureCavebot", "Lure using cavebot", false)
addCheckBox("avoidAttacks", "Avoid wave attacks", false)
addCheckBox("useSpellAttack", "Use attack spell", false)
addTextEdit("attackSpell", "Attack spell", "")
addCheckBox("useGroupAttack", "Use group attack spell", false)
addCheckBox("groupAttackIgnorePlayers", "Ignore players in group attack", false)
addTextEdit("groupAttackSpell", "Group attack spell", "")
addCheckBox("useRuneAttack", "Use attack rune", false)
addItem("attackRune", "Attack rune:", 0)
addCheckBox("useGroupAttack", "Use group attack spell", false)
addTextEdit("groupAttackSpell", "Group attack spell", "")
addCheckBox("useGroupAttackRune", "Use group attack rune", false)
addItem("groupAttackRune", "Group attack rune:", 0)
addCheckBox("groupAttackIgnorePlayers", "Ignore players in group attack", false)
addCheckBox("groupAttackIgnoreParty", "Ignore party in group attack", false)
end

View File

@@ -1,6 +1,6 @@
TargetBotCreatureEditorScrollBar < Panel
height: 35
margin-top: 7
height: 28
margin-top: 3
Label
id: text
@@ -14,7 +14,7 @@ TargetBotCreatureEditorScrollBar < Panel
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
minimum: 0
maximum: 10
step: 1
@@ -64,7 +64,7 @@ TargetBotCreatureEditorCheckBox < BotSwitch
TargetBotCreatureEditorWindow < MainWindow
text: TargetBot creature editor
width: 500
height: 650
height: 600
Label
anchors.left: parent.left

View File

@@ -145,14 +145,14 @@ TargetBot.Looting.process = function(targets, dangerLevel)
end
local container = tile:getTopUseThing()
if not container or container:getId() ~= loot.container then
if not container or not container:isContainer() then
table.remove(TargetBot.Looting.list, 1)
return true
end
g_game.open(container)
waitTill = now + 1000 -- give it 1s to open
waitingForContainer = loot.container
waitingForContainer = container:getId()
loot.tries = loot.tries + 10
return true
@@ -213,9 +213,9 @@ TargetBot.Looting.lootContainer = function(lootContainers, container)
-- loot items
local nextContainer = nil
for i, item in ipairs(container:getItems()) do
if item:isContainer() then
if item:isContainer() and not itemsById[item:getId()] then
nextContainer = item
elseif itemsById[item:getId()] or ui.everyItem:isOn() then
elseif itemsById[item:getId()] or (ui.everyItem:isOn() and not item:isContainer()) then
item.lootTries = (item.lootTries or 0) + 1
if item.lootTries < 5 then -- if can't be looted within 0.5s then skip it
return TargetBot.Looting.lootItem(lootContainers, item)

View File

@@ -1,6 +1,7 @@
local targetbotMacro = nil
local config = nil
local lastAction = 0
local cavebotAllowance = 0
-- ui
local configWidget = UI.Config()
@@ -72,6 +73,8 @@ targetbotMacro = macro(100, function()
TargetBot.Creature.attack(highestPriorityParams, targets, looting)
if lootingStatus:len() > 0 then
TargetBot.setStatus("Attack & " .. lootingStatus)
elseif cavebotAllowance > now then
TargetBot.setStatus("Luring using CaveBot")
else
TargetBot.setStatus("Attacking")
end
@@ -148,6 +151,10 @@ TargetBot.isActive = function() -- return true if attacking or looting takes pla
return lastAction + 300 > now
end
TargetBot.isCaveBotActionAllowed = function()
return cavebotAllowance > now
end
TargetBot.setStatus = function(text)
return ui.status.right:setText(text)
end
@@ -187,6 +194,10 @@ TargetBot.save = function()
config.save(data)
end
TargetBot.allowCaveBot = function(time)
cavebotAllowance = now + time
end
-- attacks
local lastSpell = 0
local lastAttackSpell = 0

View File

@@ -72,6 +72,42 @@ macro(1000, "Stack items", function()
end
end)
macro(10000, "Anti Kick", function()
local dir = player:getDirection()
turn((dir + 1) % 4)
turn(dir)
end)
UI.Separator()
UI.Label("Drop items:")
if type(storage.dropItems) ~= "table" then
storage.dropItems = {283, 284, 285}
end
local foodContainer = UI.Container(function(widget, items)
storage.dropItems = items
end, true)
foodContainer:setHeight(35)
foodContainer:setItems(storage.dropItems)
macro(5000, "drop items", function()
if not storage.dropItems[1] then return end
if TargetBot and TargetBot.isActive() then return end -- pause when attacking
for _, container in pairs(g_game.getContainers()) do
for __, item in ipairs(container:getItems()) do
for i, dropItem in ipairs(storage.dropItems) do
if item:getId() == dropItem.id then
if item:isStackable() then
return g_game.move(item, player:getPosition(), item:getCount())
else
return g_game.move(item, player:getPosition(), dropItem.count) -- count is also subtype
end
end
end
end
end
end)
UI.Separator()
UI.Label("Mana training")
@@ -80,6 +116,7 @@ if type(storage.manaTrain) ~= "table" then
end
local manatrainmacro = macro(1000, function()
if TargetBot and TargetBot.isActive() then return end -- pause when attacking
local mana = math.min(100, math.floor(100 * (player:getMana() / player:getMaxMana())))
if storage.manaTrain.max >= mana and mana >= storage.manaTrain.min then
say(storage.manaTrain.text)

View File

@@ -81,6 +81,7 @@ function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, relo
context.gcinfo = gcinfo
context.tr = tr
context.json = json
context.base64 = base64
context.regexMatch = regexMatch
context.getDistanceBetween = function(p1, p2)
return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y))

View File

@@ -1,5 +1,7 @@
local context = G.botContext
context.getMapView = function() return modules.game_interface.getMapPanel() end
context.getMapPanel = context.getMapView
context.zoomIn = function() modules.game_interface.getMapPanel():zoomIn() end
context.zoomOut = function() modules.game_interface.getMapPanel():zoomOut() end

View File

@@ -80,7 +80,7 @@ UI.Container = function(callback, unique, parent, widget)
for i, child in ipairs(widget.items:getChildren()) do
if child:getItemId() >= 100 then
if not duplicates[child:getItemId()] or not unique then
table.insert(items, {id=child:getItemId(), count=child:getItemCount()})
table.insert(items, {id=child:getItemId(), count=child:getItemCountOrSubType()})
duplicates[child:getItemId()] = true
end
end

View File

@@ -14,7 +14,7 @@ UI.EditorWindow = function(text, options, callback)
validation = text (regex)
examples = {{name, text}, {name, text}}
]]--
local window = modules.game_textedit.edit(text, options, callback)
local window = modules.client_textedit.edit(text, options, callback)
window.botWidget = true
return window
end

View File

@@ -679,7 +679,7 @@ Panel
refreshConfig()
end
ui.add.onClick = function()
modules.game_textedit.multilineEditor("Target list editor", "name:Config name", function(newText)
modules.client_textedit.multilineEditor("Target list editor", "name:Config name", function(newText)
table.insert(context.storage.attacking.configs, newText)
context.storage.attacking.activeConfig = #context.storage.attacking.configs
refreshConfig()
@@ -689,7 +689,7 @@ Panel
if not context.storage.attacking.activeConfig or not context.storage.attacking.configs[context.storage.attacking.activeConfig] then
return
end
modules.game_textedit.multilineEditor("Target list editor", context.storage.attacking.configs[context.storage.attacking.activeConfig], function(newText)
modules.client_textedit.multilineEditor("Target list editor", context.storage.attacking.configs[context.storage.attacking.activeConfig], function(newText)
context.storage.attacking.configs[context.storage.attacking.activeConfig] = newText
refreshConfig()
end)

View File

@@ -266,7 +266,7 @@ Panel
refreshConfig()
end
ui.add.onClick = function()
modules.game_textedit.multilineEditor("Looting editor", "name:Config name", function(newText)
modules.client_textedit.multilineEditor("Looting editor", "name:Config name", function(newText)
table.insert(context.storage.looting.configs, newText)
context.storage.looting.activeConfig = #context.storage.looting.configs
refreshConfig()
@@ -276,7 +276,7 @@ Panel
if not context.storage.looting.activeConfig or not context.storage.looting.configs[context.storage.looting.activeConfig] then
return
end
modules.game_textedit.multilineEditor("Looting editor", context.storage.looting.configs[context.storage.looting.activeConfig], function(newText)
modules.client_textedit.multilineEditor("Looting editor", context.storage.looting.configs[context.storage.looting.activeConfig], function(newText)
context.storage.looting.configs[context.storage.looting.activeConfig] = newText
refreshConfig()
end)

View File

@@ -312,7 +312,7 @@ Panel
end
end
ui.add.onClick = function()
modules.game_textedit.multilineEditor("Waypoints editor", "name:Config name\nlabel:start\n", function(newText)
modules.client_textedit.multilineEditor("Waypoints editor", "name:Config name\nlabel:start\n", function(newText)
table.insert(context.storage.cavebot.configs, newText)
context.storage.cavebot.activeConfig = #context.storage.cavebot.configs
refreshConfig()
@@ -322,7 +322,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.multilineEditor("Waypoints editor", context.storage.cavebot.configs[context.storage.cavebot.activeConfig], function(newText)
modules.client_textedit.multilineEditor("Waypoints editor", context.storage.cavebot.configs[context.storage.cavebot.activeConfig], function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = newText
refreshConfig()
end)
@@ -428,7 +428,7 @@ Panel
return
end
local pos = context.player:getPosition()
modules.game_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
modules.client_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\ngoto:" .. newText
refreshConfig(true)
end)
@@ -439,7 +439,7 @@ Panel
return
end
local pos = context.player:getPosition()
modules.game_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
modules.client_textedit.singlelineEditor("" .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nuse:" .. newText
refreshConfig(true)
end)
@@ -450,7 +450,7 @@ Panel
return
end
local pos = context.player:getPosition()
modules.game_textedit.singlelineEditor("ITEMID," .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
modules.client_textedit.singlelineEditor("ITEMID," .. pos.x .. "," .. pos.y .. "," .. pos.z, function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nusewith:" .. newText
refreshConfig(true)
end)
@@ -460,7 +460,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.singlelineEditor("1000", function(newText)
modules.client_textedit.singlelineEditor("1000", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nwait:" .. newText
refreshConfig(true)
end)
@@ -470,7 +470,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.singlelineEditor("text", function(newText)
modules.client_textedit.singlelineEditor("text", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nsay:" .. newText
refreshConfig(true)
end)
@@ -480,7 +480,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.singlelineEditor("text", function(newText)
modules.client_textedit.singlelineEditor("text", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nnpc:" .. newText
refreshConfig(true)
end)
@@ -490,7 +490,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.singlelineEditor("label name", function(newText)
modules.client_textedit.singlelineEditor("label name", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nlabel:" .. newText
refreshConfig(true)
end)
@@ -500,7 +500,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.singlelineEditor("creature name", function(newText)
modules.client_textedit.singlelineEditor("creature name", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nfollow:" .. newText
refreshConfig(true)
end)
@@ -510,7 +510,7 @@ Panel
if not context.storage.cavebot.activeConfig or not context.storage.cavebot.configs[context.storage.cavebot.activeConfig] then
return
end
modules.game_textedit.multilineEditor("Add function", "function(waypoints)\n -- your lua code, function is executed if previous goto was successful or is just after label\n\n -- must return true to execute next command, otherwise will run in loop till correct return\n return true\nend", function(newText)
modules.client_textedit.multilineEditor("Add function", "function(waypoints)\n -- your lua code, function is executed if previous goto was successful or is just after label\n\n -- must return true to execute next command, otherwise will run in loop till correct return\n return true\nend", function(newText)
context.storage.cavebot.configs[context.storage.cavebot.activeConfig] = context.storage.cavebot.configs[context.storage.cavebot.activeConfig] .. "\nfunction:" .. newText
refreshConfig(true)
end)

View File

@@ -29,7 +29,7 @@ BotItem < Item
&editable: true
BotTextEdit < TextEdit
@onClick: modules.game_textedit.show(self)
@onClick: modules.client_textedit.show(self)
text-align: center
multiline: false
focusable: false
@@ -52,6 +52,8 @@ BotPanel < Panel
vertical-scrollbar: botPanelScroll
layout:
type: verticalBox
$mobile:
margin-right: 16
BotSmallScrollBar
id: botPanelScroll

View File

@@ -22,7 +22,6 @@ BotIcon < UIWidget
margin-top: 0
size: 48 48
phantom: true
optimized: true
UIWidget
id: status

View File

@@ -194,6 +194,7 @@ function toggleChat()
end
function enableChat(temporarily)
if g_app.isMobile() then return end
if consoleToggleChat:isChecked() then
return consoleToggleChat:setChecked(false)
end
@@ -225,6 +226,7 @@ function enableChat(temporarily)
end
function disableChat(temporarily)
if g_app.isMobile() then return end
if not consoleToggleChat:isChecked() then
return consoleToggleChat:setChecked(true)
end
@@ -591,6 +593,29 @@ function getHighlightedText(text)
return tmpData
end
function getNewHighlightedText(text, color, highlightColor)
local tmpData = {}
for i, part in ipairs(text:split("{")) do
if i == 1 then
table.insert(tmpData, part)
table.insert(tmpData, color)
else
for j, part2 in ipairs(part:split("}")) do
if j == 1 then
table.insert(tmpData, part2)
table.insert(tmpData, highlightColor)
else
table.insert(tmpData, part2)
table.insert(tmpData, color)
end
end
end
end
return tmpData
end
function addTabText(text, speaktype, tab, creatureName)
if not tab or tab.locked or not text or #text == 0 then return end
@@ -1113,16 +1138,16 @@ function onTalk(name, level, mode, message, channelId, creaturePos)
-- Remove curly braces from screen message
local staticMessage = message
if isNpcMode then
local highlightData = getHighlightedText(staticMessage)
if #highlightData > 0 then
for i = 1, #highlightData / 3 do
local dataBlock = { _start = highlightData[(i-1)*3+1], _end = highlightData[(i-1)*3+2], words = highlightData[(i-1)*3+3] }
staticMessage = staticMessage:gsub("{"..dataBlock.words.."}", dataBlock.words)
end
local highlightData = getNewHighlightedText(staticMessage, speaktype.color, "#1f9ffe")
if #highlightData > 2 then
staticText:addColoredMessage(name, mode, highlightData)
else
staticText:addMessage(name, mode, staticMessage)
end
staticText:setColor(speaktype.color)
else
staticText:addMessage(name, mode, staticMessage)
end
staticText:addMessage(name, mode, staticMessage)
g_map.addThing(staticText, creaturePos, -1)
end

View File

@@ -50,7 +50,11 @@ function init()
if not healthInfoWindow.forceOpen then
healthInfoButton = modules.client_topmenu.addRightGameToggleButton('healthInfoButton', tr('Health Information'), '/images/topbuttons/healthinfo', toggle)
healthInfoButton:setOn(true)
if g_app.isMobile() then
healthInfoButton:hide()
else
healthInfoButton:setOn(true)
end
end
healthBar = healthInfoWindow:recursiveGetChildById('healthBar')
@@ -89,6 +93,11 @@ function init()
hideExperience()
healthInfoWindow:setup()
if g_app.isMobile() then
healthInfoWindow:close()
healthInfoButton:setOn(false)
end
end
function terminate()
@@ -166,8 +175,9 @@ function onHealthChange(localPlayer, health, maxHealth)
local healthPercent = math.floor(g_game.getLocalPlayer():getHealthPercent())
local Yhppc = math.floor(208 * (1 - (healthPercent / 100)))
local rect = { x = 0, y = Yhppc, width = 63, height = 208 }
local rect = { x = 0, y = Yhppc, width = 63, height = 208 - Yhppc + 1 }
healthCircleFront:setImageClip(rect)
healthCircleFront:setImageRect(rect)
if healthPercent > 92 then
healthCircleFront:setImageColor("#00BC00FF")
@@ -182,14 +192,13 @@ function onHealthChange(localPlayer, health, maxHealth)
else
healthCircleFront:setImageColor("#850C0CFF")
end
healthCircleFront:setMarginTop(Yhppc)
end
function onManaChange(localPlayer, mana, maxMana)
if mana > maxMana then
maxMana = mana
end
manaBar:setText(comma_value(mana) .. ' / ' .. comma_value(maxMana))
manaBar:setTooltip(tr(manaTooltip, mana, maxMana))
manaBar:setValue(mana, 0, maxMana)
@@ -198,10 +207,10 @@ function onManaChange(localPlayer, mana, maxMana)
topManaBar:setTooltip(tr(manaTooltip, mana, maxMana))
topManaBar:setValue(mana, 0, maxMana)
local Ymppc = math.floor(208 * (1 - (math.floor((g_game.getLocalPlayer():getMaxMana() - (g_game.getLocalPlayer():getMaxMana() - g_game.getLocalPlayer():getMana())) * 100 / g_game.getLocalPlayer():getMaxMana()) / 100)))
local rect = { x = 0, y = Ymppc, width = 63, height = 208 }
local Ymppc = math.floor(208 * (1 - (math.floor((maxMana - (maxMana - mana)) * 100 / maxMana) / 100)))
local rect = { x = 0, y = Ymppc, width = 63, height = 208 - Ymppc + 1 }
manaCircleFront:setImageClip(rect)
manaCircleFront:setMarginTop(Ymppc)
manaCircleFront:setImageRect(rect)
end
function onLevelChange(localPlayer, value, percent)
@@ -276,8 +285,18 @@ function setExperienceTooltip(tooltip)
end
function onOverlayGeometryChange()
if g_app.isMobile() then
topHealthBar:setMarginTop(35)
topManaBar:setMarginTop(35)
local width = overlay:getWidth()
local margin = width / 3 + 10
topHealthBar:setMarginLeft(margin)
topManaBar:setMarginRight(margin)
return
end
local classic = g_settings.getBoolean("classicView")
local minMargin = 100
local minMargin = 40
if classic then
topHealthBar:setMarginTop(15)
topManaBar:setMarginTop(15)

View File

@@ -51,9 +51,9 @@ function setupExtraHotkeys(combobox)
end
local battlePanel = modules.game_battle.battlePanel
local attackedCreature = g_game.getAttackingCreature()
local prevChild = battlePanel:getLastChild()
local prevChild = nil
for i, child in ipairs(battlePanel:getChildren()) do
if not child.creature or child:isDisabled() then
if not child.creature or child:isDisabled() or child:isHidden() then
break
end
if child.creature == attackedCreature then

View File

@@ -43,7 +43,9 @@ configValueChanged = false
-- public functions
function init()
hotkeysButton = modules.client_topmenu.addLeftGameButton('hotkeysButton', tr('Hotkeys') .. ' (Ctrl+K)', '/images/topbuttons/hotkeys', toggle, false, 7)
if not g_app.isMobile() then
hotkeysButton = modules.client_topmenu.addLeftGameButton('hotkeysButton', tr('Hotkeys') .. ' (Ctrl+K)', '/images/topbuttons/hotkeys', toggle, false, 7)
end
g_keyboard.bindKeyDown('Ctrl+K', toggle)
hotkeysWindow = g_ui.displayUI('hotkeys_manager')
hotkeysWindow:setVisible(false)
@@ -104,7 +106,9 @@ function terminate()
unload()
hotkeysWindow:destroy()
hotkeysButton:destroy()
if hotkeysButton then
hotkeysButton:destroy()
end
mouseGrabberWidget:destroy()
end

View File

@@ -3,6 +3,8 @@ gameMapPanel = nil
gameRightPanels = nil
gameLeftPanels = nil
gameBottomPanel = nil
gameActionPanel = nil
gameLeftActions = nil
logoutButton = nil
mouseGrabberWidget = nil
countWindow = nil
@@ -44,6 +46,8 @@ function init()
gameRightPanels = gameRootPanel:getChildById('gameRightPanels')
gameLeftPanels = gameRootPanel:getChildById('gameLeftPanels')
gameBottomPanel = gameRootPanel:getChildById('gameBottomPanel')
gameActionPanel = gameRootPanel:getChildById('gameActionPanel')
gameLeftActions = gameRootPanel:getChildById('gameLeftActions')
connect(gameLeftPanel, { onVisibilityChange = onLeftPanelVisibilityChange })
logoutButton = modules.client_topmenu.addLeftButton('logoutButton', tr('Exit'),
@@ -52,6 +56,7 @@ function init()
gameRightPanels:addChild(g_ui.createWidget('GameSidePanel'))
setupLeftActions()
refreshViewMode()
bindKeys()
@@ -389,12 +394,12 @@ function createThingMenu(menuPosition, lookThing, useThing, creatureThing)
local classic = modules.client_options.getOption('classicControl')
local shortcut = nil
if not classic then shortcut = '(Shift)' else shortcut = nil end
if not classic and not g_app.isMobile() then shortcut = '(Shift)' else shortcut = nil end
if lookThing then
menu:addOption(tr('Look'), function() g_game.look(lookThing) end, shortcut)
end
if not classic then shortcut = '(Ctrl)' else shortcut = nil end
if not classic and not g_app.isMobile() then shortcut = '(Ctrl)' else shortcut = nil end
if useThing then
if useThing:isContainer() then
if useThing:getParentContainer() then
@@ -470,7 +475,7 @@ function createThingMenu(menuPosition, lookThing, useThing, creatureThing)
else
local localPosition = localPlayer:getPosition()
if not classic then shortcut = '(Alt)' else shortcut = nil end
if not classic and not g_app.isMobile() then shortcut = '(Alt)' else shortcut = nil end
if creatureThing:getPosition().z == localPosition.z then
if g_game.getAttackingCreature() ~= creatureThing then
menu:addOption(tr('Attack'), function() g_game.attack(creatureThing) end, shortcut)
@@ -565,7 +570,68 @@ end
function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, useThing, creatureThing, attackCreature, marking)
local keyboardModifiers = g_keyboard.getModifiers()
if not modules.client_options.getOption('classicControl') then
if g_app.isMobile() then
if mouseButton == MouseRightButton then
createThingMenu(menuPosition, lookThing, useThing, creatureThing)
return true
end
if mouseButton ~= MouseLeftButton then
return false
end
local action = getLeftAction()
if action == "look" then
if lookThing then
resetLeftActions()
g_game.look(lookThing)
return true
end
return true
elseif action == "use" then
if useThing then
resetLeftActions()
if useThing:isContainer() then
if useThing:getParentContainer() then
g_game.open(useThing, useThing:getParentContainer())
else
g_game.open(useThing)
end
return true
elseif useThing:isMultiUse() then
startUseWith(useThing)
return true
else
g_game.use(useThing)
return true
end
end
return true
elseif action == "attack" then
if attackCreature and attackCreature ~= player then
resetLeftActions()
g_game.attack(attackCreature)
return true
elseif creatureThing and creatureThing ~= player and creatureThing:getPosition().z == autoWalkPos.z then
resetLeftActions()
g_game.attack(creatureThing)
return true
end
return true
elseif action == "follow" then
if attackCreature and attackCreature ~= player then
resetLeftActions()
g_game.follow(attackCreature)
return true
elseif creatureThing and creatureThing ~= player and creatureThing:getPosition().z == autoWalkPos.z then
resetLeftActions()
g_game.follow(creatureThing)
return true
end
return true
elseif not autoWalkPos and useThing then
createThingMenu(menuPosition, lookThing, useThing, creatureThing)
return true
end
elseif not modules.client_options.getOption('classicControl') then
if keyboardModifiers == KeyboardNoModifier and mouseButton == MouseRightButton then
createThingMenu(menuPosition, lookThing, useThing, creatureThing)
return true
@@ -595,9 +661,7 @@ function processMouseAction(menuPosition, mouseButton, autoWalkPos, lookThing, u
g_game.attack(creatureThing)
return true
end
-- classic control
else
else -- classic control
if useThing and keyboardModifiers == KeyboardNoModifier and mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then
local player = g_game.getLocalPlayer()
if attackCreature and attackCreature ~= player then
@@ -810,8 +874,12 @@ function getBottomPanel()
return gameBottomPanel
end
function getActionPanel()
return gameActionPanel
end
function refreshViewMode()
local classic = g_settings.getBoolean("classicView")
local classic = g_settings.getBoolean("classicView") and not g_app.isMobile()
local rightPanels = g_settings.getNumber("rightPanels") - gameRightPanels:getChildCount()
local leftPanels = g_settings.getNumber("leftPanels") - 1 - gameLeftPanels:getChildCount()
@@ -838,9 +906,9 @@ function refreshViewMode()
return
end
local minimumWidth = (g_settings.getNumber("rightPanels") + g_settings.getNumber("leftPanels") - 1) * 200 + 300
minimumWidth = math.max(minimumWidth, 800)
g_window.setMinimumSize({ width = minimumWidth, height = 600 })
local minimumWidth = (g_settings.getNumber("rightPanels") + g_settings.getNumber("leftPanels") - 1) * 200 + 200
minimumWidth = math.max(minimumWidth, g_resources.getLayout() == "mobile" and 640 or 800)
g_window.setMinimumSize({ width = minimumWidth, height = (g_resources.getLayout() == "mobile" and 360 or 600)})
if g_window.getWidth() < minimumWidth then
local oldPos = g_window.getPosition()
local size = { width = minimumWidth, height = g_window.getHeight() }
@@ -868,30 +936,21 @@ function refreshViewMode()
gameMapPanel:setMarginLeft(0)
gameMapPanel:setMarginRight(0)
gameMapPanel:setMarginTop(0)
else
gameLeftPanels:setMarginTop(modules.client_topmenu.getTopMenu():getHeight() - gameLeftPanels:getPaddingTop())
gameRightPanels:setMarginTop(modules.client_topmenu.getTopMenu():getHeight() - gameRightPanels:getPaddingTop())
end
gameMapPanel:setVisibleDimension({ width = 15, height = 11 })
if classic then
g_game.changeMapAwareRange(19, 15)
if not modules.client_topmenu.getTopMenu().hideIngame then
gameRootPanel:addAnchor(AnchorTop, 'topMenu', AnchorBottom)
end
gameMapPanel:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight)
gameMapPanel:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft)
gameMapPanel:addAnchor(AnchorBottom, 'gameBottomPanel', AnchorTop)
gameMapPanel:addAnchor(AnchorBottom, 'gameActionPanel', AnchorTop)
gameMapPanel:setKeepAspectRatio(true)
gameMapPanel:setLimitVisibleRange(false)
gameMapPanel:setZoom(11)
gameMapPanel:setOn(false) -- frame
gameBottomPanel:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight)
gameBottomPanel:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft)
modules.client_topmenu.getTopMenu():setImageColor('white')
gameBottomPanel:setImageColor('white')
if modules.game_console then
modules.game_console.switchMode(false)
@@ -899,13 +958,19 @@ function refreshViewMode()
else
g_game.changeMapAwareRange(31, 21)
gameMapPanel:fill('parent')
gameRootPanel:fill('parent')
gameMapPanel:setKeepAspectRatio(false)
gameMapPanel:setLimitVisibleRange(false)
gameMapPanel:setZoom(15)
gameMapPanel:setOn(true)
if g_app.isMobile() then
gameMapPanel:setZoom(11)
else
gameMapPanel:setZoom(15)
end
modules.client_topmenu.getTopMenu():setImageColor('#ffffff66')
if g_app.isMobile() then
gameMapPanel:setMarginTop(-32)
end
if modules.game_console then
modules.game_console.switchMode(true)
end
@@ -925,7 +990,9 @@ function limitZoom()
limitedZoom = true
end
function updateSize()
function updateSize()
if g_app.isMobile() then return end
local classic = g_settings.getBoolean("classicView")
local height = gameMapPanel:getHeight()
local width = gameMapPanel:getWidth()
@@ -984,3 +1051,45 @@ function updateSize()
gameMapPanel:setMarginLeft(extraMargin)
gameMapPanel:setMarginRight(extraMargin) ]]
end
function setupLeftActions()
if not g_app.isMobile() then return end
for _, widget in ipairs(gameLeftActions:getChildren()) do
widget.image:setChecked(false)
widget.onClick = function()
if widget.image:isChecked() then
widget.image:setChecked(false)
return
end
resetLeftActions()
widget.image:setChecked(true)
end
end
if not gameLeftActions.chat then return end
gameLeftActions.chat.onClick = function()
if gameBottomPanel:getHeight() <= 5 then
gameBottomPanel:setHeight(90)
else
gameBottomPanel:setHeight(0)
end
end
end
function resetLeftActions()
for _, widget in ipairs(gameLeftActions:getChildren()) do
widget.image:setChecked(false)
end
end
function getLeftAction()
for _, widget in ipairs(gameLeftActions:getChildren()) do
if widget.image:isChecked() then
return widget:getId()
end
end
return ""
end
function isChatVisible()
return gameBottomPanel:getHeight() >= 5
end

View File

@@ -1,16 +1,17 @@
GameSidePanel < UIMiniWindowContainer
image-source: /images/ui/panel_side
image-border: 4
padding: 4
padding: 3
padding-top: 0
width: 198
focusable: false
on: true
layout:
type: verticalBox
//spacing: 1
GameBottomPanel < Panel
$mobile:
padding: 0
width: 200
GameMapPanel < UIGameMap
padding: 4
@@ -19,6 +20,24 @@ GameMapPanel < UIGameMap
$on:
padding: 0
GameAction < UIButton
size: 64 64
phantom: false
UIButton
id: image
size: 48 48
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
phantom: true
$checked:
opacity: 1.0
background: #00FF0033
$!checked:
opacity: 0.6
background: alpha
UIWidget
id: gameRootPanel
@@ -33,11 +52,46 @@ UIWidget
focusable: false
Panel
id: gameLeftPanels
id: gameLeftActions
focusable: false
anchors.bottom: parent.bottom
anchors.left: parent.left
width: 64
$!mobile:
visible: false
width: 0
layout:
type: verticalBox
fit-children: true
GameAction
id: use
@onSetup: self.image:setImageSource("/images/game/mobile/use")
GameAction
id: attack
@onSetup: self.image:setImageSource("/images/game/mobile/attack")
GameAction
id: follow
@onSetup: self.image:setImageSource("/images/game/mobile/follow")
GameAction
id: look
@onSetup: self.image:setImageSource("/images/game/mobile/look")
GameAction
id: chat
@onSetup: self.image:setImageSource("/images/game/mobile/chat")
Panel
id: gameLeftPanels
focusable: false
anchors.top: parent.top
anchors.bottom: parent.bottom
focusable: false
$!mobile:
anchors.left: parent.left
$mobile:
anchors.left: gameLeftActions.right
layout:
type: horizontalBox
fit-children: true
@@ -59,19 +113,48 @@ UIWidget
anchors.left: gameLeftPanels.right
anchors.right: gameRightPanels.left
anchors.bottom: parent.bottom
height: 5
relative-margin: bottom
margin-bottom: 180
@canUpdateMargin: function(self, newMargin) if modules.client_options.getOption('dontStretchShrink') then return self:getMarginBottom() end return math.max(math.min(newMargin, self:getParent():getHeight() - 300), 80) end
@onGeometryChange: function(self) self:setMarginBottom(math.min(math.max(self:getParent():getHeight() - 300, 80), self:getMarginBottom())) end
GameBottomPanel
id: gameBottomPanel
anchors.left: bottomSplitter.left
anchors.right: bottomSplitter.right
anchors.top: bottomSplitter.top
anchors.bottom: parent.bottom
margin-top: 3
margin-bottom: 150
@canUpdateMargin: function(self, newMargin) if modules.client_options.getOption('dontStretchShrink') then return self:getMarginBottom() end return math.max(math.min(newMargin, self:getParent():getHeight() - 150), 80) end
@onGeometryChange: function(self) self:setMarginBottom(math.min(math.max(self:getParent():getHeight() - 150, 80), self:getMarginBottom())) end
$mobile:
visible: false
Panel
id: gameActionPanel
phantom: true
focusable: false
$!mobile:
anchors.left: bottomSplitter.left
anchors.right: bottomSplitter.right
anchors.top: bottomSplitter.top
margin-top: 3
$mobile:
anchors.left: gameLeftPanels.right
anchors.right: gameRightPanels.left
anchors.bottom: gameBottomPanel.top
layout:
type: verticalBox
fit-children: true
Panel
id: gameBottomPanel
$!mobile:
anchors.left: gameActionPanel.left
anchors.right: gameActionPanel.right
anchors.top: gameActionPanel.bottom
anchors.bottom: parent.bottom
$mobile:
anchors.left: gameLeftPanels.right
anchors.right: gameRightPanels.left
anchors.bottom: parent.bottom
UIWidget
id: mouseGrabber
focusable: false

View File

@@ -34,7 +34,7 @@ Module
- game_walking
- game_shop
- game_itemselector
- game_textedit
- client_textedit
- game_actionbar
- game_prey
- game_imbuing

View File

@@ -1136,10 +1136,12 @@ function Market.loadMarketItems(category)
if category == MarketCategory.All then
-- loop all categories
for category = MarketCategory.First, MarketCategory.Last do
for i = 1, #marketItems[category] do
local item = marketItems[category][i]
if isItemValid(item, category, searchFilter) then
table.insert(currentItems, item)
if marketItems[category] then
for i = 1, #marketItems[category] do
local item = marketItems[category][i]
if isItemValid(item, category, searchFilter) then
table.insert(currentItems, item)
end
end
end
end

View File

@@ -18,7 +18,7 @@ NPCItemBox < UICheckBox
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
image-color: #ffffffff
margin-top: 5
margin-top: 3
$checked on:
border-color: #ffffff
@@ -33,7 +33,7 @@ NPCItemBox < UICheckBox
MainWindow
id: npcWindow
!text: tr('NPC Trade')
size: 550 515
size: 550 340
@onEscape: modules.game_npctrade.closeNpcTrade()
TabButton
@@ -45,8 +45,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.horizontalCenter
anchors.top: parent.top
margin-right: 5
margin-top: 5
margin-top: 0
TabButton
id: sellTab
@@ -55,15 +54,13 @@ MainWindow
anchors.left: parent.horizontalCenter
anchors.right: parent.right
anchors.top: parent.top
margin-left: 5
margin-top: 5
FlatPanel
height: 250
height: 150
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 10
margin-top: 5
VerticalScrollBar
id: itemsPanelListScrollBar
@@ -75,7 +72,7 @@ MainWindow
ScrollablePanel
id: itemsPanel
height: 250
height: 200
anchors.left: parent.left
anchors.right: prev.left
anchors.top: parent.top
@@ -91,30 +88,19 @@ MainWindow
FlatPanel
id: setupPanel
height: 140
height: 105
enabled: false
anchors.left: parent.left
anchors.right: parent.horizontalCenter
anchors.top: prev.bottom
margin-top: 10
margin-top: 5
margin-right: 5
image-color: #ffffff88
Label
!text: tr('Id') .. ':'
anchors.left: parent.left
anchors.top: parent.top
margin-top: 5
margin-left: 5
width: 85
NPCOfferLabel
id: id
Label
!text: tr('Name') .. ':'
anchors.left: parent.left
anchors.top: prev.bottom
anchors.top: parent.top
margin-top: 5
margin-left: 5
width: 85
@@ -122,11 +108,23 @@ MainWindow
NPCOfferLabel
id: name
Label
!text: tr('Id') .. ':'
anchors.left: parent.left
anchors.top: parent.top
margin-top: 5
margin-left: 5
margin-left: 195
width: 15
NPCOfferLabel
id: id
Label
!text: tr('Price') .. ':'
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
margin-left: 5
width: 85
@@ -137,7 +135,7 @@ MainWindow
!text: tr('Your Money') .. ':'
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
margin-left: 5
width: 85
@@ -149,7 +147,7 @@ MainWindow
!text: tr('Weight') .. ':'
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
margin-left: 5
width: 85
@@ -161,7 +159,7 @@ MainWindow
!text: tr('Your Capacity') .. ':'
anchors.left: parent.left
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
margin-left: 5
width: 85
@@ -173,7 +171,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 5
margin-top: 3
margin-left: 5
margin-right: 5
show-value: true
@@ -184,7 +182,7 @@ MainWindow
FlatPanel
id: buyOptions
height: 140
height: 80
anchors.top: prev.top
anchors.left: parent.horizontalCenter
anchors.right: parent.right
@@ -217,7 +215,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.right
margin-left: 5
margin-top: 5
margin-top: 3
@onCheckChange: modules.game_npctrade.onBuyWithBackpackChange()
CheckBox
@@ -227,7 +225,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.right
margin-left: 5
margin-top: 5
margin-top: 3
@onCheckChange: modules.game_npctrade.onIgnoreCapacityChange()
CheckBox
@@ -237,7 +235,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.right
margin-left: 5
margin-top: 5
margin-top: 3
visible: false
checked: true
@onCheckChange: modules.game_npctrade.onIgnoreEquippedChange()
@@ -249,7 +247,7 @@ MainWindow
anchors.left: parent.left
anchors.right: parent.right
margin-left: 5
margin-top: 5
margin-top: 3
visible: false
checked: true
@onCheckChange: modules.game_npctrade.onShowAllItemsChange()

View File

@@ -5,7 +5,7 @@ PrevMountButton < PreviousButton
MainWindow
!text: tr('Select Outfit')
size: 338 375
size: 338 355
@onEnter: modules.game_outfit.accept()
@onEscape: modules.game_outfit.destroy()
@@ -158,24 +158,21 @@ MainWindow
num-columns: 19
num-lines: 7
// Action Button Section
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 5
margin-top: 5
Button
id: randomizeButton
!text: tr('Randomize')
!tooltip: tr('Randomize characters outfit')
width: 75
anchors.left: prev.left
anchors.top: prev.bottom
margin-right: 16
@onClick: modules.game_outfit.randomize()
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: next.top
margin-bottom: 10
margin-top: 5
anchors.bottom: parent.bottom
@onClick: modules.game_outfit.randomize()
Button
id: outfitOkButton

View File

@@ -4,9 +4,11 @@ questLineWindow = nil
function init()
g_ui.importStyle('questlogwindow')
g_ui.importStyle('questlinewindow')
questLogButton = modules.client_topmenu.addLeftGameButton('questLogButton', tr('Quest Log'), '/images/topbuttons/questlog', function() g_game.requestQuestLog() end, false, 8)
if not g_app.isMobile() then
questLogButton = modules.client_topmenu.addLeftGameButton('questLogButton', tr('Quest Log'), '/images/topbuttons/questlog', function() g_game.requestQuestLog() end, false, 8)
end
connect(g_game, { onQuestLog = onGameQuestLog,
onQuestLine = onGameQuestLine,
onGameEnd = destroyWindows})
@@ -18,7 +20,9 @@ function terminate()
onGameEnd = destroyWindows})
destroyWindows()
questLogButton:destroy()
if questLogButton then
questLogButton:destroy()
end
end
function destroyWindows()

View File

@@ -23,6 +23,8 @@ QuestLogWindow < MainWindow
!text: tr('Quest Log')
size: 500 400
@onEscape: self:destroy()
$mobile:
size: 500 350
TextList
id: questList

View File

@@ -530,8 +530,8 @@ function buyConfirmed()
sendAction("buy", selectedOffer)
if g_game.getFeature(GameIngameStore) and selectedOffer.id and not otcv8shop then
local offerName = selectedOffer.title:lower()
if string.find(offerName, "name") and string.find(offerName, "change") and modules.game_textedit then
modules.game_textedit.singlelineEditor("", function(newName)
if string.find(offerName, "name") and string.find(offerName, "change") and modules.client_textedit then
modules.client_textedit.singlelineEditor("", function(newName)
if newName:len() == 0 then
return
end

View File

@@ -39,7 +39,6 @@ ShopCategoryCreature < ShopCategory
margin-bottom: 2
margin-left: 2
size: 32 32
raw: true
ShopCategoryImage < ShopCategory
Label
@@ -118,7 +117,6 @@ ShopOfferCreature < ShopOffer
margin-bottom: 4
margin-left: 2
size: 48 48
raw: true
ShopOfferImage < ShopOffer
Label

View File

@@ -375,8 +375,11 @@ function walk(dir, ticks)
elseif player:isServerWalking() then
g_game.stop()
return
elseif not toTile then
player:lockWalk(100) -- bug fix for missing stairs down on map
return
else
player:lockWalk(200)
return -- not walkable tile
end
end

View File

@@ -4,10 +4,6 @@ Module
author: OTClient team
website: https://github.com/edubart/otclient
dependencies:
- game_features
- game_things
@onLoad: |
dofile 'const'
dofile 'util'

View File

@@ -86,6 +86,13 @@ function ProtocolLogin:sendLoginPacket()
if self.getLoginExtendedData then
local data = self:getLoginExtendedData()
msg:addString(data)
else
msg:addString("OTCv8")
local version = g_app.getVersion():split(" ")[1]:gsub("%.", "")
if version:len() == 2 then
version = version .. "0"
end
msg:addU16(tonumber(version))
end
local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)

View File

@@ -26,8 +26,11 @@ function UIItem:onDrop(widget, mousePos, forced)
if not item or not item:isItem() then return false end
if self.selectable then
self:setItem(Item.create(item:getId(), item:getCountOrSubType()))
return
if item:isPickupable() then
self:setItem(Item.create(item:getId(), item:getCountOrSubType()))
return true
end
return false
end
local toPos = self.position
@@ -85,7 +88,7 @@ function UIItem:onMouseRelease(mousePosition, mouseButton)
local item = self:getItem()
if not item or not self:containsPoint(mousePosition) then return false end
if modules.client_options.getOption('classicControl') and
if modules.client_options.getOption('classicControl') and not g_app.isMobile() and
((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) or
(g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then
g_game.look(item)

276
modules/updater/updater.lua Normal file
View File

@@ -0,0 +1,276 @@
Updater = { }
Updater.maxRetries = 5
--[[
]]--
local updaterWindow
local loadModulesFunction
local scheduledEvent
local httpOperationId = 0
local function onLog(level, message, time)
if level == LogError then
Updater.error(message)
g_logger.setOnLog(nil)
end
end
local function initAppWindow()
if g_resources.getLayout() == "mobile" then
g_window.setMinimumSize({ width = 640, height = 360 })
else
g_window.setMinimumSize({ width = 800, height = 640 })
end
-- window size
local size = { width = 1024, height = 600 }
size = g_settings.getSize('window-size', size)
g_window.resize(size)
-- window position, default is the screen center
local displaySize = g_window.getDisplaySize()
local defaultPos = { x = (displaySize.width - size.width)/2,
y = (displaySize.height - size.height)/2 }
local pos = g_settings.getPoint('window-pos', defaultPos)
pos.x = math.max(pos.x, 0)
pos.y = math.max(pos.y, 0)
g_window.move(pos)
-- window maximized?
local maximized = g_settings.getBoolean('window-maximized', false)
if maximized then g_window.maximize() end
g_window.setTitle(g_app.getName())
g_window.setIcon('/images/clienticon')
if g_app.isMobile() then
scheduleEvent(function()
g_app.scale(5.0)
end, 10)
end
end
local function loadModules()
if loadModulesFunction then
local tmpLoadFunc = loadModulesFunction
loadModulesFunction = nil
tmpLoadFunc()
end
end
local function downloadFiles(url, files, index, retries, doneCallback)
local entry = files[index]
if not entry then -- finished
return doneCallback()
end
local file = entry[1]
local file_checksum = entry[2]
if retries > 0 then
updaterWindow.downloadStatus:setText(tr("Downloading (%i retry):\n%s", retries, file))
else
updaterWindow.downloadStatus:setText(tr("Downloading:\n%s", file))
end
updaterWindow.downloadProgress:setPercent(0)
updaterWindow.mainProgress:setPercent(math.floor(100 * index / #files))
httpOperationId = HTTP.download(url .. file, file,
function (file, checksum, err)
if not err and checksum ~= file_checksum then
err = "Invalid checksum of: " .. file .. ".\nShould be " .. file_checksum .. ", is: " .. checksum
end
if err then
if retries >= Updater.maxRetries then
Updater.error("Can't download file: " .. file .. ".\nError: " .. err)
else
scheduledEvent = scheduleEvent(function()
downloadFiles(url, files, index, retries + 1, doneCallback)
end, 250)
end
return
end
downloadFiles(url, files, index + 1, 0, doneCallback)
end,
function (progress, speed)
updaterWindow.downloadProgress:setPercent(progress)
updaterWindow.downloadProgress:setText(speed .. " kbps")
end)
end
local function updateFiles(data, keepCurrentFiles)
if type(data) ~= "table" then
return Updater.error("Invalid data from updater api (not table)")
end
if type(data["error"]) == 'string' and data["error"]:len() > 0 then
return Updater.error(data["error"])
end
if not data["files"] or type(data["url"]) ~= 'string' or data["url"]:len() < 4 then
return Updater.error("Invalid data from updater api: " .. json.encode(data, 2))
end
if data["keepFiles"] then
keepCurrentFiles = true
end
local newFiles = false
local finalFiles = {}
local localFiles = g_resources.filesChecksums()
local toUpdate = {}
-- keep all files or files from data/things
for file, checksum in pairs(localFiles) do
if keepCurrentFiles or string.find(file, "data/things") then
table.insert(finalFiles, file)
end
end
-- update files
for file, checksum in pairs(data["files"]) do
table.insert(finalFiles, file)
if not localFiles[file] or localFiles[file] ~= checksum then
table.insert(toUpdate, {file, checksum})
newFiles = true
end
end
-- update binary
local binary = nil
if type(data["binary"]) == "table" and data["binary"]["file"]:len() > 1 then
local selfChecksum = g_resources.selfChecksum()
if selfChecksum:len() > 0 and selfChecksum ~= data["binary"]["checksum"] then
binary = data["binary"]["file"]
table.insert(toUpdate, {binary, data["binary"]["checksum"]})
end
end
if #toUpdate == 0 then -- nothing to update
updaterWindow.mainProgress:setPercent(100)
scheduledEvent = scheduleEvent(Updater.abort, 20)
return
end
-- update of some files require full client restart
local forceRestart = false
local reloadModules = false
local forceRestartPattern = {"init.lua", "corelib", "updater", "otmod"}
for _, file in ipairs(toUpdate) do
for __, pattern in ipairs(forceRestartPattern) do
if string.find(file[1], pattern) then
forceRestart = true
end
if not string.find(file[1], "data/things") then
reloadModules = true
end
end
end
updaterWindow.status:setText(tr("Updating %i files", #toUpdate))
updaterWindow.mainProgress:setPercent(0)
updaterWindow.downloadProgress:setPercent(0)
updaterWindow.downloadProgress:show()
updaterWindow.downloadStatus:show()
updaterWindow.changeUrlButton:hide()
downloadFiles(data["url"], toUpdate, 1, 0, function()
updaterWindow.status:setText(tr("Updating client (may take few seconds)"))
updaterWindow.mainProgress:setPercent(100)
updaterWindow.downloadProgress:hide()
updaterWindow.downloadStatus:hide()
scheduledEvent = scheduleEvent(function()
local restart = binary or (not loadModulesFunction and reloadModules) or forceRestart
if newFiles then
g_resources.updateData(finalFiles, not restart)
end
if binary then
g_resources.updateExecutable(binary)
end
if restart then
g_app.restart()
else
if reloadModules then
g_modules.reloadModules()
end
Updater.abort()
end
end, 100)
end)
end
-- public functions
function Updater.init(loadModulesFunc)
g_logger.setOnLog(onLog)
loadModulesFunction = loadModulesFunc
initAppWindow()
Updater.check()
end
function Updater.terminate()
loadModulesFunction = nil
Updater.abort()
end
function Updater.abort()
HTTP.cancel(httpOperationId)
removeEvent(scheduledEvent)
if updaterWindow then
updaterWindow:destroy()
updaterWindow = nil
end
loadModules()
end
function Updater.check(args)
if updaterWindow then return end
updaterWindow = g_ui.displayUI('updater')
updaterWindow:show()
updaterWindow:focus()
updaterWindow:raise()
local updateData = nil
local function progressUpdater(value)
removeEvent(scheduledEvent)
if value == 100 then
return Updater.error(tr("Timeout"))
end
if updateData and (value > 60 or (not g_app.isMobile() or not ALLOW_CUSTOM_SERVERS or not loadModulesFunc)) then -- gives 3s to set custom updater for mobile version
return updateFiles(updateData)
end
scheduledEvent = scheduleEvent(function() progressUpdater(value + 1) end, 50)
updaterWindow.mainProgress:setPercent(value)
end
progressUpdater(0)
httpOperationId = HTTP.postJSON(Services.updater, {
version = APP_VERSION,
build = g_app.getVersion(),
os = g_app.getOs(),
platform = g_window.getPlatformType(),
args = args or {}
}, function(data, err)
if err then
return Updater.error(err)
end
updateData = data
end)
end
function Updater.error(message)
removeEvent(scheduledEvent)
if not updaterWindow then return end
displayErrorBox(tr("Updater Error"), message).onOk = function()
Updater.abort()
end
end
function Updater.changeUrl()
removeEvent(scheduledEvent)
modules.client_textedit.edit(Services.updater, {title="Enter updater url", width=300}, function(newUrl)
if updaterWindow and newUrl:len() > 4 then
Services.updater = newUrl
end
if updaterWindow then
updaterWindow:destroy()
updaterWindow = nil
end
Updater.check()
end)
end

View File

@@ -1,9 +1,12 @@
Module
name: client_updater
name: updater
description: Updates client
author: otclient@otclient.ovh
website: otclient.ovh
reloadable: false
scripts: [ updater ]
@onLoad: Updater.init()
dependencies: [ client_locales, client_styles, client_textedit ]
@onUnload: Updater.terminate()
load-later:
- client_background

View File

@@ -0,0 +1,53 @@
StaticMainWindow
id: updaterWindow
!text: tr('Updater')
width: 350
layout:
type: verticalBox
fit-children: true
Label
id: status
!text: tr('Checking for updates')
text-align: center
ProgressBar
id: mainProgress
height: 15
background-color: #4444ff
margin-bottom: 5
margin-top: 5
Label
id: downloadStatus
text-align: center
text-auto-resize: true
text-wrap: true
visible: false
ProgressBar
id: downloadProgress
height: 15
background-color: #4444ff
margin-bottom: 5
margin-top: 5
visible: false
Button
id: changeUrlButton
margin-left: 50
margin-right: 50
margin-top: 5
margin-bottom: 10
!text: tr('Change updater URL')
@onClick: Updater.changeUrl()
$!mobile:
visible: false
Button
margin-left: 90
margin-right: 90
!text: tr('Cancel')
@onClick: Updater.abort()