This commit is contained in:
Tulioh 2015-04-19 12:22:16 -03:00
commit 2b96ae7f09
386 changed files with 2616 additions and 921 deletions

View File

@ -1,6 +1,6 @@
OTClient is made available under the MIT License OTClient is made available under the MIT License
Copyright (c) 2010-2012 OTClient <https://github.com/edubart/otclient> Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -41,6 +41,7 @@ A package with all required libraries for compiling OTClient on Windows can be f
In short, if you need to compile OTClient, follow these tutorials: In short, if you need to compile OTClient, follow these tutorials:
* [Compiling on Windows](https://github.com/edubart/otclient/wiki/Compiling-on-Windows) * [Compiling on Windows](https://github.com/edubart/otclient/wiki/Compiling-on-Windows)
* [Compiling on Linux](https://github.com/edubart/otclient/wiki/Compiling-on-Linux) * [Compiling on Linux](https://github.com/edubart/otclient/wiki/Compiling-on-Linux)
* [Compiling on OS X](https://github.com/edubart/otclient/wiki/Compiling-on-Mac-OS-X)

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -4,6 +4,10 @@ locale = {
charset = "cp1252", charset = "cp1252",
languageName = "Deutsch", languageName = "Deutsch",
formatNumbers = true,
decimalSeperator = ',',
thousandsSeperator = ' ',
translation = { translation = {
["1a) Offensive Name"] = false, ["1a) Offensive Name"] = false,
["1b) Invalid Name Format"] = false, ["1b) Invalid Name Format"] = false,

View File

@ -3,6 +3,10 @@ locale = {
charset = "cp1252", charset = "cp1252",
languageName = "English", languageName = "English",
formatNumbers = true,
decimalSeperator = '.',
thousandsSeperator = ',',
-- translations are not needed because everything is already in english -- translations are not needed because everything is already in english
translation = {} translation = {}
} }

View File

@ -7,6 +7,10 @@ locale = {
charset = "cp1252", charset = "cp1252",
languageName = "Español", languageName = "Español",
formatNumbers = true,
decimalSeperator = ',',
thousandsSeperator = '.',
translation = { translation = {
["1a) Offensive Name"] = "1a) Nombre ofensivo", ["1a) Offensive Name"] = "1a) Nombre ofensivo",
["1b) Invalid Name Format"] = "1b) Formato invalido para nombre", ["1b) Invalid Name Format"] = "1b) Formato invalido para nombre",

View File

@ -2,6 +2,10 @@ locale = {
name = "pl", name = "pl",
languageName = "Polski", languageName = "Polski",
formatNumbers = true,
decimalSeperator = '.',
thousandsSeperator = ' ',
translation = { translation = {
["1a) Offensive Name"] = "1a) Obrazliwe Imie", ["1a) Offensive Name"] = "1a) Obrazliwe Imie",
["1b) Invalid Name Format"] = "1b) Niepoprawny Format Imienia", ["1b) Invalid Name Format"] = "1b) Niepoprawny Format Imienia",

View File

@ -3,6 +3,10 @@ locale = {
charset = "cp1252", charset = "cp1252",
languageName = "Português", languageName = "Português",
formatNumbers = true,
decimalSeperator = ',',
thousandsSeperator = '.',
-- As traduções devem vir sempre em ordem alfabética. -- As traduções devem vir sempre em ordem alfabética.
translation = { translation = {
["%d of experience per hour"] = "%d de experiência por hora", ["%d of experience per hour"] = "%d de experiência por hora",

View File

@ -5,6 +5,10 @@ locale = {
charset = "cp1252", charset = "cp1252",
languageName = "Svenska", languageName = "Svenska",
formatNumbers = true,
decimalSeperator = ',',
thousandsSeperator = ' ',
translation = { translation = {
["1a) Offensive Name"] = "1a) Offensivt Namn", ["1a) Offensive Name"] = "1a) Offensivt Namn",
["1b) Invalid Name Format"] = "1b) Ogiltigt Namnformat", ["1b) Invalid Name Format"] = "1b) Ogiltigt Namnformat",

View File

@ -8,6 +8,7 @@ Button < UIButton
image-clip: 0 0 22 23 image-clip: 0 0 22 23
image-border: 3 image-border: 3
padding: 5 10 5 10 padding: 5 10 5 10
opacity: 1.0
$hover !disabled: $hover !disabled:
image-clip: 0 23 22 23 image-clip: 0 23 22 23
@ -45,6 +46,7 @@ NextButton < UIButton
size: 12 21 size: 12 21
image-source: /images/ui/arrow_horizontal image-source: /images/ui/arrow_horizontal
image-clip: 12 0 12 21 image-clip: 12 0 12 21
image-color: #ffffff
$hover !disabled: $hover !disabled:
image-clip: 12 21 12 21 image-clip: 12 21 12 21
@ -53,12 +55,13 @@ NextButton < UIButton
image-clip: 12 21 12 21 image-clip: 12 21 12 21
$disabled: $disabled:
image-color: #dfdfdf55 image-color: #dfdfdf88
PreviousButton < UIButton PreviousButton < UIButton
size: 12 21 size: 12 21
image-source: /images/ui/arrow_horizontal image-source: /images/ui/arrow_horizontal
image-clip: 0 0 12 21 image-clip: 0 0 12 21
image-color: #ffffff
$hover !disabled: $hover !disabled:
image-clip: 0 21 12 21 image-clip: 0 21 12 21
@ -67,7 +70,7 @@ PreviousButton < UIButton
image-clip: 0 21 12 21 image-clip: 0 21 12 21
$disabled: $disabled:
image-color: #dfdfdf55 image-color: #dfdfdf88
AddButton < UIButton AddButton < UIButton
size: 20 20 size: 20 20

View File

@ -27,7 +27,7 @@ MoveableTabBarButton < UIButton
color: #dfdfdf color: #dfdfdf
$on !checked: $on !checked:
color: #dfdfdf color: #de6f6f
TabBar < UITabBar TabBar < UITabBar
size: 80 21 size: 80 21
@ -36,10 +36,11 @@ TabBar < UITabBar
anchors.fill: parent anchors.fill: parent
TabBarPanel < Panel TabBarPanel < Panel
TabBarButton < UIButton TabBarButton < UIButton
size: 22 23 size: 20 21
image-source: /images/ui/tabbutton_square
image-source: /images/ui/tabbutton_square image-source: /images/ui/tabbutton_square
image-color: #dfdfdf image-color: #dfdfdf
image-clip: 0 0 22 23 image-clip: 0 0 20 21
image-border: 3 image-border: 3
image-border-bottom: 0 image-border-bottom: 0
icon-color: #dfdfdf icon-color: #dfdfdf
@ -55,7 +56,7 @@ TabBarButton < UIButton
margin-left: 5 margin-left: 5
$hover !checked: $hover !checked:
image-clip: 0 23 22 23 image-clip: 0 21 20 21
color: #dfdfdf color: #dfdfdf
$disabled: $disabled:
@ -63,7 +64,7 @@ TabBarButton < UIButton
icon-color: #dfdfdf icon-color: #dfdfdf
$checked: $checked:
image-clip: 0 46 22 23 image-clip: 0 42 20 21
color: #dfdfdf color: #dfdfdf
$on !checked: $on !checked:
@ -73,6 +74,14 @@ TabBarRounded < TabBar
TabBarRoundedPanel < TabBarPanel TabBarRoundedPanel < TabBarPanel
TabBarRoundedButton < TabBarButton TabBarRoundedButton < TabBarButton
image-source: /images/ui/tabbutton_rounded image-source: /images/ui/tabbutton_rounded
size: 22 23
image-clip: 0 0 22 23
$hover !checked:
image-clip: 0 23 22 23
$checked:
image-clip: 0 46 22 23
TabBarVertical < UITabBar TabBarVertical < UITabBar
width: 96 width: 96

View File

@ -1,26 +1,62 @@
Table < UITable Table < UITable
layout: verticalBox layout: verticalBox
header-column-style: HeaderTableColumn header-column-style: TableHeaderColumn
header-row-style: HeaderTableRow header-row-style: TableHeaderRow
column-style: TableColumn column-style: TableColumn
row-style: TableRow row-style: TableRow
TableData < UIScrollArea TableData < UIScrollArea
layout: verticalBox layout: verticalBox
TableRow < Label TableRow < UITableRow
layout: horizontalBox layout: horizontalBox
height: 10 height: 10
text-wrap: true text-wrap: true
focusable: true
even-background-color: alpha
odd-background-color: #00000022
$focus:
background-color: #294f6d
color: #ffffff
TableColumn < Label TableColumn < Label
width: 30 width: 30
text-wrap: true text-wrap: true
focusable: false
TableHeaderRow < Label TableHeaderRow < Label
layout: horizontalBox layout: horizontalBox
focusable: false
height: 10 height: 10
text-wrap: true text-wrap: true
TableHeaderColumn < Button TableHeaderColumn < UITableHeaderColumn
width: 30 font: verdana-11px-antialised
background-color: alpha
color: #dfdfdfff
height: 23
focusable: true
text-offset: 0 0
image-source: /images/ui/button
image-color: #dfdfdf
image-clip: 0 0 22 23
image-border: 3
padding: 5 10 5 10
enabled: false
focusable: false
$hover !disabled:
image-clip: 0 23 22 23
$pressed:
image-clip: 0 46 22 23
text-offset: 1 1
$disabled:
color: #dfdfdf88
opacity: 0.8
SortableTableHeaderColumn < TableHeaderColumn
enabled: true
focusable: true

View File

@ -27,7 +27,7 @@ local function tryLogin(charInfo, tries)
CharacterList.hide() CharacterList.hide()
g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName) g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey)
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...')) loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...'))
connect(loadBox, { onCancel = function() connect(loadBox, { onCancel = function()
@ -109,6 +109,16 @@ function onGameLoginError(message)
end end
end end
function onGameLoginToken(unknown)
CharacterList.destroyLoadBox()
-- TODO: make it possible to enter a new token here / prompt token
errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.')
errorBox.onOk = function()
errorBox = nil
EnterGame.show()
end
end
function onGameConnectionError(message, code) function onGameConnectionError(message, code)
CharacterList.destroyLoadBox() CharacterList.destroyLoadBox()
local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message) local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message)
@ -131,6 +141,7 @@ end
-- public functions -- public functions
function CharacterList.init() function CharacterList.init()
connect(g_game, { onLoginError = onGameLoginError }) connect(g_game, { onLoginError = onGameLoginError })
connect(g_game, { onLoginToken = onGameLoginToken })
connect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) connect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
connect(g_game, { onConnectionError = onGameConnectionError }) connect(g_game, { onConnectionError = onGameConnectionError })
connect(g_game, { onGameStart = CharacterList.destroyLoadBox }) connect(g_game, { onGameStart = CharacterList.destroyLoadBox })
@ -144,6 +155,7 @@ end
function CharacterList.terminate() function CharacterList.terminate()
disconnect(g_game, { onLoginError = onGameLoginError }) disconnect(g_game, { onLoginError = onGameLoginError })
disconnect(g_game, { onLoginToken = onGameLoginToken })
disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded }) disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
disconnect(g_game, { onConnectionError = onGameConnectionError }) disconnect(g_game, { onConnectionError = onGameConnectionError })
disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox }) disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox })

View File

@ -16,7 +16,7 @@ CharacterWidget < UIWidget
Label Label
id: name id: name
color: #aaaaaa color: #bbbbbb
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
font: verdana-11px-monochrome font: verdana-11px-monochrome
@ -29,8 +29,7 @@ CharacterWidget < UIWidget
Label Label
id: worldName id: worldName
color: #ffffff color: #bbbbbb
color: #aaaaaa
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
margin-right: 5 margin-right: 5
@ -45,16 +44,21 @@ CharacterWidget < UIWidget
MainWindow MainWindow
id: charactersWindow id: charactersWindow
!text: tr('Character List') !text: tr('Character List')
size: 250 248
visible: false visible: false
@onEnter: CharacterList.doLogin() @onEnter: CharacterList.doLogin()
@onEscape: CharacterList.hide(true) @onEscape: CharacterList.hide(true)
@onSetup: | @onSetup: |
g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self) g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self)
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self) g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
if g_game.getFeature(GamePreviewState) then
self:setSize({width = 350, height = 400})
else
self:setSize({width = 250, height = 248})
end
TextList TextList
id: characters id: characters
background-color: #565656
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: characterListScrollBar.left anchors.right: characterListScrollBar.left

View File

@ -33,10 +33,17 @@ local function onMotd(protocol, motd)
end end
end end
local function onSessionKey(protocol, sessionKey)
G.sessionKey = sessionKey
end
local function onCharacterList(protocol, characters, account, otui) local function onCharacterList(protocol, characters, account, otui)
-- Try add server to the server list -- Try add server to the server list
ServerList.add(G.host, G.port, g_game.getClientVersion()) ServerList.add(G.host, G.port, g_game.getClientVersion())
-- Save 'Stay logged in' setting
g_settings.set('staylogged', enterGame:getChildById('stayLoggedBox'):isChecked())
if enterGame:getChildById('rememberPasswordBox'):isChecked() then if enterGame:getChildById('rememberPasswordBox'):isChecked() then
local account = g_crypt.encrypt(G.account) local account = g_crypt.encrypt(G.account)
local password = g_crypt.encrypt(G.password) local password = g_crypt.encrypt(G.password)
@ -59,6 +66,12 @@ local function onCharacterList(protocol, characters, account, otui)
loadBox:destroy() loadBox:destroy()
loadBox = nil loadBox = nil
for _, characterInfo in pairs(characters) do
if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then
characterInfo.worldName = characterInfo.worldName .. ', Preview'
end
end
CharacterList.create(characters, account, otui) CharacterList.create(characters, account, otui)
CharacterList.show() CharacterList.show()
@ -103,9 +116,10 @@ function EnterGame.init()
local password = g_settings.get('password') local password = g_settings.get('password')
local host = g_settings.get('host') local host = g_settings.get('host')
local port = g_settings.get('port') local port = g_settings.get('port')
local stayLogged = g_settings.getBoolean('staylogged')
local autologin = g_settings.getBoolean('autologin') local autologin = g_settings.getBoolean('autologin')
local clientVersion = g_settings.getInteger('client-version') local clientVersion = g_settings.getInteger('client-version')
if clientVersion == 0 then clientVersion = 860 end if clientVersion == 0 then clientVersion = 1074 end
if port == nil or port == 0 then port = 7171 end if port == nil or port == 0 then port = 7171 end
@ -115,6 +129,7 @@ function EnterGame.init()
enterGame:getChildById('serverHostTextEdit'):setText(host) enterGame:getChildById('serverHostTextEdit'):setText(host)
enterGame:getChildById('serverPortTextEdit'):setText(port) enterGame:getChildById('serverPortTextEdit'):setText(port)
enterGame:getChildById('autoLoginBox'):setChecked(autologin) enterGame:getChildById('autoLoginBox'):setChecked(autologin)
enterGame:getChildById('stayLoggedBox'):setChecked(stayLogged)
clientBox = enterGame:getChildById('clientComboBox') clientBox = enterGame:getChildById('clientComboBox')
for _, proto in pairs(g_game.getSupportedClients()) do for _, proto in pairs(g_game.getSupportedClients()) do
@ -122,6 +137,10 @@ function EnterGame.init()
end end
clientBox:setCurrentOption(clientVersion) clientBox:setCurrentOption(clientVersion)
EnterGame.toggleAuthenticatorToken(clientVersion, true)
EnterGame.toggleStayLoggedBox(clientVersion, true)
connect(clientBox, { onOptionChange = EnterGame.onClientVersionChange })
enterGame:hide() enterGame:hide()
if g_app.isRunning() and not g_game.isOnline() then if g_app.isRunning() and not g_game.isOnline() then
@ -146,6 +165,7 @@ end
function EnterGame.terminate() function EnterGame.terminate()
g_keyboard.unbindKeyDown('Ctrl+G') g_keyboard.unbindKeyDown('Ctrl+G')
disconnect(clientBox, { onOptionChange = EnterGame.onClientVersionChange })
enterGame:destroy() enterGame:destroy()
enterGame = nil enterGame = nil
enterGameButton:destroy() enterGameButton:destroy()
@ -204,14 +224,80 @@ end
function EnterGame.clearAccountFields() function EnterGame.clearAccountFields()
enterGame:getChildById('accountNameTextEdit'):clearText() enterGame:getChildById('accountNameTextEdit'):clearText()
enterGame:getChildById('accountPasswordTextEdit'):clearText() enterGame:getChildById('accountPasswordTextEdit'):clearText()
enterGame:getChildById('authenticatorTokenTextEdit'):clearText()
enterGame:getChildById('accountNameTextEdit'):focus() enterGame:getChildById('accountNameTextEdit'):focus()
g_settings.remove('account') g_settings.remove('account')
g_settings.remove('password') g_settings.remove('password')
end end
function EnterGame.toggleAuthenticatorToken(clientVersion, init)
local enabled = (clientVersion >= 1072)
if enabled == enterGame.authenticatorEnabled then
return
end
enterGame:getChildById('authenticatorTokenLabel'):setOn(enabled)
enterGame:getChildById('authenticatorTokenTextEdit'):setOn(enabled)
local newHeight = enterGame:getHeight()
local newY = enterGame:getY()
if enabled then
newY = newY - enterGame.authenticatorHeight
newHeight = newHeight + enterGame.authenticatorHeight
else
newY = newY + enterGame.authenticatorHeight
newHeight = newHeight - enterGame.authenticatorHeight
end
if not init then
enterGame:breakAnchors()
enterGame:setY(newY)
enterGame:bindRectToParent()
end
enterGame:setHeight(newHeight)
enterGame.authenticatorEnabled = enabled
end
function EnterGame.toggleStayLoggedBox(clientVersion, init)
local enabled = (clientVersion >= 1074)
if enabled == enterGame.stayLoggedBoxEnabled then
return
end
enterGame:getChildById('stayLoggedBox'):setOn(enabled)
local newHeight = enterGame:getHeight()
local newY = enterGame:getY()
if enabled then
newY = newY - enterGame.stayLoggedBoxHeight
newHeight = newHeight + enterGame.stayLoggedBoxHeight
else
newY = newY + enterGame.stayLoggedBoxHeight
newHeight = newHeight - enterGame.stayLoggedBoxHeight
end
if not init then
enterGame:breakAnchors()
enterGame:setY(newY)
enterGame:bindRectToParent()
end
enterGame:setHeight(newHeight)
enterGame.stayLoggedBoxEnabled = enabled
end
function EnterGame.onClientVersionChange(comboBox, text, data)
local clientVersion = tonumber(text)
EnterGame.toggleAuthenticatorToken(clientVersion)
EnterGame.toggleStayLoggedBox(clientVersion)
end
function EnterGame.doLogin() function EnterGame.doLogin()
G.account = enterGame:getChildById('accountNameTextEdit'):getText() G.account = enterGame:getChildById('accountNameTextEdit'):getText()
G.password = enterGame:getChildById('accountPasswordTextEdit'):getText() G.password = enterGame:getChildById('accountPasswordTextEdit'):getText()
G.authenticatorToken = enterGame:getChildById('authenticatorTokenTextEdit'):getText()
G.stayLogged = enterGame:getChildById('stayLoggedBox'):isChecked()
G.host = enterGame:getChildById('serverHostTextEdit'):getText() G.host = enterGame:getChildById('serverHostTextEdit'):getText()
G.port = tonumber(enterGame:getChildById('serverPortTextEdit'):getText()) G.port = tonumber(enterGame:getChildById('serverPortTextEdit'):getText())
local clientVersion = tonumber(clientBox:getText()) local clientVersion = tonumber(clientBox:getText())
@ -230,6 +316,7 @@ function EnterGame.doLogin()
protocolLogin = ProtocolLogin.create() protocolLogin = ProtocolLogin.create()
protocolLogin.onLoginError = onError protocolLogin.onLoginError = onError
protocolLogin.onMotd = onMotd protocolLogin.onMotd = onMotd
protocolLogin.onSessionKey = onSessionKey
protocolLogin.onCharacterList = onCharacterList protocolLogin.onCharacterList = onCharacterList
protocolLogin.onUpdateNeeded = onUpdateNeeded protocolLogin.onUpdateNeeded = onUpdateNeeded
@ -240,12 +327,12 @@ function EnterGame.doLogin()
EnterGame.show() EnterGame.show()
end }) end })
g_game.chooseRsa(G.host)
g_game.setClientVersion(clientVersion) g_game.setClientVersion(clientVersion)
g_game.setProtocolVersion(g_game.getClientProtocolVersion(clientVersion)) g_game.setProtocolVersion(g_game.getClientProtocolVersion(clientVersion))
g_game.chooseRsa(G.host)
if modules.game_things.isLoaded() then if modules.game_things.isLoaded() then
protocolLogin:login(G.host, G.port, G.account, G.password) protocolLogin:login(G.host, G.port, G.account, G.password, G.authenticatorToken, G.stayLogged)
else else
loadBox:destroy() loadBox:destroy()
loadBox = nil loadBox = nil
@ -266,6 +353,7 @@ function EnterGame.setDefaultServer(host, port, protocol)
local clientLabel = enterGame:getChildById('clientLabel') local clientLabel = enterGame:getChildById('clientLabel')
local accountTextEdit = enterGame:getChildById('accountNameTextEdit') local accountTextEdit = enterGame:getChildById('accountNameTextEdit')
local passwordTextEdit = enterGame:getChildById('accountPasswordTextEdit') local passwordTextEdit = enterGame:getChildById('accountPasswordTextEdit')
local authenticatorTokenTextEdit = enterGame:getChildById('authenticatorTokenTextEdit')
if hostTextEdit:getText() ~= host then if hostTextEdit:getText() ~= host then
hostTextEdit:setText(host) hostTextEdit:setText(host)
@ -273,6 +361,7 @@ function EnterGame.setDefaultServer(host, port, protocol)
clientBox:setCurrentOption(protocol) clientBox:setCurrentOption(protocol)
accountTextEdit:setText('') accountTextEdit:setText('')
passwordTextEdit:setText('') passwordTextEdit:setText('')
authenticatorTokenTextEdit:setText('')
end end
end end
@ -286,6 +375,16 @@ function EnterGame.setUniqueServer(host, port, protocol, windowWidth, windowHeig
portTextEdit:setVisible(false) portTextEdit:setVisible(false)
portTextEdit:setHeight(0) portTextEdit:setHeight(0)
local authenticatorTokenTextEdit = enterGame:getChildById('authenticatorTokenTextEdit')
authenticatorTokenTextEdit:setText('')
authenticatorTokenTextEdit:setOn(false)
local authenticatorTokenLabel = enterGame:getChildById('authenticatorTokenLabel')
authenticatorTokenLabel:setOn(false)
local stayLoggedBox = enterGame:getChildById('stayLoggedBox')
stayLoggedBox:setChecked(false)
stayLoggedBox:setOn(false)
clientBox:setCurrentOption(protocol) clientBox:setCurrentOption(protocol)
clientBox:setVisible(false) clientBox:setVisible(false)
clientBox:setHeight(0) clientBox:setHeight(0)
@ -306,11 +405,11 @@ function EnterGame.setUniqueServer(host, port, protocol, windowWidth, windowHeig
serverListButton:setWidth(0) serverListButton:setWidth(0)
local rememberPasswordBox = enterGame:getChildById('rememberPasswordBox') local rememberPasswordBox = enterGame:getChildById('rememberPasswordBox')
rememberPasswordBox:setMarginTop(-5) rememberPasswordBox:setMarginTop(-8)
if not windowWidth then windowWidth = 236 end if not windowWidth then windowWidth = 236 end
enterGame:setWidth(windowWidth) enterGame:setWidth(windowWidth)
if not windowHeight then windowHeight = 200 end if not windowHeight then windowHeight = 210 end
enterGame:setHeight(windowHeight) enterGame:setHeight(windowHeight)
end end

View File

@ -1,6 +1,6 @@
EnterGameWindow < MainWindow EnterGameWindow < MainWindow
!text: tr('Enter Game') !text: tr('Enter Game')
size: 236 274 size: 236 298
EnterGameButton < Button EnterGameButton < Button
width: 64 width: 64
@ -21,6 +21,10 @@ ServerListButton < UIButton
EnterGameWindow EnterGameWindow
id: enterGame id: enterGame
&authenticatorEnabled: false
&authenticatorHeight: 44
&stayLoggedBoxEnabled: false
&stayLoggedBoxHeight: 24
@onEnter: EnterGame.doLogin() @onEnter: EnterGame.doLogin()
MenuLabel MenuLabel
@ -50,6 +54,52 @@ EnterGameWindow
anchors.top: prev.bottom anchors.top: prev.bottom
margin-top: 2 margin-top: 2
MenuLabel
id: authenticatorTokenLabel
!text: tr('Authenticator Token')
anchors.left: prev.left
anchors.top: prev.bottom
text-auto-resize: true
margin-top: -12
visible: false
$on:
visible: true
margin-top: 8
TextEdit
id: authenticatorTokenTextEdit
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: -22
visible: false
max-length: 8
$on:
visible: true
margin-top: 2
CheckBox
id: stayLoggedBox
!text: tr('Stay logged during session')
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
margin-top: -16
visible: false
$on:
visible: true
margin-top: 8
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 8
MenuLabel MenuLabel
id: serverLabel id: serverLabel
!text: tr('Server') !text: tr('Server')
@ -132,16 +182,24 @@ EnterGameWindow
anchors.top: prev.bottom anchors.top: prev.bottom
margin-top: 2 margin-top: 2
HorizontalSeparator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: prev.bottom
margin-top: 6
EnterGameButton EnterGameButton
!text: tr('Ok') !text: tr('Ok')
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.top: prev.bottom
margin-top: 4
@onClick: EnterGame.doLogin() @onClick: EnterGame.doLogin()
Label Label
id: serverInfoLabel id: serverInfoLabel
font: verdana-11px-rounded font: verdana-11px-rounded
anchors.bottom: parent.bottom anchors.top: prev.top
anchors.left: parent.left anchors.left: parent.left
margin-top: 5
color: green color: green
text-auto-resize: true text-auto-resize: true

View File

@ -166,16 +166,20 @@ end
-- global function used to translate texts -- global function used to translate texts
function _G.tr(text, ...) function _G.tr(text, ...)
if currentLocale then if currentLocale then
if tonumber(text) then if tonumber(text) and currentLocale.formatNumbers then
-- todo: use locale information to calculate this. also detect floating numbers local number = tostring(text):split('.')
local out = '' local out = ''
local number = tostring(text):reverse() local reverseNumber = number[1]:reverse()
for i=1,#number do for i=1,#reverseNumber do
out = out .. number:sub(i, i) out = out .. reverseNumber:sub(i, i)
if i % 3 == 0 and i ~= #number then if i % 3 == 0 and i ~= #number then
out = out .. ',' out = out .. currentLocale.thousandsSeperator
end end
end end
if number[2] then
out = number[2] .. currentLocale.decimalSeperator .. out
end
return out:reverse() return out:reverse()
elseif tostring(text) then elseif tostring(text) then
local translation = currentLocale.translation[text] local translation = currentLocale.translation[text]

View File

@ -30,6 +30,10 @@ local allLines = {}
-- private functions -- private functions
local function navigateCommand(step) local function navigateCommand(step)
if commandTextEdit:isMultiline() then
return
end
local numCommands = #commandHistory local numCommands = #commandHistory
if numCommands > 0 then if numCommands > 0 then
currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands) currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands)
@ -96,16 +100,29 @@ local function completeCommand()
end end
end end
local function doCommand() local function doCommand(textWidget)
local currentCommand = commandTextEdit:getText() local currentCommand = textWidget:getText()
executeCommand(currentCommand) executeCommand(currentCommand)
textWidget:clearText()
if commandTextEdit then
commandTextEdit:clearText()
end
return true return true
end end
local function addNewline(textWidget)
if not textWidget:isOn() then
textWidget:setOn(true)
end
textWidget:appendText('\n')
end
local function onCommandChange(textWidget, newText, oldText)
local _, newLineCount = string.gsub(newText, '\n', '\n')
textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight)
if newLineCount == 0 and textWidget:isOn() then
textWidget:setOn(false)
end
end
local function onLog(level, message, time) local function onLog(level, message, time)
if disabled then return end if disabled then return end
-- avoid logging while reporting logs (would cause a infinite loop) -- avoid logging while reporting logs (would cause a infinite loop)
@ -129,6 +146,8 @@ function init()
commandHistory = g_settings.getList('terminal-history') commandHistory = g_settings.getList('terminal-history')
commandTextEdit = terminalWindow:getChildById('commandTextEdit') commandTextEdit = terminalWindow:getChildById('commandTextEdit')
commandTextEdit:setHeight(commandTextEdit.baseHeight)
connect(commandTextEdit, {onTextChange = onCommandChange})
g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit) g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit)
g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit) g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit)
g_keyboard.bindKeyPress('Ctrl+C', g_keyboard.bindKeyPress('Ctrl+C',
@ -138,6 +157,7 @@ function init()
return true return true
end, commandTextEdit) end, commandTextEdit)
g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit) g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit)
g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit)
g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit) g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit)
g_keyboard.bindKeyDown('Escape', hide, terminalWindow) g_keyboard.bindKeyDown('Escape', hide, terminalWindow)
@ -293,7 +313,7 @@ function addLine(text, color)
end end
function executeCommand(command) function executeCommand(command)
if command == nil or #command == 0 then return end if command == nil or #string.gsub(command, '\n', '') == 0 then return end
-- add command line -- add command line
addLine("> " .. command, "#ffffff") addLine("> " .. command, "#ffffff")

View File

@ -47,7 +47,7 @@ UIWindow
anchors.left: parent.left anchors.left: parent.left
anchors.right: terminalScroll.left anchors.right: terminalScroll.left
anchors.top: terminalScroll.top anchors.top: terminalScroll.top
anchors.bottom: commandSymbolLabel.top anchors.bottom: commandTextEdit.top
layout: layout:
type: verticalBox type: verticalBox
align-bottom: true align-bottom: true
@ -80,14 +80,25 @@ UIWindow
UITextEdit UITextEdit
id: commandTextEdit id: commandTextEdit
height: 12 background: #aaaaaa11
border-color: #aaaaaa88
&baseHeight: 12
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: commandSymbolLabel.right anchors.left: commandSymbolLabel.right
anchors.right: parent.right anchors.right: terminalScroll.left
margin-left: 1 margin-left: 1
padding-left: 2
font: terminus-10px font: terminus-10px
selection-color: black selection-color: black
selection-background-color: white selection-background-color: white
border-width-left: 0
border-width-top: 0
multiline: false
$on:
border-width-left: 1
border-width-top: 1
multiline: true
ResizeBorder ResizeBorder
id: bottomResizeBorder id: bottomResizeBorder

View File

@ -17,19 +17,19 @@ local function updateMargins(tabBar, ignored)
end end
local function updateNavigation(tabBar) local function updateNavigation(tabBar)
if prevNavigation then if tabBar.prevNavigation then
if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then
prevNavigation:enable() tabBar.prevNavigation:enable()
else else
prevNavigation:disable() tabBar.prevNavigation:disable()
end end
end end
if nextNavigation then if tabBar.nextNavigation then
if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then
nextNavigation:enable() tabBar.nextNavigation:enable()
else else
nextNavigation:disable() tabBar.nextNavigation:disable()
end end
end end
end end
@ -187,7 +187,7 @@ local function onTabDragMove(tab, mousePos, mouseMoved)
end end
local function tabBlink(tab, step) local function tabBlink(tab, step)
step = step or 0 local step = step or 0
tab:setOn(not tab:isOn()) tab:setOn(not tab:isOn())
removeEvent(tab.blinkEvent) removeEvent(tab.blinkEvent)
@ -218,6 +218,19 @@ function UIMoveableTabBar.create()
return tabbar return tabbar
end end
function UIMoveableTabBar:onDestroy()
if self.prevNavigation then
self.prevNavigation:disable()
end
if self.nextNavigation then
self.nextNavigation:disable()
end
self.nextNavigation = nil
self.prevNavigation = nil
end
function UIMoveableTabBar:setContentWidget(widget) function UIMoveableTabBar:setContentWidget(widget)
self.contentWidget = widget self.contentWidget = widget
if #self.tabs > 0 then if #self.tabs > 0 then
@ -467,14 +480,14 @@ function UIMoveableTabBar:getCurrentTab()
end end
function UIMoveableTabBar:setNavigation(prevButton, nextButton) function UIMoveableTabBar:setNavigation(prevButton, nextButton)
prevNavigation = prevButton self.prevNavigation = prevButton
nextNavigation = nextButton self.nextNavigation = nextButton
if prevNavigation then if self.prevNavigation then
prevNavigation.onClick = function() self:selectPrevTab() end self.prevNavigation.onClick = function() self:selectPrevTab() end
end end
if nextNavigation then if self.nextNavigation then
nextNavigation.onClick = function() self:selectNextTab() end self.nextNavigation.onClick = function() self:selectNextTab() end
end end
updateNavigation(self) updateNavigation(self)
end end

View File

@ -8,6 +8,7 @@ function UIPopupMenu.create()
local layout = UIVerticalLayout.create(menu) local layout = UIVerticalLayout.create(menu)
layout:setFitChildren(true) layout:setFitChildren(true)
menu:setLayout(layout) menu:setLayout(layout)
menu.isGameMenu = false
return menu return menu
end end
@ -34,6 +35,7 @@ function UIPopupMenu:display(pos)
rootWidget:addChild(self) rootWidget:addChild(self)
self:setPosition(pos) self:setPosition(pos)
self:grabMouse() self:grabMouse()
self:focus()
--self:grabKeyboard() --self:grabKeyboard()
currentMenu = self currentMenu = self
end end
@ -76,6 +78,10 @@ function UIPopupMenu:addSeparator()
g_ui.createWidget(self:getStyleName() .. 'Separator', self) g_ui.createWidget(self:getStyleName() .. 'Separator', self)
end end
function UIPopupMenu:setGameMenu(state)
self.isGameMenu = state
end
function UIPopupMenu:onDestroy() function UIPopupMenu:onDestroy()
if currentMenu == self then if currentMenu == self then
currentMenu = nil currentMenu = nil
@ -105,4 +111,12 @@ local function onRootGeometryUpdate()
currentMenu:destroy() currentMenu:destroy()
end end
end end
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate} )
local function onGameEnd()
if currentMenu and currentMenu.isGameMenu then
currentMenu:destroy()
end
end
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate })
connect(g_game, { onGameEnd = onGameEnd } )

View File

@ -12,7 +12,7 @@ function UISpinBox.create()
spinbox.step = 1 spinbox.step = 1
spinbox.firstchange = true spinbox.firstchange = true
spinbox.mouseScroll = true spinbox.mouseScroll = true
spinbox:setText("0") spinbox:setText("1")
spinbox:setValue(1) spinbox:setValue(1)
return spinbox return spinbox
end end
@ -23,7 +23,7 @@ function UISpinBox:onSetup()
end end
function UISpinBox:onMouseWheel(mousePos, direction) function UISpinBox:onMouseWheel(mousePos, direction)
if not self.mouseScroll then if not self.mouseScroll then
return false return false
end end
if direction == MouseWheelUp then if direction == MouseWheelUp then
@ -66,7 +66,15 @@ function UISpinBox:onTextChange(text, oldText)
end end
function UISpinBox:onValueChange(value) function UISpinBox:onValueChange(value)
-- nothing todo -- nothing to do
end
function UISpinBox:onFocusChange(focused)
if not focused then
if self:getText():len() == 0 then
self:setText(self.minimum)
end
end
end end
function UISpinBox:onStyleApply(styleName, styleNode) function UISpinBox:onStyleApply(styleName, styleNode)
@ -109,14 +117,16 @@ function UISpinBox:down()
self:setValue(self.value - self.step) self:setValue(self.value - self.step)
end end
function UISpinBox:setValue(value) function UISpinBox:setValue(value, dontSignal)
value = value or 0 value = value or 0
value = math.max(math.min(self.maximum, value), self.minimum) value = math.max(math.min(self.maximum, value), self.minimum)
if value == self.value then return end if value == self.value then return end
self.value = value
if self:getText():len() > 0 then if self:getText():len() > 0 then
self:setText(value) self:setText(value)
end end
self.value = value
local upButton = self:getChildById('up') local upButton = self:getChildById('up')
local downButton = self:getChildById('down') local downButton = self:getChildById('down')
@ -127,7 +137,9 @@ function UISpinBox:setValue(value)
downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum) downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum)
end end
signalcall(self.onValueChange, self, value) if not dontSignal then
signalcall(self.onValueChange, self, value)
end
end end
function UISpinBox:getValue() function UISpinBox:getValue()

View File

@ -38,6 +38,7 @@ function UITabBar:addTab(text, panel, icon)
end end
local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel) local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel)
panel.isTab = true panel.isTab = true
tab.tabPanel = panel tab.tabPanel = panel
tab.tabBar = self tab.tabBar = self

View File

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

View File

@ -313,7 +313,7 @@ function signalcall(param, ...)
perror(ret) perror(ret)
end end
end end
elseif func ~= nil then elseif param ~= nil then
error('attempt to call a non function value') error('attempt to call a non function value')
end end
return false return false

View File

@ -15,7 +15,8 @@ fightModeRadioGroup = nil
pvpModeRadioGroup = nil pvpModeRadioGroup = nil
function init() function init()
combatControlsButton = modules.client_topmenu.addRightGameToggleButton('combatControlsButton', tr('Combat Controls'), '/images/topbuttons/combatcontrols', toggle) combatControlsButton = modules.client_topmenu.addRightGameToggleButton('combatControlsButton',
tr('Combat Controls'), '/images/topbuttons/combatcontrols', toggle)
combatControlsButton:setOn(true) combatControlsButton:setOn(true)
combatControlsWindow = g_ui.loadUI('combatcontrols', modules.game_interface.getRightPanel()) combatControlsWindow = g_ui.loadUI('combatcontrols', modules.game_interface.getRightPanel())
combatControlsWindow:disableResize() combatControlsWindow:disableResize()

View File

@ -3,7 +3,7 @@ SpeakTypesSettings = {
say = { speakType = MessageModes.Say, color = '#FFFF00' }, say = { speakType = MessageModes.Say, color = '#FFFF00' },
whisper = { speakType = MessageModes.Whisper, color = '#FFFF00' }, whisper = { speakType = MessageModes.Whisper, color = '#FFFF00' },
yell = { speakType = MessageModes.Yell, color = '#FFFF00' }, yell = { speakType = MessageModes.Yell, color = '#FFFF00' },
broadcast = { speakType = MessageModes.GamemasterPrivateFrom, color = '#F55E5E' }, broadcast = { speakType = MessageModes.GamemasterBroadcast, color = '#F55E5E' },
private = { speakType = MessageModes.PrivateTo, color = '#5FF7F7', private = true }, private = { speakType = MessageModes.PrivateTo, color = '#5FF7F7', private = true },
privateRed = { speakType = MessageModes.GamemasterTo, color = '#F55E5E', private = true }, privateRed = { speakType = MessageModes.GamemasterTo, color = '#F55E5E', private = true },
privatePlayerToPlayer = { speakType = MessageModes.PrivateTo, color = '#9F9DFD', private = true }, privatePlayerToPlayer = { speakType = MessageModes.PrivateTo, color = '#9F9DFD', private = true },
@ -164,12 +164,18 @@ function enableChat()
g_keyboard.unbindKeyUp("Space") g_keyboard.unbindKeyUp("Space")
g_keyboard.unbindKeyUp("Enter") g_keyboard.unbindKeyUp("Enter")
g_keyboard.unbindKeyUp("Escape")
gameInterface.unbindWalkKey("W") gameInterface.unbindWalkKey("W")
gameInterface.unbindWalkKey("D") gameInterface.unbindWalkKey("D")
gameInterface.unbindWalkKey("S") gameInterface.unbindWalkKey("S")
gameInterface.unbindWalkKey("A") gameInterface.unbindWalkKey("A")
gameInterface.unbindWalkKey("E")
gameInterface.unbindWalkKey("Q")
gameInterface.unbindWalkKey("C")
gameInterface.unbindWalkKey("Z")
consoleToggleChat:setTooltip(tr("Disable chat mode, allow to walk using ASDW")) consoleToggleChat:setTooltip(tr("Disable chat mode, allow to walk using ASDW"))
end end
@ -187,12 +193,18 @@ function disableChat()
end end
g_keyboard.bindKeyUp("Space", quickFunc) g_keyboard.bindKeyUp("Space", quickFunc)
g_keyboard.bindKeyUp("Enter", quickFunc) g_keyboard.bindKeyUp("Enter", quickFunc)
g_keyboard.bindKeyUp("Escape", quickFunc)
gameInterface.bindWalkKey("W", North) gameInterface.bindWalkKey("W", North)
gameInterface.bindWalkKey("D", East) gameInterface.bindWalkKey("D", East)
gameInterface.bindWalkKey("S", South) gameInterface.bindWalkKey("S", South)
gameInterface.bindWalkKey("A", West) gameInterface.bindWalkKey("A", West)
gameInterface.bindWalkKey("E", NorthEast)
gameInterface.bindWalkKey("Q", NorthWest)
gameInterface.bindWalkKey("C", SouthEast)
gameInterface.bindWalkKey("Z", SouthWest)
consoleToggleChat:setTooltip(tr("Enable chat mode")) consoleToggleChat:setTooltip(tr("Enable chat mode"))
end end
@ -233,7 +245,13 @@ function terminate()
violationWindow:destroy() violationWindow:destroy()
end end
consoleTabBar = nil
consoleContentPanel = nil
consoleToggleChat = nil
consoleTextEdit = nil
consolePanel:destroy() consolePanel:destroy()
consolePanel = nil
ownPrivateName = nil ownPrivateName = nil
Console = nil Console = nil
@ -288,11 +306,14 @@ function clear()
channels = {} channels = {}
consoleTabBar:removeTab(defaultTab) consoleTabBar:removeTab(defaultTab)
defaultTab = nil
consoleTabBar:removeTab(serverTab) consoleTabBar:removeTab(serverTab)
serverTab = nil
local npcTab = consoleTabBar:getTab('NPCs') local npcTab = consoleTabBar:getTab('NPCs')
if npcTab then if npcTab then
consoleTabBar:removeTab(npcTab) consoleTabBar:removeTab(npcTab)
npcTab = nil
end end
if violationReportTab then if violationReportTab then
@ -578,6 +599,7 @@ end
function processChannelTabMenu(tab, mousePos, mouseButton) function processChannelTabMenu(tab, mousePos, mouseButton)
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
channelName = tab:getText() channelName = tab:getText()
if tab ~= defaultTab and tab ~= serverTab then if tab ~= defaultTab and tab ~= serverTab then
@ -597,6 +619,7 @@ end
function processMessageMenu(mousePos, mouseButton, creatureName, text, label, tab) function processMessageMenu(mousePos, mouseButton, creatureName, text, label, tab)
if mouseButton == MouseRightButton then if mouseButton == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
if creatureName and #creatureName > 0 then if creatureName and #creatureName > 0 then
if creatureName ~= g_game.getCharacterName() then if creatureName ~= g_game.getCharacterName() then
menu:addOption(tr('Message to ' .. creatureName), function () g_game.openPrivateChannel(creatureName) end) menu:addOption(tr('Message to ' .. creatureName), function () g_game.openPrivateChannel(creatureName) end)
@ -687,7 +710,7 @@ function sendMessage(message, tab)
end end
-- player used whisper -- player used whisper
local chatCommandMessage = message:match("^%#[w|W] (.*)") chatCommandMessage = message:match("^%#[w|W] (.*)")
if chatCommandMessage ~= nil then if chatCommandMessage ~= nil then
chatCommandSayMode = 'whisper' chatCommandSayMode = 'whisper'
message = chatCommandMessage message = chatCommandMessage
@ -695,12 +718,27 @@ function sendMessage(message, tab)
end end
-- player say -- player say
local chatCommandMessage = message:match("^%#[s|S] (.*)") chatCommandMessage = message:match("^%#[s|S] (.*)")
if chatCommandMessage ~= nil then if chatCommandMessage ~= nil then
chatCommandSayMode = 'say' chatCommandSayMode = 'say'
message = chatCommandMessage message = chatCommandMessage
channel = 0 channel = 0
end end
-- player red talk on channel
chatCommandMessage = message:match("^%#[c|C] (.*)")
if chatCommandMessage ~= nil then
chatCommandSayMode = 'channelRed'
message = chatCommandMessage
end
-- player broadcast
chatCommandMessage = message:match("^%#[b|B] (.*)")
if chatCommandMessage ~= nil then
chatCommandSayMode = 'broadcast'
message = chatCommandMessage
channel = 0
end
local findIni, findEnd, chatCommandInitial, chatCommandPrivate, chatCommandEnd, chatCommandMessage = message:find("([%*%@])(.+)([%*%@])(.*)") local findIni, findEnd, chatCommandInitial, chatCommandPrivate, chatCommandEnd, chatCommandMessage = message:find("([%*%@])(.+)([%*%@])(.*)")
if findIni ~= nil and findIni == 1 then -- player used private chat command if findIni ~= nil and findIni == 1 then -- player used private chat command

View File

@ -442,7 +442,10 @@ end
function createThingMenu(menuPosition, lookThing, useThing, creatureThing) function createThingMenu(menuPosition, lookThing, useThing, creatureThing)
if not g_game.isOnline() then return end if not g_game.isOnline() then return end
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
local classic = modules.client_options.getOption('classicControl') local classic = modules.client_options.getOption('classicControl')
local shortcut = nil local shortcut = nil

View File

@ -30,5 +30,6 @@ Module
- game_spelllist - game_spelllist
- game_cooldown - game_cooldown
- game_modaldialog - game_modaldialog
- game_unjustifiedpoints
@onLoad: init() @onLoad: init()
@onUnload: terminate() @onUnload: terminate()

View File

@ -17,7 +17,10 @@ inventoryButton = nil
purseButton = nil purseButton = nil
function init() function init()
connect(LocalPlayer, { onInventoryChange = onInventoryChange }) connect(LocalPlayer, {
onInventoryChange = onInventoryChange,
onBlessingsChange = onBlessingsChange
})
connect(g_game, { onGameStart = refresh }) connect(g_game, { onGameStart = refresh })
g_keyboard.bindKeyDown('Ctrl+I', toggle) g_keyboard.bindKeyDown('Ctrl+I', toggle)
@ -43,7 +46,10 @@ function init()
end end
function terminate() function terminate()
disconnect(LocalPlayer, { onInventoryChange = onInventoryChange }) disconnect(LocalPlayer, {
onInventoryChange = onInventoryChange,
onBlessingsChange = onBlessingsChange
})
disconnect(g_game, { onGameStart = refresh }) disconnect(g_game, { onGameStart = refresh })
g_keyboard.unbindKeyDown('Ctrl+I') g_keyboard.unbindKeyDown('Ctrl+I')
@ -52,6 +58,15 @@ function terminate()
inventoryButton:destroy() inventoryButton:destroy()
end end
function toggleAdventurerStyle(hasBlessing)
for slot = InventorySlotFirst, InventorySlotLast do
local itemWidget = inventoryPanel:getChildById('slot' .. slot)
if itemWidget then
itemWidget:setOn(hasBlessing)
end
end
end
function refresh() function refresh()
local player = g_game.getLocalPlayer() local player = g_game.getLocalPlayer()
for i = InventorySlotFirst, InventorySlotPurse do for i = InventorySlotFirst, InventorySlotPurse do
@ -60,6 +75,7 @@ function refresh()
else else
onInventoryChange(player, i, nil) onInventoryChange(player, i, nil)
end end
toggleAdventurerStyle(player and Bit.hasBit(player:getBlessings(), Blessings.Adventurer) or false)
end end
purseButton:setVisible(g_game.getFeature(GamePurseSlot)) purseButton:setVisible(g_game.getFeature(GamePurseSlot))
@ -92,10 +108,17 @@ function onInventoryChange(player, slot, item, oldItem)
local itemWidget = inventoryPanel:getChildById('slot' .. slot) local itemWidget = inventoryPanel:getChildById('slot' .. slot)
if item then if item then
itemWidget:setStyle('Item') itemWidget:setStyle('InventoryItem')
itemWidget:setItem(item) itemWidget:setItem(item)
else else
itemWidget:setStyle(InventorySlotStyles[slot]) itemWidget:setStyle(InventorySlotStyles[slot])
itemWidget:setItem(nil) itemWidget:setItem(nil)
end end
end end
function onBlessingsChange(player, blessings, oldBlessings)
local hasAdventurerBlessing = Bit.hasBit(blessings, Blessings.Adventurer)
if hasAdventurerBlessing ~= Bit.hasBit(oldBlessings, Blessings.Adventurer) then
toggleAdventurerStyle(hasAdventurerBlessing)
end
end

View File

@ -1,54 +1,76 @@
InventoryItem < Item InventoryItem < Item
$on:
image-source: /images/ui/item-blessed
HeadSlot < InventoryItem HeadSlot < InventoryItem
id: slot1 id: slot1
image-source: /images/game/slots/head image-source: /images/game/slots/head
&position: {x=65535, y=1, z=0} &position: {x=65535, y=1, z=0}
$on:
image-source: /images/game/slots/head-blessed
BodySlot < InventoryItem BodySlot < InventoryItem
id: slot4 id: slot4
image-source: /images/game/slots/body image-source: /images/game/slots/body
&position: {x=65535, y=4, z=0} &position: {x=65535, y=4, z=0}
$on:
image-source: /images/game/slots/body-blessed
LegSlot < InventoryItem LegSlot < InventoryItem
id: slot7 id: slot7
image-source: /images/game/slots/legs image-source: /images/game/slots/legs
&position: {x=65535, y=7, z=0} &position: {x=65535, y=7, z=0}
$on:
image-source: /images/game/slots/legs-blessed
FeetSlot < InventoryItem FeetSlot < InventoryItem
id: slot8 id: slot8
image-source: /images/game/slots/feet image-source: /images/game/slots/feet
&position: {x=65535, y=8, z=0} &position: {x=65535, y=8, z=0}
$on:
image-source: /images/game/slots/feet-blessed
NeckSlot < InventoryItem NeckSlot < InventoryItem
id: slot2 id: slot2
image-source: /images/game/slots/neck image-source: /images/game/slots/neck
&position: {x=65535, y=2, z=0} &position: {x=65535, y=2, z=0}
$on:
image-source: /images/game/slots/neck-blessed
LeftSlot < InventoryItem LeftSlot < InventoryItem
id: slot6 id: slot6
image-source: /images/game/slots/left-hand image-source: /images/game/slots/left-hand
&position: {x=65535, y=6, z=0} &position: {x=65535, y=6, z=0}
$on:
image-source: /images/game/slots/left-hand-blessed
FingerSlot < InventoryItem FingerSlot < InventoryItem
id: slot9 id: slot9
image-source: /images/game/slots/finger image-source: /images/game/slots/finger
&position: {x=65535, y=9, z=0} &position: {x=65535, y=9, z=0}
$on:
image-source: /images/game/slots/finger-blessed
BackSlot < InventoryItem BackSlot < InventoryItem
id: slot3 id: slot3
image-source: /images/game/slots/back image-source: /images/game/slots/back
&position: {x=65535, y=3, z=0} &position: {x=65535, y=3, z=0}
$on:
image-source: /images/game/slots/back-blessed
RightSlot < InventoryItem RightSlot < InventoryItem
id: slot5 id: slot5
image-source: /images/game/slots/right-hand image-source: /images/game/slots/right-hand
&position: {x=65535, y=5, z=0} &position: {x=65535, y=5, z=0}
$on:
image-source: /images/game/slots/right-hand-blessed
AmmoSlot < InventoryItem AmmoSlot < InventoryItem
id: slot10 id: slot10
image-source: /images/game/slots/ammo image-source: /images/game/slots/ammo
&position: {x=65535, y=10, z=0} &position: {x=65535, y=10, z=0}
$on:
image-source: /images/game/slots/ammo-blessed
PurseButton < Button PurseButton < Button
id: purseButton id: purseButton

View File

@ -13,12 +13,8 @@
* Clean up the interface building * Clean up the interface building
- Add a new market interface file to handle building? - Add a new market interface file to handle building?
* Add offer table column ordering.
- Player Name, Amount, Total Price, Peice Price and Ends At
* Extend information features * Extend information features
- Hover over offers for purchase information (balance after transaction, etc) - Hover over offers for purchase information (balance after transaction, etc)
- Display out of trend market offers based on their previous statistics (like cipsoft does)
]] ]]
Market = {} Market = {}
@ -75,17 +71,10 @@ information = {}
currentItems = {} currentItems = {}
lastCreatedOffer = 0 lastCreatedOffer = 0
fee = 0 fee = 0
averagePrice = 0
loaded = false loaded = false
local offerTableHeader = {
{['text'] = 'Player Name', ['width'] = 100},
{['text'] = 'Amount', ['width'] = 60},
{['text'] = 'Total Price', ['width'] = 90},
{['text'] = 'Piece Price', ['width'] = 80},
{['text'] = 'Ends at', ['width'] = 120}
}
local function isItemValid(item, category, searchFilter) local function isItemValid(item, category, searchFilter)
if not item or not item.marketData then if not item or not item.marketData then
return false return false
@ -110,7 +99,7 @@ local function isItemValid(item, category, searchFilter)
local filterDepot = filterButtons[MarketFilters.Depot]:isChecked() local filterDepot = filterButtons[MarketFilters.Depot]:isChecked()
if slotFilter then if slotFilter then
if slotFilter ~= 255 and item.ptr:getClothSlot() ~= slotFilter then if slotFilter ~= 255 and item.thingType:getClothSlot() ~= slotFilter then
return false return false
end end
end end
@ -124,7 +113,7 @@ local function isItemValid(item, category, searchFilter)
return false return false
end end
end end
if filterDepot and Market.depotContains(item.ptr:getId()) <= 0 then if filterDepot and Market.getDepotCount(item.marketData.tradeAs) <= 0 then
return false return false
end end
if searchFilter then if searchFilter then
@ -147,15 +136,15 @@ end
local function clearFilters() local function clearFilters()
for _, filter in pairs(filterButtons) do for _, filter in pairs(filterButtons) do
if filter and filter:isChecked() then if filter and filter:isChecked() ~= filter.default then
filter:setChecked(false) filter:setChecked(filter.default)
end end
end end
end end
local function clearFee() local function clearFee()
feeLabel:setText('') feeLabel:setText('')
fee = 0 fee = 20
end end
local function refreshTypeList() local function refreshTypeList()
@ -163,13 +152,13 @@ local function refreshTypeList()
offerTypeList:addOption('Buy') offerTypeList:addOption('Buy')
if Market.isItemSelected() then if Market.isItemSelected() then
if Market.depotContains(selectedItem.item.ptr:getId()) > 0 then if Market.getDepotCount(selectedItem.item.marketData.tradeAs) > 0 then
offerTypeList:addOption('Sell') offerTypeList:addOption('Sell')
end end
end end
end end
local function addOffer(offer, type) local function addOffer(offer, offerType)
if not offer then if not offer then
return false return false
end end
@ -179,26 +168,57 @@ local function addOffer(offer, type)
local price = offer:getPrice() local price = offer:getPrice()
local timestamp = offer:getTimeStamp() local timestamp = offer:getTimeStamp()
buyOfferTable:toggleSorting(false)
sellOfferTable:toggleSorting(false)
if amount < 1 then return false end if amount < 1 then return false end
if type == MarketAction.Buy then if offerType == MarketAction.Buy then
local data = { local data = {
{['text'] = player, ['width'] = 100}, {text = player},
{['text'] = amount, ['width'] = 60}, {text = amount},
{['text'] = price*amount, ['width'] = 90}, {text = price*amount},
{['text'] = price, ['width'] = 80}, {text = price},
{['text'] = string.gsub(os.date('%c', timestamp), " ", " "), ['width'] = 120} {text = string.gsub(os.date('%c', timestamp), " ", " ")}
} }
buyOfferTable:addRow(data, id)
if offer.warn then
buyOfferTable:setColumnStyle('OfferTableWarningColumn', true)
end
local row = buyOfferTable:addRow(data)
row.ref = id
if offer.warn then
row:setTooltip(tr('This offer is 25%% below the average market price'))
buyOfferTable:setColumnStyle('OfferTableColumn', true)
end
else else
local data = { local data = {
{['text'] = player, ['width'] = 100}, {text = player},
{['text'] = amount, ['width'] = 60}, {text = amount},
{['text'] = price*amount, ['width'] = 90}, {text = price*amount},
{['text'] = price, ['width'] = 80}, {text = price},
{['text'] = string.gsub(os.date('%c', timestamp), " ", " "), ['width'] = 120} {text = string.gsub(os.date('%c', timestamp), " ", " "), sortvalue = timestamp}
} }
sellOfferTable:addRow(data, id)
if offer.warn then
sellOfferTable:setColumnStyle('OfferTableWarningColumn', true)
end
local row = sellOfferTable:addRow(data)
row.ref = id
if offer.warn then
row:setTooltip(tr('This offer is 25%% above the average market price'))
sellOfferTable:setColumnStyle('OfferTableColumn', true)
end
end end
buyOfferTable:toggleSorting(false)
sellOfferTable:toggleSorting(false)
buyOfferTable:sort()
sellOfferTable:sort()
return true return true
end end
@ -206,12 +226,17 @@ local function mergeOffer(offer)
if not offer then if not offer then
return false return false
end end
local id = offer:getId() local id = offer:getId()
local type = offer:getType() local offerType = offer:getType()
local amount = offer:getAmount() local amount = offer:getAmount()
local replaced = false local replaced = false
if type == MarketAction.Buy then if offerType == MarketAction.Buy then
if averagePrice > 0 then
offer.warn = offer:getPrice() <= averagePrice - math.floor(averagePrice / 4)
end
for i = 1, #marketOffers[MarketAction.Buy] do for i = 1, #marketOffers[MarketAction.Buy] do
local o = marketOffers[MarketAction.Buy][i] local o = marketOffers[MarketAction.Buy][i]
-- replace existing offer -- replace existing offer
@ -224,6 +249,10 @@ local function mergeOffer(offer)
table.insert(marketOffers[MarketAction.Buy], offer) table.insert(marketOffers[MarketAction.Buy], offer)
end end
else else
if averagePrice > 0 then
offer.warn = offer:getPrice() >= averagePrice + math.floor(averagePrice / 4)
end
for i = 1, #marketOffers[MarketAction.Sell] do for i = 1, #marketOffers[MarketAction.Sell] do
local o = marketOffers[MarketAction.Sell][i] local o = marketOffers[MarketAction.Sell][i]
-- replace existing offer -- replace existing offer
@ -250,7 +279,9 @@ local function updateOffers(offers)
-- clear existing offer data -- clear existing offer data
buyOfferTable:clearData() buyOfferTable:clearData()
buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
sellOfferTable:clearData() sellOfferTable:clearData()
sellOfferTable:setSorting(4, TABLE_SORTING_ASC)
sellButton:setEnabled(false) sellButton:setEnabled(false)
buyButton:setEnabled(false) buyButton:setEnabled(false)
@ -274,8 +305,8 @@ local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
detailsTable:clearData() detailsTable:clearData()
for k, desc in pairs(descriptions) do for k, desc in pairs(descriptions) do
local columns = { local columns = {
{['text'] = getMarketDescriptionName(desc[1])..':'}, {text = getMarketDescriptionName(desc[1])..':'},
{['text'] = desc[2], ['width'] = 330} {text = desc[2]}
} }
detailsTable:addRow(columns) detailsTable:addRow(columns)
end end
@ -283,11 +314,13 @@ local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
-- update sale item statistics -- update sale item statistics
sellStatsTable:clearData() sellStatsTable:clearData()
if table.empty(saleStats) then if table.empty(saleStats) then
sellStatsTable:addRow({{['text'] = 'No information'}}) sellStatsTable:addRow({{text = 'No information'}})
else else
local offerAmount = 0
local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0 local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
for _, stat in pairs(saleStats) do for _, stat in pairs(saleStats) do
if not stat:isNull() then if not stat:isNull() then
offerAmount = offerAmount + 1
transactions = transactions + stat:getTransactions() transactions = transactions + stat:getTransactions()
totalPrice = totalPrice + stat:getTotalPrice() totalPrice = totalPrice + stat:getTotalPrice()
local newHigh = stat:getHighestPrice() local newHigh = stat:getHighestPrice()
@ -301,28 +334,30 @@ local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
end end
end end
end end
sellStatsTable:addRow({{['text'] = 'Total Transations:'},
{['text'] = transactions, ['width'] = 270}})
sellStatsTable:addRow({{['text'] = 'Highest Price:'}, if offerAmount >= 5 and transactions >= 10 then
{['text'] = highestPrice, ['width'] = 270}}) averagePrice = math.round(totalPrice / transactions)
if totalPrice > 0 and transactions > 0 then
sellStatsTable:addRow({{['text'] = 'Average Price:'},
{['text'] = math.floor(totalPrice/transactions), ['width'] = 270}})
else else
sellStatsTable:addRow({{['text'] = 'Average Price:'}, averagePrice = 0
{['text'] = 0, ['width'] = 270}})
end end
sellStatsTable:addRow({{['text'] = 'Lowest Price:'}, sellStatsTable:addRow({{text = 'Total Transations:'}, {text = transactions}})
{['text'] = lowestPrice, ['width'] = 270}}) sellStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}})
if totalPrice > 0 and transactions > 0 then
sellStatsTable:addRow({{text = 'Average Price:'},
{text = math.floor(totalPrice/transactions)}})
else
sellStatsTable:addRow({{text = 'Average Price:'}, {text = 0}})
end
sellStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}})
end end
-- update buy item statistics -- update buy item statistics
buyStatsTable:clearData() buyStatsTable:clearData()
if table.empty(purchaseStats) then if table.empty(purchaseStats) then
buyStatsTable:addRow({{['text'] = 'No information'}}) buyStatsTable:addRow({{text = 'No information'}})
else else
local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0 local transactions, totalPrice, highestPrice, lowestPrice = 0, 0, 0, 0
for _, stat in pairs(purchaseStats) do for _, stat in pairs(purchaseStats) do
@ -340,22 +375,18 @@ local function updateDetails(itemId, descriptions, purchaseStats, saleStats)
end end
end end
end end
buyStatsTable:addRow({{['text'] = 'Total Transations:'},
{['text'] = transactions, ['width'] = 270}})
buyStatsTable:addRow({{['text'] = 'Highest Price:'}, buyStatsTable:addRow({{text = 'Total Transations:'},{text = transactions}})
{['text'] = highestPrice, ['width'] = 270}}) buyStatsTable:addRow({{text = 'Highest Price:'}, {text = highestPrice}})
if totalPrice > 0 and transactions > 0 then if totalPrice > 0 and transactions > 0 then
buyStatsTable:addRow({{['text'] = 'Average Price:'}, buyStatsTable:addRow({{text = 'Average Price:'},
{['text'] = math.floor(totalPrice/transactions), ['width'] = 270}}) {text = math.floor(totalPrice/transactions)}})
else else
buyStatsTable:addRow({{['text'] = 'Average Price:'}, buyStatsTable:addRow({{text = 'Average Price:'}, {text = 0}})
{['text'] = 0, ['width'] = 270}})
end end
buyStatsTable:addRow({{['text'] = 'Lowest Price:'}, buyStatsTable:addRow({{text = 'Lowest Price:'}, {text = lowestPrice}})
{['text'] = lowestPrice, ['width'] = 270}})
end end
end end
@ -365,12 +396,12 @@ local function updateSelectedItem(widget)
Market.resetCreateOffer() Market.resetCreateOffer()
if Market.isItemSelected() then if Market.isItemSelected() then
selectedItem:setItem(selectedItem.item.ptr) selectedItem:setItem(selectedItem.item.displayItem)
nameLabel:setText(selectedItem.item.marketData.name) nameLabel:setText(selectedItem.item.marketData.name)
clearOffers() clearOffers()
Market.enableCreateOffer(true) -- update offer types Market.enableCreateOffer(true) -- update offer types
MarketProtocol.sendMarketBrowse(selectedItem.item.ptr:getId()) -- send browsed msg MarketProtocol.sendMarketBrowse(selectedItem.item.marketData.tradeAs) -- send browsed msg
else else
Market.clearSelectedItem() Market.clearSelectedItem()
end end
@ -400,55 +431,69 @@ local function updateFee(price, amount)
feeLabel:resizeToText() feeLabel:resizeToText()
end end
local function openAmountWindow(callback, type, actionText) local function destroyAmountWindow()
local actionText = actionText or '' if amountWindow then
if not Market.isOfferSelected(type) then amountWindow:destroy()
amountWindow = nil
end
end
local function openAmountWindow(callback, actionType, actionText)
if not Market.isOfferSelected(actionType) then
return return
end end
amountWindow = g_ui.createWidget('AmountWindow', rootWidget) amountWindow = g_ui.createWidget('AmountWindow', rootWidget)
amountWindow:lock() amountWindow:lock()
local item = selectedOffer[type]:getItem()
local max = selectedOffer[type]:getAmount(item:getId()) local offer = selectedOffer[actionType]
if type == MarketAction.Sell then local item = offer:getItem()
local depot = Market.depotContains(item:getId())
if max > depot then local maximum = offer:getAmount()
max = depot if actionType == MarketAction.Sell then
local depot = Market.getDepotCount(item:getId())
if maximum > depot then
maximum = depot
end end
else
maximum = math.min(maximum, math.floor(information.balance / offer:getPrice()))
end
if item:isStackable() then
maximum = math.min(maximum, MarketMaxAmountStackable)
else
maximum = math.min(maximum, MarketMaxAmount)
end end
local itembox = amountWindow:getChildById('item') local itembox = amountWindow:getChildById('item')
itembox:setItemId(item:getId()) itembox:setItemId(item:getId())
itembox:setText(1)
local scrollbar = amountWindow:getChildById('amountScrollBar') local scrollbar = amountWindow:getChildById('amountScrollBar')
scrollbar:setText(tostring(selectedOffer[type]:getPrice())..'gp') scrollbar:setText(offer:getPrice()..'gp')
scrollbar:setMaximum(max)
scrollbar:setMinimum(1)
scrollbar:setValue(1)
scrollbar.onValueChange = function(widget, value) scrollbar.onValueChange = function(widget, value)
widget:setText(tostring(value*selectedOffer[type]:getPrice())..'gp') widget:setText((value*offer:getPrice())..'gp')
itembox:setText(tostring(value)) itembox:setText(value)
end end
scrollbar:setRange(1, maximum)
scrollbar:setValue(1)
local okButton = amountWindow:getChildById('buttonOk') local okButton = amountWindow:getChildById('buttonOk')
if actionText ~= '' then if actionText then
okButton:setText(actionText) okButton:setText(actionText)
end end
local okFunc = function() local okFunc = function()
local counter = selectedOffer[type]:getCounter() local counter = offer:getCounter()
local timestamp = selectedOffer[type]:getTimeStamp() local timestamp = offer:getTimeStamp()
callback(scrollbar:getValue(), timestamp, counter) callback(scrollbar:getValue(), timestamp, counter)
okButton:getParent():destroy() destroyAmountWindow()
amountWindow = nil
end end
local cancelButton = amountWindow:getChildById('buttonCancel') local cancelButton = amountWindow:getChildById('buttonCancel')
local cancelFunc = function() local cancelFunc = function()
cancelButton:getParent():destroy() destroyAmountWindow()
amountWindow = nil
end end
amountWindow.onEnter = okFunc amountWindow.onEnter = okFunc
@ -492,7 +537,7 @@ local function onSelectBuyOffer(table, selectedRow, previousSelectedRow)
for _, offer in pairs(marketOffers[MarketAction.Buy]) do for _, offer in pairs(marketOffers[MarketAction.Buy]) do
if offer:isEqual(selectedRow.ref) then if offer:isEqual(selectedRow.ref) then
selectedOffer[MarketAction.Sell] = offer selectedOffer[MarketAction.Sell] = offer
if Market.depotContains(offer:getItem():getId()) > 0 then if Market.getDepotCount(offer:getItem():getId()) > 0 then
sellButton:setEnabled(true) sellButton:setEnabled(true)
else else
sellButton:setEnabled(false) sellButton:setEnabled(false)
@ -535,74 +580,84 @@ local function onChangeSlotFilter(combobox, option)
end end
local function onChangeOfferType(combobox, option) local function onChangeOfferType(combobox, option)
local id = selectedItem.item.ptr:getId() local item = selectedItem.item
local maximum = item.thingType:isStackable() and MarketMaxAmountStackable or MarketMaxAmount
if option == 'Sell' then if option == 'Sell' then
local max = Market.depotContains(id) maximum = math.min(maximum, Market.getDepotCount(item.marketData.tradeAs))
amountEdit:setMaximum(max) amountEdit:setMaximum(maximum)
else else
amountEdit:setMaximum(999999) amountEdit:setMaximum(maximum)
end end
end end
local function onTotalPriceChange() local function onTotalPriceChange()
local totalPrice = totalPriceEdit:getValue()
local piecePrice = piecePriceEdit:getValue()
local amount = amountEdit:getValue() local amount = amountEdit:getValue()
local totalPrice = totalPriceEdit:getValue()
local piecePrice = math.floor(totalPrice/amount)
piecePriceEdit:setValue(math.floor(totalPrice/amount)) piecePriceEdit:setValue(piecePrice, true)
if Market.isItemSelected() then if Market.isItemSelected() then
updateFee(totalPrice, amount) updateFee(piecePrice, amount)
end end
end end
local function onPiecePriceChange() local function onPiecePriceChange()
local amount = amountEdit:getValue()
local totalPrice = totalPriceEdit:getValue() local totalPrice = totalPriceEdit:getValue()
local piecePrice = piecePriceEdit:getValue() local piecePrice = piecePriceEdit:getValue()
local amount = amountEdit:getValue()
totalPriceEdit:setValue(piecePrice*amount) totalPriceEdit:setValue(piecePrice*amount, true)
if Market.isItemSelected() then if Market.isItemSelected() then
updateFee(totalPrice, amount) updateFee(piecePrice, amount)
end end
end end
local function onAmountChange() local function onAmountChange()
local totalPrice = totalPriceEdit:getValue()
local piecePrice = piecePriceEdit:getValue()
local amount = amountEdit:getValue() local amount = amountEdit:getValue()
local piecePrice = piecePriceEdit:getValue()
local totalPrice = piecePrice * amount
piecePriceEdit:setValue(math.floor(totalPrice/amount)) totalPriceEdit:setValue(piecePrice*amount, true)
totalPriceEdit:setValue(piecePrice*amount)
if Market.isItemSelected() then if Market.isItemSelected() then
updateFee(totalPrice, amount) updateFee(piecePrice, amount)
end end
end end
local function initMarketItems(category) local function onMarketMessage(messageMode, message)
Market.displayMessage(message)
end
local function initMarketItems()
for c = MarketCategory.First, MarketCategory.Last do for c = MarketCategory.First, MarketCategory.Last do
marketItems[c] = {} marketItems[c] = {}
end end
-- save a list of items which are already added
local itemSet = {}
-- populate all market items -- populate all market items
local types = g_things.findThingTypeByAttr(ThingAttrMarket, 0) local types = g_things.findThingTypeByAttr(ThingAttrMarket, 0)
for i = 1, #types do for i = 1, #types do
local t = types[i] local itemType = types[i]
local newItem = Item.create(t:getId()) local item = Item.create(itemType:getId())
if newItem then if item then
local marketData = t:getMarketData() local marketData = itemType:getMarketData()
if not table.empty(marketData) then if not table.empty(marketData) and not itemSet[marketData.tradeAs] then
if marketData.category == category or category == MarketCategory.All then -- Some items use a different sprite in Market
item:setId(marketData.showAs)
-- create new item block -- create new marketItem block
local item = { local marketItem = {
ptr = newItem, displayItem = item,
thingType = itemType,
marketData = marketData marketData = marketData
} }
-- add new market item -- add new market item
table.insert(marketItems[marketData.category], item) table.insert(marketItems[marketData.category], marketItem)
end itemSet[marketData.tradeAs] = true
end end
end end
end end
@ -624,8 +679,10 @@ local function initInterface()
browsePanel = g_ui.loadUI('ui/marketoffers/browse') browsePanel = g_ui.loadUI('ui/marketoffers/browse')
selectionTabBar:addTab(tr('Browse'), browsePanel) selectionTabBar:addTab(tr('Browse'), browsePanel)
overviewPanel = g_ui.loadUI('ui/marketoffers/overview') -- Currently not used
selectionTabBar:addTab(tr('Overview'), overviewPanel) -- "Reserved for more functionality later"
--overviewPanel = g_ui.loadUI('ui/marketoffers/overview')
--selectionTabBar:addTab(tr('Overview'), overviewPanel)
displaysTabBar = marketOffersPanel:getChildById('rightTabBar') displaysTabBar = marketOffersPanel:getChildById('rightTabBar')
displaysTabBar:setContentWidget(marketOffersPanel:getChildById('rightTabContent')) displaysTabBar:setContentWidget(marketOffersPanel:getChildById('rightTabContent'))
@ -665,7 +722,6 @@ local function initInterface()
-- setup selected item -- setup selected item
nameLabel = marketOffersPanel:getChildById('nameLabel') nameLabel = marketOffersPanel:getChildById('nameLabel')
selectedItem = marketOffersPanel:getChildById('selectedItem') selectedItem = marketOffersPanel:getChildById('selectedItem')
selectedItem.item = {}
-- setup create new offer -- setup create new offer
totalPriceEdit = marketOffersPanel:getChildById('totalPriceEdit') totalPriceEdit = marketOffersPanel:getChildById('totalPriceEdit')
@ -690,6 +746,14 @@ local function initInterface()
filterButtons[MarketFilters.Depot] = browsePanel:getChildById('filterDepot') filterButtons[MarketFilters.Depot] = browsePanel:getChildById('filterDepot')
filterButtons[MarketFilters.SearchAll] = browsePanel:getChildById('filterSearchAll') filterButtons[MarketFilters.SearchAll] = browsePanel:getChildById('filterSearchAll')
-- set filter default values
clearFilters()
-- hook filters
for _, filter in pairs(filterButtons) do
filter.onCheckChange = Market.updateCurrentItems
end
searchEdit = browsePanel:getChildById('searchEdit') searchEdit = browsePanel:getChildById('searchEdit')
categoryList = browsePanel:getChildById('categoryComboBox') categoryList = browsePanel:getChildById('categoryComboBox')
subCategoryList = browsePanel:getChildById('subCategoryComboBox') subCategoryList = browsePanel:getChildById('subCategoryComboBox')
@ -722,6 +786,13 @@ local function initInterface()
sellStatsTable = itemStatsPanel:recursiveGetChildById('sellStatsTable') sellStatsTable = itemStatsPanel:recursiveGetChildById('sellStatsTable')
buyOfferTable.onSelectionChange = onSelectBuyOffer buyOfferTable.onSelectionChange = onSelectBuyOffer
sellOfferTable.onSelectionChange = onSelectSellOffer sellOfferTable.onSelectionChange = onSelectSellOffer
buyStatsTable:setColumnWidth({120, 270})
sellStatsTable:setColumnWidth({120, 270})
detailsTable:setColumnWidth({80, 330})
buyOfferTable:setSorting(4, TABLE_SORTING_DESC)
sellOfferTable:setSorting(4, TABLE_SORTING_ASC)
end end
function init() function init()
@ -734,6 +805,8 @@ function init()
offerExhaust[MarketAction.Sell] = 10 offerExhaust[MarketAction.Sell] = 10
offerExhaust[MarketAction.Buy] = 20 offerExhaust[MarketAction.Buy] = 20
registerMessageMode(MessageModes.Market, onMarketMessage)
protocol.initProtocol() protocol.initProtocol()
connect(g_game, { onGameEnd = Market.reset }) connect(g_game, { onGameEnd = Market.reset })
connect(g_game, { onGameEnd = Market.close }) connect(g_game, { onGameEnd = Market.close })
@ -744,10 +817,15 @@ function init()
end end
function terminate() function terminate()
Market.close()
unregisterMessageMode(MessageModes.Market, onMarketMessage)
protocol.terminateProtocol() protocol.terminateProtocol()
disconnect(g_game, { onGameEnd = Market.reset }) disconnect(g_game, { onGameEnd = Market.reset })
disconnect(g_game, { onGameEnd = Market.close }) disconnect(g_game, { onGameEnd = Market.close })
destroyAmountWindow()
marketWindow:destroy() marketWindow:destroy()
Market = nil Market = nil
@ -763,9 +841,16 @@ function Market.reset()
end end
end end
function Market.displayMessage(message)
if marketWindow:isHidden() then return end
local infoBox = displayInfoBox(tr('Market Error'), message)
infoBox:lock()
end
function Market.clearSelectedItem() function Market.clearSelectedItem()
if Market.isItemSelected() then if Market.isItemSelected() then
Market.resetCreateOffer() Market.resetCreateOffer(true)
offerTypeList:clearOptions() offerTypeList:clearOptions()
offerTypeList:setText('Please Select') offerTypeList:setText('Please Select')
offerTypeList:setEnabled(false) offerTypeList:setEnabled(false)
@ -787,22 +872,15 @@ function Market.clearSelectedItem()
end end
function Market.isItemSelected() function Market.isItemSelected()
return selectedItem and not table.empty(selectedItem.item) and selectedItem.item.ptr return selectedItem and selectedItem.item
end end
function Market.isOfferSelected(type) function Market.isOfferSelected(type)
return selectedOffer[type] and not selectedOffer[type]:isNull() return selectedOffer[type] and not selectedOffer[type]:isNull()
end end
function Market.depotContains(itemId) function Market.getDepotCount(itemId)
local count = 0 return information.depotItems[itemId] or 0
for i = 1, #information.depotItems do
local item = information.depotItems[i]
if item and item.ptr:getId() == itemId then
count = count + item.ptr:getCount()
end
end
return count
end end
function Market.enableCreateOffer(enable) function Market.enableCreateOffer(enable)
@ -825,6 +903,8 @@ function Market.close(notify)
if not marketWindow:isHidden() then if not marketWindow:isHidden() then
marketWindow:hide() marketWindow:hide()
marketWindow:unlock() marketWindow:unlock()
Market.clearSelectedItem(
) Market.reset()
if notify then if notify then
MarketProtocol.sendMarketLeave() MarketProtocol.sendMarketLeave()
end end
@ -847,12 +927,17 @@ function Market.updateCurrentItems()
Market.loadMarketItems(id) Market.loadMarketItems(id)
end end
function Market.resetCreateOffer() function Market.resetCreateOffer(resetFee)
piecePriceEdit:setValue(1) piecePriceEdit:setValue(1)
totalPriceEdit:setValue(1) totalPriceEdit:setValue(1)
amountEdit:setValue(1) amountEdit:setValue(1)
refreshTypeList() refreshTypeList()
clearFee()
if resetFee then
clearFee()
else
updateFee(0, 0)
end
end end
function Market.refreshItemsWidget(selectItem) function Market.refreshItemsWidget(selectItem)
@ -877,15 +962,15 @@ function Market.refreshItemsWidget(selectItem)
itemBox.onCheckChange = Market.onItemBoxChecked itemBox.onCheckChange = Market.onItemBoxChecked
itemBox.item = item itemBox.item = item
if selectItem > 0 and item.ptr:getId() == selectItem then if selectItem > 0 and item.marketData.tradeAs == selectItem then
select = itemBox select = itemBox
selectItem = 0
end end
local itemWidget = itemBox:getChildById('item') local itemWidget = itemBox:getChildById('item')
item.ptr:setCount(1) -- reset item count for image itemWidget:setItem(item.displayItem)
itemWidget:setItem(item.ptr)
local amount = Market.depotContains(item.ptr:getId()) local amount = Market.getDepotCount(item.marketData.tradeAs)
if amount > 0 then if amount > 0 then
itemWidget:setText(amount) itemWidget:setText(amount)
itemBox:setTooltip('You have '.. amount ..' in your depot.') itemBox:setTooltip('You have '.. amount ..' in your depot.')
@ -893,8 +978,9 @@ function Market.refreshItemsWidget(selectItem)
radioItemSet:addWidget(itemBox) radioItemSet:addWidget(itemBox)
end end
if select then if select then
select:setChecked(true) radioItemSet:selectWidget(select, false)
end end
layout:enableUpdates() layout:enableUpdates()
@ -925,6 +1011,7 @@ function Market.loadMarketItems(category)
for i = 1, #marketItems[category] do for i = 1, #marketItems[category] do
local item = marketItems[category][i] local item = marketItems[category][i]
if isItemValid(item, category, searchFilter) then if isItemValid(item, category, searchFilter) then
table.insert(currentItems, item) table.insert(currentItems, item)
end end
end end
@ -942,56 +1029,6 @@ function Market.loadMarketItems(category)
Market.refreshItemsWidget() Market.refreshItemsWidget()
end end
function Market.loadDepotItems(depotItems)
information.depotItems = {}
local items = {}
for i = 1, #depotItems do
local data = depotItems[i]
local id, count = data[1], data[2]
local tmpItem = Item.create(id)
if tmpItem:isStackable() then
if count > 100 then
local createCount = math.floor(count/100)
local remainder = count % 100
if remainder > 0 then
createCount = createCount + 1
end
for i = 1, createCount do
local newItem = Item.create(id)
if i == createCount and remainder > 0 then
newItem:setCount(remainder)
else
newItem:setCount(100)
end
table.insert(items, newItem)
end
else
local newItem = Item.create(id)
newItem:setCount(count)
table.insert(items, newItem)
end
else
for i = 1, count do
table.insert(items, Item.create(id))
end
end
end
for _, newItem in pairs(items) do
local marketData = newItem:getMarketData()
if not table.empty(marketData) then
local item = {
ptr = newItem,
marketData = marketData
}
table.insert(information.depotItems, item)
end
end
end
function Market.createNewOffer() function Market.createNewOffer()
local type = offerTypeList:getCurrentOption().text local type = offerTypeList:getCurrentOption().text
if type == 'Sell' then if type == 'Sell' then
@ -1003,12 +1040,10 @@ function Market.createNewOffer()
if not Market.isItemSelected() then if not Market.isItemSelected() then
return return
end end
local item = selectedItem.item
local spriteId = item.ptr:getId() local spriteId = selectedItem.item.marketData.tradeAs
local piecePrice = piecePriceEdit:getValue() local piecePrice = piecePriceEdit:getValue()
local totalPrice = totalPriceEdit:getValue()
local amount = amountEdit:getValue() local amount = amountEdit:getValue()
local anonymous = anonymous:isChecked() and 1 or 0 local anonymous = anonymous:isChecked() and 1 or 0
@ -1019,7 +1054,10 @@ function Market.createNewOffer()
errorMsg = errorMsg..'Not enough balance to create this offer.\n' errorMsg = errorMsg..'Not enough balance to create this offer.\n'
end end
elseif type == MarketAction.Sell then elseif type == MarketAction.Sell then
if Market.depotContains(spriteId) < amount then if information.balance < fee then
errorMsg = errorMsg..'Not enough balance to create this offer.\n'
end
if Market.getDepotCount(spriteId) < amount then
errorMsg = errorMsg..'Not enough items in your depot to create this offer.\n' errorMsg = errorMsg..'Not enough items in your depot to create this offer.\n'
end end
end end
@ -1029,12 +1067,21 @@ function Market.createNewOffer()
elseif piecePrice < piecePriceEdit.minimum then elseif piecePrice < piecePriceEdit.minimum then
errorMsg = errorMsg..'Price is too low.\n' errorMsg = errorMsg..'Price is too low.\n'
end end
if amount > amountEdit.maximum then if amount > amountEdit.maximum then
errorMsg = errorMsg..'Amount is too high.\n' errorMsg = errorMsg..'Amount is too high.\n'
elseif amount < amountEdit.minimum then elseif amount < amountEdit.minimum then
errorMsg = errorMsg..'Amount is too low.\n' errorMsg = errorMsg..'Amount is too low.\n'
end end
if amount * piecePrice > MarketMaxPrice then
errorMsg = errorMsg..'Total price is too high.\n'
end
if information.totalOffers >= MarketMaxOffers then
errorMsg = errorMsg..'You cannot create more offers.\n'
end
local timeCheck = os.time() - lastCreatedOffer local timeCheck = os.time() - lastCreatedOffer
if timeCheck < offerExhaust[type] then if timeCheck < offerExhaust[type] then
local waitTime = math.ceil(offerExhaust[type] - timeCheck) local waitTime = math.ceil(offerExhaust[type] - timeCheck)
@ -1042,7 +1089,7 @@ function Market.createNewOffer()
end end
if errorMsg ~= '' then if errorMsg ~= '' then
displayInfoBox('Error', errorMsg) Market.displayMessage(errorMsg)
return return
end end
@ -1060,9 +1107,6 @@ end
function Market.onItemBoxChecked(widget) function Market.onItemBoxChecked(widget)
if widget:isChecked() then if widget:isChecked() then
if selectedItem.ref and widget ~= selectedItem.ref then
selectedItem.ref:setChecked(false) -- temporary fix?
end
updateSelectedItem(widget) updateSelectedItem(widget)
end end
end end
@ -1071,12 +1115,12 @@ end
function Market.onMarketEnter(depotItems, offers, balance, vocation) function Market.onMarketEnter(depotItems, offers, balance, vocation)
if not loaded then if not loaded then
initMarketItems(MarketCategory.All) initMarketItems()
loaded = true loaded = true
end end
Market.clearSelectedItem()
updateBalance(balance) updateBalance(balance)
averagePrice = 0
information.totalOffers = offers information.totalOffers = offers
local player = g_game.getLocalPlayer() local player = g_game.getLocalPlayer()
@ -1092,10 +1136,12 @@ function Market.onMarketEnter(depotItems, offers, balance, vocation)
information.vocation = vocation information.vocation = vocation
end end
Market.loadDepotItems(depotItems) -- set list of depot items
information.depotItems = depotItems
-- update the items widget to match depot items -- update the items widget to match depot items
if Market.isItemSelected() then if Market.isItemSelected() then
local spriteId = selectedItem.item.ptr:getId() local spriteId = selectedItem.item.marketData.tradeAs
MarketProtocol.silent(true) -- disable protocol messages MarketProtocol.silent(true) -- disable protocol messages
Market.refreshItemsWidget(spriteId) Market.refreshItemsWidget(spriteId)
MarketProtocol.silent(false) -- enable protocol messages MarketProtocol.silent(false) -- enable protocol messages
@ -1107,14 +1153,6 @@ function Market.onMarketEnter(depotItems, offers, balance, vocation)
Market.loadMarketItems(MarketCategory.First) Market.loadMarketItems(MarketCategory.First)
end end
-- build offer table header
if buyOfferTable and not buyOfferTable:hasHeader() then
buyOfferTable:addHeaderRow(offerTableHeader)
end
if sellOfferTable and not sellOfferTable:hasHeader() then
sellOfferTable:addHeaderRow(offerTableHeader)
end
if g_game.isOnline() then if g_game.isOnline() then
marketWindow:lock() marketWindow:lock()
marketWindow:show() marketWindow:show()

View File

@ -3,7 +3,7 @@ MarketWindow < MainWindow
!text: tr('Market') !text: tr('Market')
size: 700 530 size: 700 530
@onEscape: Market.close(true) @onEscape: Market.close()
// Main Panel Window // Main Panel Window
@ -54,6 +54,7 @@ MarketWindow < MainWindow
Button Button
id: resetButton id: resetButton
!text: tr('Reset Market') !text: tr('Reset Market')
!tooltip: tr('Reset selection, filters & search')
anchors.top: mainTabContent.bottom anchors.top: mainTabContent.bottom
anchors.left: mainTabContent.left anchors.left: mainTabContent.left
margin-top: 5 margin-top: 5

View File

@ -50,14 +50,14 @@ local function parseMarketEnter(protocol, msg)
vocation = msg:getU8() -- get vocation id vocation = msg:getU8() -- get vocation id
end end
local offers = msg:getU8() local offers = msg:getU8()
local depotItems = {}
local depotItems = {}
local depotCount = msg:getU16() local depotCount = msg:getU16()
for i = 1, depotCount do for i = 1, depotCount do
local itemId = msg:getU16() -- item id local itemId = msg:getU16() -- item id
local itemCount = msg:getU16() -- item count local itemCount = msg:getU16() -- item count
table.insert(depotItems, {itemId, itemCount}) depotItems[itemId] = itemCount
end end
signalcall(Market.onMarketEnter, depotItems, offers, balance, vocation) signalcall(Market.onMarketEnter, depotItems, offers, balance, vocation)

View File

@ -1,7 +1,7 @@
AmountWindow < MainWindow AmountWindow < MainWindow
id: amountWindow id: amountWindow
!text: tr('Amount') !text: tr('Amount')
size: 270 80 size: 270 90
Item Item
id: item id: item

View File

@ -4,14 +4,8 @@ MarketButtonBox < ButtonBoxRounded
size: 106 22 size: 106 22
text-offset: 0 2 text-offset: 0 2
text-align: center text-align: center
image-clip: 0 0 20 20
image-border: 2
$hover !disabled:
image-clip: 0 20 20 20
$checked: $checked:
image-clip: 0 40 20 20
color: white color: white
$disabled: $disabled:

View File

@ -10,15 +10,9 @@ MarketTabBarButton < TabBarButton
margin-left: 0 margin-left: 0
$hover !checked: $hover !checked:
image-clip: 0 20 20 20 color: #ffffff
color: white
$disabled:
image-color: #ffffff66
icon-color: #888888
$checked: $checked:
image-clip: 0 20 20 20
color: #ffffff color: #ffffff
$on !checked: $on !checked:
@ -26,36 +20,24 @@ MarketTabBarButton < TabBarButton
MarketRightTabBar < TabBar MarketRightTabBar < TabBar
MarketRightTabBarPanel < TabBarPanel MarketRightTabBarPanel < TabBarPanel
// TODO: inherit style from TabBarButton and adjust it MarketRightTabBarButton < TabBarButton
// anchors.left: none did not seem to work for me
MarketRightTabBarButton < UIButton
size: 20 25 size: 20 25
font: verdana-11px-rounded font: verdana-11px-rounded
text-offset: 0 2 text-offset: 0 2
image-source: /images/ui/tabbutton_square color: #929292
image-color: white
image-clip: 0 0 20 20
image-border: 3
icon-color: white
color: #aaaaaa
anchors.top: parent.top
padding: 5
anchors.right: prev.left
$first: $first:
anchors.right: parent.right anchors.right: parent.right
anchors.left: none
$!first:
anchors.right: prev.left
anchors.left: none
$hover !checked: $hover !checked:
image-clip: 0 20 20 20 color: #ffffff
color: white
$disabled:
image-color: #ffffff66
icon-color: #888888
$checked: $checked:
image-clip: 0 20 20 20
color: #ffffff color: #ffffff
$on !checked: $on !checked:

View File

@ -135,8 +135,7 @@ Panel
font: verdana-11px-rounded font: verdana-11px-rounded
text-offset: 0 2 text-offset: 0 2
anchors.top: offerTypeLabel.top anchors.top: offerTypeLabel.top
anchors.left: prev.right anchors.left: amountEdit.left
margin-left: 32
PreviousButton PreviousButton
id: prevAmountButton id: prevAmountButton
@ -147,7 +146,7 @@ Panel
SpinBox SpinBox
id: amountEdit id: amountEdit
anchors.verticalCenter: prev.verticalCenter anchors.top: prev.top
anchors.left: prev.right anchors.left: prev.right
margin-left: 3 margin-left: 3
width: 55 width: 55
@ -184,8 +183,6 @@ Panel
Label Label
id: feeLabel id: feeLabel
font: verdana-11px-rounded font: verdana-11px-rounded
text-offset: 0 2
anchors.top: createOfferButton.bottom anchors.top: createOfferButton.bottom
anchors.right: parent.right anchors.left: createOfferButton.left
margin-right: 8 margin: 2
margin-top: 3

View File

@ -52,7 +52,7 @@ Panel
MarketButtonBox MarketButtonBox
id: filterLevel id: filterLevel
checked: false &default: false
!text: tr('Level') !text: tr('Level')
!tooltip: tr('Filter list to match your level') !tooltip: tr('Filter list to match your level')
anchors.top: prev.bottom anchors.top: prev.bottom
@ -62,11 +62,10 @@ Panel
margin-left: 3 margin-left: 3
width: 40 width: 40
height: 20 height: 20
@onCheckChange: Market.updateCurrentItems()
MarketButtonBox MarketButtonBox
id: filterVocation id: filterVocation
checked: false &default: false
!text: tr('Voc.') !text: tr('Voc.')
!tooltip: tr('Filter list to match your vocation') !tooltip: tr('Filter list to match your vocation')
anchors.top: prev.top anchors.top: prev.top
@ -75,7 +74,6 @@ Panel
margin-left: 3 margin-left: 3
width: 34 width: 34
height: 20 height: 20
@onCheckChange: Market.updateCurrentItems()
MarketComboBox MarketComboBox
id: slotComboBox id: slotComboBox
@ -90,7 +88,7 @@ Panel
MarketButtonBox MarketButtonBox
id: filterDepot id: filterDepot
checked: false &default: false
!text: tr('Show Depot Only') !text: tr('Show Depot Only')
!tooltip: tr('Show your depot items only') !tooltip: tr('Show your depot items only')
anchors.top: prev.bottom anchors.top: prev.bottom
@ -99,7 +97,6 @@ Panel
margin-top: 6 margin-top: 6
margin-right: 3 margin-right: 3
margin-left: 3 margin-left: 3
@onCheckChange: Market.updateCurrentItems()
Panel Panel
id: itemsContainer id: itemsContainer
@ -152,11 +149,10 @@ Panel
MarketButtonBox MarketButtonBox
id: filterSearchAll id: filterSearchAll
checked: false &default: true
!text: tr('All') !text: tr('All')
!tooltip: tr('Search all items') !tooltip: tr('Search all items')
anchors.verticalCenter: prev.verticalCenter anchors.verticalCenter: prev.verticalCenter
anchors.left: prev.right anchors.left: prev.right
anchors.right: itemsContainer.right anchors.right: itemsContainer.right
margin-left: 3 margin-left: 3
@onCheckChange: Market.updateCurrentItems()

View File

@ -1,11 +1,12 @@
DetailsTableRow < TableRow DetailsTableRow < TableRow
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha
focusable: true focusable: true
color: #cccccc color: #cccccc
height: 45 height: 45
focusable: false focusable: false
padding: 2 padding: 2
even-background-color: alpha
odd-background-color: alpha
DetailsTableColumn < TableColumn DetailsTableColumn < TableColumn
font: verdana-11px-monochrome font: verdana-11px-monochrome

View File

@ -1,35 +1,27 @@
OfferTableRow < TableRow OfferTableRow < TableRow
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha
focusable: true
color: #cccccc color: #cccccc
height: 15 height: 15
$focus:
background-color: #294f6d
color: #ffffff
OfferTableColumn < TableColumn OfferTableColumn < TableColumn
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha background-color: alpha
text-offset: 5 0 text-offset: 5 0
color: #cccccc color: #cccccc
width: 80 width: 80
focusable: false
OfferTableWarningColumn < OfferTableColumn
color: #e03d3d
OfferTableHeaderRow < TableHeaderRow OfferTableHeaderRow < TableHeaderRow
font: verdana-11px-monochrome font: verdana-11px-monochrome
focusable: false
color: #cccccc color: #cccccc
height: 20 height: 20
OfferTableHeaderColumn < TableHeaderColumn OfferTableHeaderColumn < SortableTableHeaderColumn
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha
text-offset: 2 0 text-offset: 2 0
color: #cccccc color: #cccccc
width: 80
focusable: true
$focus: $focus:
background-color: #294f6d background-color: #294f6d
@ -74,8 +66,26 @@ Panel
table-data: sellingTableData table-data: sellingTableData
row-style: OfferTableRow row-style: OfferTableRow
column-style: OfferTableColumn column-style: OfferTableColumn
header-row-style: OfferTableHeaderRow header-column-style: false
header-column-style: OfferTableHeaderColumn header-row-style: false
OfferTableHeaderRow
id: header
OfferTableHeaderColumn
!text: tr('Buyer Name')
width: 100
OfferTableHeaderColumn
!text: tr('Amount')
width: 60
OfferTableHeaderColumn
!text: tr('Total Price')
width: 90
OfferTableHeaderColumn
!text: tr('Piece Price')
width: 80
OfferTableHeaderColumn
!text: tr('Auction End')
width: 120
TableData TableData
id: sellingTableData id: sellingTableData
@ -129,8 +139,26 @@ Panel
table-data: buyingTableData table-data: buyingTableData
row-style: OfferTableRow row-style: OfferTableRow
column-style: OfferTableColumn column-style: OfferTableColumn
header-row-style: OfferTableHeaderRow header-column-style: false
header-column-style: OfferTableHeaderColumn header-row-style: false
OfferTableHeaderRow
id: header
OfferTableHeaderColumn
!text: tr('Seller Name')
width: 100
OfferTableHeaderColumn
!text: tr('Amount')
width: 60
OfferTableHeaderColumn
!text: tr('Total Price')
width: 90
OfferTableHeaderColumn
!text: tr('Piece Price')
width: 80
OfferTableHeaderColumn
!text: tr('Auction End')
width: 120
TableData TableData
id: buyingTableData id: buyingTableData

View File

@ -1,6 +1,5 @@
StatsTableRow < TableRow StatsTableRow < TableRow
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha
focusable: true focusable: true
color: #cccccc color: #cccccc
height: 20 height: 20
@ -9,7 +8,7 @@ StatsTableRow < TableRow
StatsTableColumn < TableColumn StatsTableColumn < TableColumn
font: verdana-11px-monochrome font: verdana-11px-monochrome
background-color: alpha background-color: alpha
text-offset: 5 0 text-offset: 5 3
color: #cccccc color: #cccccc
width: 110 width: 110
focusable: false focusable: false

View File

@ -177,6 +177,7 @@ function itemPopup(self, mousePosition, mouseButton)
if mouseButton == MouseRightButton then if mouseButton == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
menu:addOption(tr('Look'), function() return g_game.inspectNpcTrade(self:getItem()) end) menu:addOption(tr('Look'), function() return g_game.inspectNpcTrade(self:getItem()) end)
menu:display(mousePosition) menu:display(mousePosition)
return true return true

View File

@ -25,13 +25,17 @@ mountCreature = nil
currentMount = 1 currentMount = 1
function init() function init()
connect(g_game, { onOpenOutfitWindow = create, connect(g_game, {
onGameEnd = destroy }) onOpenOutfitWindow = create,
onGameEnd = destroy
})
end end
function terminate() function terminate()
disconnect(g_game, { onOpenOutfitWindow = create, disconnect(g_game, {
onGameEnd = destroy }) onOpenOutfitWindow = create,
onGameEnd = destroy
})
destroy() destroy()
end end
@ -300,17 +304,11 @@ function updateOutfit()
addon.widget:setChecked(false) addon.widget:setChecked(false)
addon.widget:setEnabled(false) addon.widget:setEnabled(false)
end end
outfit.addons = 0
if availableAddons > 0 then if availableAddons > 0 then
for _, i in pairs(ADDON_SETS[availableAddons]) do for _, i in pairs(ADDON_SETS[availableAddons]) do
addons[i].widget:setEnabled(true) addons[i].widget:setEnabled(true)
end
end
outfit.addons = 0
for i = 1, #prevAddons do
local addon = prevAddons[i]
if addon and addons[i].widget:isEnabled() then
addons[i].widget:setChecked(true) addons[i].widget:setChecked(true)
end end
end end

View File

@ -1,10 +1,11 @@
DeathWindow < MainWindow DeathWindow < MainWindow
id: deathWindow id: deathWindow
!text: tr('You are dead') !text: tr('You are dead')
size: 350 155 &baseWidth: 350
&baseHeight: 15
Label Label
!text: tr('Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!') id: labelText
width: 550 width: 550
height: 140 height: 140
anchors.left: parent.left anchors.left: parent.left

View File

@ -1,5 +1,11 @@
deathWindow = nil deathWindow = nil
local deathTexts = {
regular = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!', height = 140, width = 0},
unfair = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nThis death penalty has been reduced by %i%%\nbecause it was an unfair fight.\n\nSimply click on Ok to resume your journeys!', height = 185, width = 0},
blessed = {text = 'Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back into this world\n\nThis death penalty has been reduced by 100%\nbecause you are blessed with the Adventurer\'s Blessing\n\nSimply click on Ok to resume your journeys!', height = 170, width = 90}
}
function init() function init()
g_ui.importStyle('deathwindow') g_ui.importStyle('deathwindow')
@ -21,9 +27,9 @@ function reset()
end end
end end
function display() function display(deathType, penalty)
displayDeadMessage() displayDeadMessage()
openWindow() openWindow(deathType, penalty)
end end
function displayDeadMessage() function displayDeadMessage()
@ -33,12 +39,31 @@ function displayDeadMessage()
modules.game_textmessage.displayGameMessage(tr('You are dead.')) modules.game_textmessage.displayGameMessage(tr('You are dead.'))
end end
function openWindow() function openWindow(deathType, penalty)
if deathWindow then if deathWindow then
deathWindow:destroy() deathWindow:destroy()
return return
end end
deathWindow = g_ui.createWidget('DeathWindow', rootWidget) deathWindow = g_ui.createWidget('DeathWindow', rootWidget)
local textLabel = deathWindow:getChildById('labelText')
if deathType == DeathType.Regular then
if penalty == 100 then
textLabel:setText(deathTexts.regular.text)
deathWindow:setHeight(deathWindow.baseHeight + deathTexts.regular.height)
deathWindow:setWidth(deathWindow.baseWidth + deathTexts.regular.width)
else
textLabel:setText(string.format(deathTexts.unfair.text, 100 - penalty))
deathWindow:setHeight(deathWindow.baseHeight + deathTexts.unfair.height)
deathWindow:setWidth(deathWindow.baseWidth + deathTexts.unfair.width)
end
elseif deathType == DeathType.Blessed then
textLabel:setText(deathTexts.blessed.text)
deathWindow:setHeight(deathWindow.baseHeight + deathTexts.blessed.height)
deathWindow:setWidth(deathWindow.baseWidth + deathTexts.blessed.width)
end
local okButton = deathWindow:getChildById('buttonOk') local okButton = deathWindow:getChildById('buttonOk')
local cancelButton = deathWindow:getChildById('buttonCancel') local cancelButton = deathWindow:getChildById('buttonCancel')

View File

@ -44,7 +44,6 @@ MessageTypes = {
[MessageModes.Party] = MessageSettings.centerGreen, [MessageModes.Party] = MessageSettings.centerGreen,
[MessageModes.PartyManagement] = MessageSettings.centerWhite, [MessageModes.PartyManagement] = MessageSettings.centerWhite,
[MessageModes.TutorialHint] = MessageSettings.centerWhite, [MessageModes.TutorialHint] = MessageSettings.centerWhite,
[MessageModes.Market] = MessageSettings.centerWhite,
[MessageModes.BeyondLast] = MessageSettings.centerWhite, [MessageModes.BeyondLast] = MessageSettings.centerWhite,
[MessageModes.Report] = MessageSettings.consoleRed, [MessageModes.Report] = MessageSettings.consoleRed,
[MessageModes.HotkeyUse] = MessageSettings.centerGreen, [MessageModes.HotkeyUse] = MessageSettings.centerGreen,
@ -55,13 +54,19 @@ MessageTypes = {
messagesPanel = nil messagesPanel = nil
function init() function init()
connect(g_game, 'onTextMessage', displayMessage) for messageMode, _ in pairs(MessageTypes) do
registerMessageMode(messageMode, displayMessage)
end
connect(g_game, 'onGameEnd', clearMessages) connect(g_game, 'onGameEnd', clearMessages)
messagesPanel = g_ui.loadUI('textmessage', modules.game_interface.getRootPanel()) messagesPanel = g_ui.loadUI('textmessage', modules.game_interface.getRootPanel())
end end
function terminate() function terminate()
disconnect(g_game, 'onTextMessage', displayMessage) for messageMode, _ in pairs(MessageTypes) do
unregisterMessageMode(messageMode, displayMessage)
end
disconnect(g_game, 'onGameEnd', clearMessages) disconnect(g_game, 'onGameEnd', clearMessages)
clearMessages() clearMessages()
messagesPanel:destroy() messagesPanel:destroy()
@ -75,9 +80,7 @@ function displayMessage(mode, text)
if not g_game.isOnline() then return end if not g_game.isOnline() then return end
local msgtype = MessageTypes[mode] local msgtype = MessageTypes[mode]
if not msgtype then if not msgtype then
perror('unhandled onTextMessage message mode ' .. mode .. ': ' .. text)
return return
end end

View File

@ -8,6 +8,7 @@ TextMessageLabel < UILabel
Panel Panel
anchors.fill: gameMapPanel anchors.fill: gameMapPanel
anchors.bottom: gameBottomPanel.top
focusable: false focusable: false
Panel Panel

View File

@ -0,0 +1,148 @@
unjustifiedPointsWindow = nil
unjustifiedPointsButton = nil
contentsPanel = nil
openPvpSituationsLabel = nil
currentSkullWidget = nil
skullTimeLabel = nil
dayProgressBar = nil
weekProgressBar = nil
monthProgressBar = nil
daySkullWidget = nil
weekSkullWidget = nil
monthSkullWidget = nil
function init()
connect(g_game, { onGameStart = online,
onUnjustifiedPointsChange = onUnjustifiedPointsChange,
onOpenPvpSituationsChange = onOpenPvpSituationsChange })
connect(LocalPlayer, { onSkullChange = onSkullChange } )
unjustifiedPointsButton = modules.client_topmenu.addRightGameToggleButton('unjustifiedPointsButton',
tr('Unjustified Points'), '/images/topbuttons/unjustifiedpoints', toggle)
unjustifiedPointsButton:setOn(true)
unjustifiedPointsButton:hide()
unjustifiedPointsWindow = g_ui.loadUI('unjustifiedpoints', modules.game_interface.getRightPanel())
unjustifiedPointsWindow:disableResize()
unjustifiedPointsWindow:setup()
contentsPanel = unjustifiedPointsWindow:getChildById('contentsPanel')
openPvpSituationsLabel = contentsPanel:getChildById('openPvpSituationsLabel')
currentSkullWidget = contentsPanel:getChildById('currentSkullWidget')
skullTimeLabel = contentsPanel:getChildById('skullTimeLabel')
dayProgressBar = contentsPanel:getChildById('dayProgressBar')
weekProgressBar = contentsPanel:getChildById('weekProgressBar')
monthProgressBar = contentsPanel:getChildById('monthProgressBar')
daySkullWidget = contentsPanel:getChildById('daySkullWidget')
weekSkullWidget = contentsPanel:getChildById('weekSkullWidget')
monthSkullWidget = contentsPanel:getChildById('monthSkullWidget')
if g_game.isOnline() then
online()
end
end
function terminate()
disconnect(g_game, { onGameStart = online,
onUnjustifiedPointsChange = onUnjustifiedPointsChange,
onOpenPvpSituationsChange = onOpenPvpSituationsChange })
disconnect(LocalPlayer, { onSkullChange = onSkullChange } )
unjustifiedPointsWindow:destroy()
unjustifiedPointsButton:destroy()
end
function onMiniWindowClose()
unjustifiedPointsButton:setOn(false)
end
function toggle()
if unjustifiedPointsButton:isOn() then
unjustifiedPointsWindow:close()
unjustifiedPointsButton:setOn(false)
else
unjustifiedPointsWindow:open()
unjustifiedPointsButton:setOn(true)
end
end
function online()
if g_game.getFeature(GameUnjustifiedPoints) then
unjustifiedPointsButton:show()
else
unjustifiedPointsButton:hide()
unjustifiedPointsWindow:close()
end
refresh()
end
function refresh()
local localPlayer = g_game.getLocalPlayer()
local unjustifiedPoints = g_game.getUnjustifiedPoints()
onUnjustifiedPointsChange(unjustifiedPoints)
onSkullChange(localPlayer, localPlayer:getSkull())
onOpenPvpSituationsChange(g_game.getOpenPvpSituations())
end
function onSkullChange(localPlayer, skull)
if not localPlayer:isLocalPlayer() then return end
if skull == SkullRed or skull == SkullBlack then
currentSkullWidget:setIcon(getSkullImagePath(skull))
currentSkullWidget:setTooltip('Remaining skull time')
else
currentSkullWidget:setIcon('')
currentSkullWidget:setTooltip('You have no skull')
end
daySkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
weekSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
monthSkullWidget:setIcon(getSkullImagePath(getNextSkullId(skull)))
end
function onOpenPvpSituationsChange(amount)
openPvpSituationsLabel:setText(amount)
end
local function getColorByKills(kills)
if kills < 2 then
return 'red'
elseif kills < 3 then
return 'yellow'
end
return 'green'
end
function onUnjustifiedPointsChange(unjustifiedPoints)
if unjustifiedPoints.skullTime == 0 then
skullTimeLabel:setText('No skull')
skullTimeLabel:setTooltip('You have no skull')
else
skullTimeLabel:setText(unjustifiedPoints.skullTime .. ' days')
skullTimeLabel:setTooltip('Remaining skull time')
end
dayProgressBar:setValue(unjustifiedPoints.killsDay, 0, 100)
dayProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsDayRemaining))
dayProgressBar:setTooltip(string.format('Unjustified points gained during the last 24 hours.\n%i kill%s left.', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's')))
dayProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsDayRemaining, (unjustifiedPoints.killsDayRemaining == 1 and '' or 's')))
weekProgressBar:setValue(unjustifiedPoints.killsWeek, 0, 100)
weekProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsWeekRemaining))
weekProgressBar:setTooltip(string.format('Unjustified points gained during the last 7 days.\n%i kill%s left.', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's')))
weekProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsWeekRemaining, (unjustifiedPoints.killsWeekRemaining == 1 and '' or 's')))
monthProgressBar:setValue(unjustifiedPoints.killsMonth, 0, 100)
monthProgressBar:setBackgroundColor(getColorByKills(unjustifiedPoints.killsMonthRemaining))
monthProgressBar:setTooltip(string.format('Unjustified points gained during the last 30 days.\n%i kill%s left.', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's')))
monthProgressBar:setText(string.format('%i kill%s left', unjustifiedPoints.killsMonthRemaining, (unjustifiedPoints.killsMonthRemaining == 1 and '' or 's')))
end

View File

@ -0,0 +1,8 @@
Module
name: game_unjustifiedpoints
description: View unjustified points
author: Summ
sandboxed: true
scripts: [ unjustifiedpoints ]
@onLoad: init()
@onUnload: terminate()

View File

@ -0,0 +1,80 @@
SkullProgressBar < ProgressBar
height: 13
margin: 4 18 0 10
anchors.top: prev.bottom
anchors.left: parent.left
anchors.right: parent.right
SkullWidget < UIWidget
size: 13 13
margin-right: 2
anchors.right: parent.right
image-source: /images/game/skull_socket
MiniWindow
id: unjustifiedPointsWindow
!text: tr('Unjustified Points')
height: 114
icon: /images/topbuttons/unjustifiedpoints
@onClose: modules.game_unjustifiedpoints.onMiniWindowClose()
&save: true
MiniWindowContents
Label
anchors.top: parent.top
anchors.left: parent.left
!text: tr('Open PvP')
!tooltip: tr('Open PvP Situations')
phantom: false
margin-top: 2
margin-left: 10
Label
id: openPvpSituationsLabel
anchors.top: prev.bottom
anchors.left: parent.left
font: verdana-11px-rounded
margin-left: 12
phantom: false
Label
anchors.top: parent.top
anchors.right: parent.right
!text: tr('Skull Time')
margin-top: 2
margin-right: 10
SkullWidget
id: currentSkullWidget
anchors.top: prev.bottom
margin-right: 10
Label
id: skullTimeLabel
anchors.top: prev.top
anchors.right: prev.left
font: verdana-11px-rounded
margin-right: 6
phantom: false
SkullProgressBar
id: dayProgressBar
margin-top: 10
SkullWidget
id: daySkullWidget
anchors.top: prev.top
SkullProgressBar
id: weekProgressBar
SkullWidget
id: weekSkullWidget
anchors.top: prev.top
SkullProgressBar
id: monthProgressBar
SkullWidget
id: monthSkullWidget
anchors.top: prev.top

View File

@ -345,6 +345,7 @@ function onVipListMousePress(widget, mousePos, mouseButton)
local vipList = vipWindow:getChildById('contentsPanel') local vipList = vipWindow:getChildById('contentsPanel')
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
menu:addOption(tr('Add new VIP'), function() createAddWindow() end) menu:addOption(tr('Add new VIP'), function() createAddWindow() end)
menu:addSeparator() menu:addSeparator()
@ -377,6 +378,7 @@ function onVipListLabelMousePress(widget, mousePos, mouseButton)
local vipList = vipWindow:getChildById('contentsPanel') local vipList = vipWindow:getChildById('contentsPanel')
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
menu:addOption(tr('Send Message'), function() g_game.openPrivateChannel(widget:getText()) end) menu:addOption(tr('Send Message'), function() g_game.openPrivateChannel(widget:getText()) end)
menu:addOption(tr('Add new VIP'), function() createAddWindow() end) menu:addOption(tr('Add new VIP'), function() createAddWindow() end)
menu:addOption(tr('Edit %s', widget:getText()), function() if widget then createEditWindow(widget) end end) menu:addOption(tr('Edit %s', widget:getText()), function() if widget then createEditWindow(widget) end end)

View File

@ -120,6 +120,17 @@ GameSpritesAlphaChannel = 56
GamePremiumExpiration = 57 GamePremiumExpiration = 57
GameBrowseField = 58 GameBrowseField = 58
GameEnhancedAnimations = 59 GameEnhancedAnimations = 59
GameOGLInformation = 60
GameMessageSizeCheck = 61
GamePreviewState = 62
GameLoginPacketEncryption = 63
GameClientVersion = 64
GameContentRevision = 65
GameExperienceBonus = 66
GameAuthenticator = 67
GameUnjustifiedPoints = 68
GameSessionKey = 69
GameDeathType = 70
TextColors = { TextColors = {
red = '#f55e5e', --'#c83200' red = '#f55e5e', --'#c83200'
@ -184,7 +195,9 @@ MessageModes = {
RVRChannel = 46, RVRChannel = 46,
RVRAnswer = 47, RVRAnswer = 47,
RVRContinue = 48, RVRContinue = 48,
Last = 49, GameHighlight = 49,
NpcFromStartBlock = 50,
Last = 51,
Invalid = 255, Invalid = 255,
} }
@ -201,7 +214,7 @@ CIPSOFT_RSA = "1321277432058722840622950990822933849527763264961655079678763618"
"88792221429527047321331896351555606801473202394175817" "88792221429527047321331896351555606801473202394175817"
-- set to the latest Tibia.pic signature to make otclient compatible with official tibia -- set to the latest Tibia.pic signature to make otclient compatible with official tibia
PIC_SIGNATURE = 0x53208400 PIC_SIGNATURE = 0x542100C1
OsTypes = { OsTypes = {
Linux = 1, Linux = 1,
@ -244,4 +257,25 @@ ExtendedIds = {
NeedsUpdate = 7 NeedsUpdate = 7
} }
PreviewState = {
Default = 0,
Inactive = 1,
Active = 2
}
Blessings = {
None = 0,
Adventurer = 1,
SpiritualShielding = 2,
EmbraceOfTibia = 4,
FireOfSuns = 8,
WisdomOfSolitude = 16,
SparkOfPhoenix = 32
}
DeathType = {
Regular = 0,
Blessed = 1
}
-- @} -- @}

View File

@ -35,6 +35,13 @@ NpcIconTradeQuest = 4
-- @} -- @}
function getNextSkullId(skullId)
if skullId == SkullRed or skullId == SkullBlack then
return SkullBlack
end
return SkullRed
end
function getSkullImagePath(skullId) function getSkullImagePath(skullId)
local path local path
if skullId == SkullYellow then if skullId == SkullYellow then

View File

@ -1,7 +1,5 @@
local currentRsa
function g_game.getRsa() function g_game.getRsa()
return currentRsa return G.currentRsa
end end
function g_game.findPlayerItem(itemId, subType) function g_game.findPlayerItem(itemId, subType)
@ -19,7 +17,7 @@ function g_game.findPlayerItem(itemId, subType)
end end
function g_game.chooseRsa(host) function g_game.chooseRsa(host)
if currentRsa ~= CIPSOFT_RSA and currentRsa ~= OTSERV_RSA then return end if G.currentRsa ~= CIPSOFT_RSA and G.currentRsa ~= OTSERV_RSA then return end
if host:ends('.tibia.com') or host:ends('.cipsoft.com') then if host:ends('.tibia.com') or host:ends('.cipsoft.com') then
g_game.setRsa(CIPSOFT_RSA) g_game.setRsa(CIPSOFT_RSA)
@ -29,7 +27,7 @@ function g_game.chooseRsa(host)
g_game.setCustomOs(OsTypes.Linux) g_game.setCustomOs(OsTypes.Linux)
end end
else else
if currentRsa == CIPSOFT_RSA then if G.currentRsa == CIPSOFT_RSA then
g_game.setCustomOs(-1) g_game.setCustomOs(-1)
end end
g_game.setRsa(OTSERV_RSA) g_game.setRsa(OTSERV_RSA)
@ -44,47 +42,49 @@ end
function g_game.setRsa(rsa, e) function g_game.setRsa(rsa, e)
e = e or '65537' e = e or '65537'
g_crypt.rsaSetPublicKey(rsa, e) g_crypt.rsaSetPublicKey(rsa, e)
currentRsa = rsa G.currentRsa = rsa
end end
function g_game.isOfficialTibia() function g_game.isOfficialTibia()
return currentRsa == CIPSOFT_RSA return G.currentRsa == CIPSOFT_RSA
end end
function g_game.getSupportedClients() function g_game.getSupportedClients()
return { return {
740, 741, 750, 760, 770, 772, 740, 741, 750, 760, 770, 772,
780, 781, 782, 790, 792, 780, 781, 782, 790, 792,
800, 810, 811, 820, 821, 822, 800, 810, 811, 820, 821, 822,
830, 831, 840, 842, 850, 853, 830, 831, 840, 842, 850, 853,
854, 855, 857, 860, 861, 862, 854, 855, 857, 860, 861, 862,
870, 871, 870, 871,
900, 910, 920, 931, 940, 943, 900, 910, 920, 931, 940, 943,
944, 951, 952, 953, 954, 960, 944, 951, 952, 953, 954, 960,
961, 963, 970, 971, 972, 973, 961, 963, 970, 971, 972, 973,
980, 981, 982, 983, 984, 985, 980, 981, 982, 983, 984, 985,
986, 986,
1000, 1001, 1002, 1010, 1011, 1000, 1001, 1002, 1010, 1011,
1012, 1013, 1020, 1021, 1022, 1012, 1013, 1020, 1021, 1022,
1030, 1031, 1032, 1033, 1034, 1030, 1031, 1032, 1033, 1034,
1035, 1036, 1037, 1038, 1039, 1035, 1036, 1037, 1038, 1039,
1040, 1041, 1050, 1051, 1052, 1040, 1041, 1050, 1051, 1052,
1053, 1054, 1055, 1056, 1057, 1053, 1054, 1055, 1056, 1057,
1058, 1059, 1060, 1061 1058, 1059, 1060, 1061, 1062,
1063, 1064, 1070, 1071, 1072,
1073, 1074, 1075, 1076
} }
end end
-- The client version and protocol version where -- The client version and protocol version where
-- unsynchronized for some releases, not sure if this -- unsynchronized for some releases, not sure if this
-- will be the normal standard. -- will be the normal standard.
-- Client Version: Publicly given version when -- Client Version: Publicly given version when
-- downloading Cipsoft client. -- downloading Cipsoft client.
-- Protocol Version: Previously was the same as -- Protocol Version: Previously was the same as
-- the client version, but was unsychronized in some -- the client version, but was unsychronized in some
-- releases, now it needs to be verified and added here -- releases, now it needs to be verified and added here
-- if it does not match the client version. -- if it does not match the client version.
@ -92,7 +92,7 @@ end
-- Reason for defining both: The server now requires a -- Reason for defining both: The server now requires a
-- Client version and Protocol version from the client. -- Client version and Protocol version from the client.
-- Important: Use getClientVersion for specific protocol -- Important: Use getClientVersion for specific protocol
-- features to ensure we are using the proper version. -- features to ensure we are using the proper version.
function g_game.getClientProtocolVersion(client) function g_game.getClientProtocolVersion(client)
@ -110,4 +110,6 @@ function g_game.getClientProtocolVersion(client)
return clients[client] or client return clients[client] or client
end end
g_game.setRsa(OTSERV_RSA) if not G.currentRsa then
g_game.setRsa(OTSERV_RSA)
end

View File

@ -19,6 +19,7 @@ Module
dofile 'creature' dofile 'creature'
dofile 'player' dofile 'player'
dofile 'market' dofile 'market'
dofile 'textmessages'
dofile 'thing' dofile 'thing'
dofile 'spells' dofile 'spells'

View File

@ -1,3 +1,8 @@
MarketMaxAmount = 2000
MarketMaxAmountStackable = 64000
MarketMaxPrice = 999999999
MarketMaxOffers = 100
MarketAction = { MarketAction = {
Buy = 0, Buy = 0,
Sell = 1 Sell = 1
@ -160,8 +165,8 @@ MarketFilters = {
SearchAll = 4 SearchAll = 4
} }
MarketFilters.First = MarketFilters.vocation MarketFilters.First = MarketFilters.Vocation
MarketFilters.Last = MarketFilters.depot MarketFilters.Last = MarketFilters.Depot
function getMarketSlotFilterId(name) function getMarketSlotFilterId(name)
local id = table.find(MarketSlotFilters, name) local id = table.find(MarketSlotFilters, name)

View File

@ -2,13 +2,20 @@
ProtocolLogin = extends(Protocol, "ProtocolLogin") ProtocolLogin = extends(Protocol, "ProtocolLogin")
LoginServerError = 10 LoginServerError = 10
LoginServerTokenSuccess = 12
LoginServerTokenError = 13
LoginServerUpdate = 17 LoginServerUpdate = 17
LoginServerMotd = 20 LoginServerMotd = 20
LoginServerUpdateNeeded = 30 LoginServerUpdateNeeded = 30
LoginServerSessionKey = 40
LoginServerCharacterList = 100 LoginServerCharacterList = 100
LoginServerExtendedCharacterList = 101 LoginServerExtendedCharacterList = 101
function ProtocolLogin:login(host, port, accountName, accountPassword) -- Since 10.76
LoginServerRetry = 10
LoginServerErrorNew = 11
function ProtocolLogin:login(host, port, accountName, accountPassword, authenticatorToken, stayLogged)
if string.len(host) == 0 or port == nil or port == 0 then if string.len(host) == 0 or port == nil or port == 0 then
signalcall(self.onLoginError, self, tr("You must enter a valid server address and port.")) signalcall(self.onLoginError, self, tr("You must enter a valid server address and port."))
return return
@ -16,6 +23,8 @@ function ProtocolLogin:login(host, port, accountName, accountPassword)
self.accountName = accountName self.accountName = accountName
self.accountPassword = accountPassword self.accountPassword = accountPassword
self.authenticatorToken = authenticatorToken
self.stayLogged = stayLogged
self.connectCallback = self.sendLoginPacket self.connectCallback = self.sendLoginPacket
self:connect(host, port) self:connect(host, port)
@ -32,23 +41,28 @@ function ProtocolLogin:sendLoginPacket()
msg:addU16(g_game.getProtocolVersion()) msg:addU16(g_game.getProtocolVersion())
if g_game.getClientVersion() >= 980 then if g_game.getFeature(GameClientVersion) then
msg:addU32(g_game.getClientVersion()) msg:addU32(g_game.getClientVersion())
end end
msg:addU32(g_things.getDatSignature()) if g_game.getFeature(GameContentRevision) then
msg:addU16(g_things.getContentRevision())
msg:addU16(0)
else
msg:addU32(g_things.getDatSignature())
end
msg:addU32(g_sprites.getSprSignature()) msg:addU32(g_sprites.getSprSignature())
msg:addU32(PIC_SIGNATURE) msg:addU32(PIC_SIGNATURE)
if g_game.getClientVersion() >= 980 then if g_game.getFeature(GamePreviewState) then
msg:addU8(0) -- clientType msg:addU8(0)
end end
local offset = msg:getMessageSize() local offset = msg:getMessageSize()
if g_game.getFeature(GameLoginPacketEncryption) then
if g_game.getClientVersion() >= 770 then
-- first RSA byte must be 0 -- first RSA byte must be 0
msg:addU8(0) msg:addU8(0)
-- xtea key -- xtea key
self:generateXteaKey() self:generateXteaKey()
local xteaKey = self:getXteaKey() local xteaKey = self:getXteaKey()
@ -73,8 +87,44 @@ function ProtocolLogin:sendLoginPacket()
local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset) local paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
assert(paddingBytes >= 0) assert(paddingBytes >= 0)
msg:addPaddingBytes(paddingBytes, 0) for i = 1, paddingBytes do
if g_game.getClientVersion() >= 770 then msg:addU8(math.random(0, 0xff))
end
if g_game.getFeature(GameLoginPacketEncryption) then
msg:encryptRsa()
end
if g_game.getFeature(GameOGLInformation) then
msg:addU8(1) --unknown
msg:addU8(1) --unknown
if g_game.getClientVersion() >= 1072 then
msg:addString(string.format('%s %s', g_graphics.getVendor(), g_graphics.getRenderer()))
else
msg:addString(g_graphics.getRenderer())
end
msg:addString(g_graphics.getVersion())
end
-- add RSA encrypted auth token
if g_game.getFeature(GameAuthenticator) then
offset = msg:getMessageSize()
-- first RSA byte must be 0
msg:addU8(0)
msg:addString(self.authenticatorToken)
if g_game.getFeature(GameSessionKey) then
msg:addU8(booleantonumber(self.stayLogged))
end
paddingBytes = g_crypt.rsaGetSize() - (msg:getMessageSize() - offset)
assert(paddingBytes >= 0)
for i = 1, paddingBytes do
msg:addU8(math.random(0, 0xff))
end
msg:encryptRsa() msg:encryptRsa()
end end
@ -83,7 +133,7 @@ function ProtocolLogin:sendLoginPacket()
end end
self:send(msg) self:send(msg)
if g_game.getClientVersion() >= 770 then if g_game.getFeature(GameLoginPacketEncryption) then
self:enableXteaEncryption() self:enableXteaEncryption()
end end
self:recv() self:recv()
@ -98,12 +148,20 @@ end
function ProtocolLogin:onRecv(msg) function ProtocolLogin:onRecv(msg)
while not msg:eof() do while not msg:eof() do
local opcode = msg:getU8() local opcode = msg:getU8()
if opcode == LoginServerError then if opcode == LoginServerErrorNew then
self:parseError(msg)
elseif opcode == LoginServerError then
self:parseError(msg) self:parseError(msg)
elseif opcode == LoginServerMotd then elseif opcode == LoginServerMotd then
self:parseMotd(msg) self:parseMotd(msg)
elseif opcode == LoginServerUpdateNeeded then elseif opcode == LoginServerUpdateNeeded then
signalcall(self.onLoginError, self, tr("Client needs update.")) signalcall(self.onLoginError, self, tr("Client needs update."))
elseif opcode == LoginServerTokenSuccess then
local unknown = msg:getU8()
elseif opcode == LoginServerTokenError then
-- TODO: prompt for token here
local unknown = msg:getU8()
signalcall(self.onLoginError, self, tr("Invalid authentification token."))
elseif opcode == LoginServerCharacterList then elseif opcode == LoginServerCharacterList then
self:parseCharacterList(msg) self:parseCharacterList(msg)
elseif opcode == LoginServerExtendedCharacterList then elseif opcode == LoginServerExtendedCharacterList then
@ -111,6 +169,8 @@ function ProtocolLogin:onRecv(msg)
elseif opcode == LoginServerUpdate then elseif opcode == LoginServerUpdate then
local signature = msg:getString() local signature = msg:getString()
signalcall(self.onUpdateNeeded, self, signature) signalcall(self.onUpdateNeeded, self, signature)
elseif opcode == LoginServerSessionKey then
self:parseSessionKey(msg)
else else
self:parseOpcode(opcode, msg) self:parseOpcode(opcode, msg)
end end
@ -128,6 +188,11 @@ function ProtocolLogin:parseMotd(msg)
signalcall(self.onMotd, self, motd) signalcall(self.onMotd, self, motd)
end end
function ProtocolLogin:parseSessionKey(msg)
local sessionKey = msg:getString()
signalcall(self.onSessionKey, self, sessionKey)
end
function ProtocolLogin:parseCharacterList(msg) function ProtocolLogin:parseCharacterList(msg)
local characters = {} local characters = {}
@ -141,7 +206,7 @@ function ProtocolLogin:parseCharacterList(msg)
world.worldName = msg:getString() world.worldName = msg:getString()
world.worldIp = msg:getString() world.worldIp = msg:getString()
world.worldPort = msg:getU16() world.worldPort = msg:getU16()
msg:getU8() -- unknow byte? world.previewState = msg:getU8()
worlds[worldId] = world worlds[worldId] = world
end end
@ -153,6 +218,7 @@ function ProtocolLogin:parseCharacterList(msg)
character.worldName = worlds[worldId].worldName character.worldName = worlds[worldId].worldName
character.worldIp = worlds[worldId].worldIp character.worldIp = worlds[worldId].worldIp
character.worldPort = worlds[worldId].worldPort character.worldPort = worlds[worldId].worldPort
character.previewState = worlds[worldId].previewState
characters[i] = character characters[i] = character
end end
@ -165,8 +231,8 @@ function ProtocolLogin:parseCharacterList(msg)
character.worldIp = iptostring(msg:getU32()) character.worldIp = iptostring(msg:getU32())
character.worldPort = msg:getU16() character.worldPort = msg:getU16()
if g_game.getClientVersion() >= 980 then if g_game.getFeature(GamePreviewState) then
character.unknown = msg:getU8() character.previewState = msg:getU8()
end end
characters[i] = character characters[i] = character

View File

@ -0,0 +1,30 @@
local messageModeCallbacks = {}
function g_game.onTextMessage(messageMode, message)
local callbacks = messageModeCallbacks[messageMode]
if not callbacks or #callbacks == 0 then
perror(string.format('Unhandled onTextMessage message mode %i: %s', messageMode, message))
return
end
for _, callback in pairs(callbacks) do
callback(messageMode, message)
end
end
function registerMessageMode(messageMode, callback)
if not messageModeCallbacks[messageMode] then
messageModeCallbacks[messageMode] = {}
end
table.insert(messageModeCallbacks[messageMode], callback)
return true
end
function unregisterMessageMode(messageMode, callback)
if not messageModeCallbacks[messageMode] then
return false
end
return table.removevalue(messageModeCallbacks[messageMode], callback)
end

View File

@ -85,6 +85,7 @@ end
local function onFlagMouseRelease(widget, pos, button) local function onFlagMouseRelease(widget, pos, button)
if button == MouseRightButton then if button == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
menu:addOption(tr('Delete mark'), function() widget:destroy() end) menu:addOption(tr('Delete mark'), function() widget:destroy() end)
menu:display(pos) menu:display(pos)
return true return true
@ -228,6 +229,7 @@ function UIMinimap:onMouseRelease(pos, button)
return true return true
elseif button == MouseRightButton then elseif button == MouseRightButton then
local menu = g_ui.createWidget('PopupMenu') local menu = g_ui.createWidget('PopupMenu')
menu:setGameMenu(true)
menu:addOption(tr('Create mark'), function() self:createFlagWindow(mapPos) end) menu:addOption(tr('Create mark'), function() self:createFlagWindow(mapPos) end)
menu:display(pos) menu:display(pos)
return true return true

View File

@ -24,6 +24,8 @@ set(client_SOURCES ${client_SOURCES}
# core # core
${CMAKE_CURRENT_LIST_DIR}/animatedtext.cpp ${CMAKE_CURRENT_LIST_DIR}/animatedtext.cpp
${CMAKE_CURRENT_LIST_DIR}/animatedtext.h ${CMAKE_CURRENT_LIST_DIR}/animatedtext.h
${CMAKE_CURRENT_LIST_DIR}/animator.h
${CMAKE_CURRENT_LIST_DIR}/animator.cpp
${CMAKE_CURRENT_LIST_DIR}/container.cpp ${CMAKE_CURRENT_LIST_DIR}/container.cpp
${CMAKE_CURRENT_LIST_DIR}/container.h ${CMAKE_CURRENT_LIST_DIR}/container.h
${CMAKE_CURRENT_LIST_DIR}/creature.cpp ${CMAKE_CURRENT_LIST_DIR}/creature.cpp

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

199
src/client/animator.cpp Normal file
View File

@ -0,0 +1,199 @@
/*
* Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
*
* 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.
*/
#include "declarations.h"
#include "animator.h"
#include <framework/core/clock.h>
#include <framework/core/filestream.h>
Animator::Animator()
{
m_animationPhases = 0;
m_startPhase = 0;
m_loopCount = 0;
m_async = false;
m_currentDuration = 0;
m_currentDirection = AnimDirForward;
m_currentLoop = 0;
m_lastPhaseTicks = 0;
m_isComplete = false;
m_phase = 0;
}
void Animator::unserialize(int animationPhases, const FileStreamPtr& fin)
{
m_animationPhases = animationPhases;
m_async = fin->getU8() == 0;
m_loopCount = fin->get32();
m_startPhase = fin->get8();
for(int i = 0; i < m_animationPhases; ++i) {
int minimum = fin->getU32();
int maximum = fin->getU32();
m_phaseDurations.push_back(std::make_tuple(minimum, maximum));
}
m_phase = getStartPhase();
assert(m_animationPhases == (int)m_phaseDurations.size());
assert(m_startPhase >= -1 && m_startPhase < m_animationPhases);
}
void Animator::serialize(const FileStreamPtr& fin)
{
fin->addU8(m_async ? 0 : 1);
fin->add32(m_loopCount);
fin->add8(m_startPhase);
for(std::tuple<int, int> phase : m_phaseDurations) {
fin->addU32(std::get<0>(phase));
fin->addU32(std::get<1>(phase));
}
}
void Animator::setPhase(int phase)
{
if(m_phase == phase) return;
if(m_async) {
if(phase == AnimPhaseAsync)
m_phase = 0;
else if(phase == AnimPhaseRandom)
m_phase = (int)stdext::random_range(0, (long)m_animationPhases);
else if(phase >= 0 && phase < m_animationPhases)
m_phase = phase;
else
m_phase = getStartPhase();
m_isComplete = false;
m_lastPhaseTicks = g_clock.millis();
m_currentDuration = getPhaseDuration(phase);
m_currentLoop = 0;
} else
calculateSynchronous();
}
int Animator::getPhase()
{
ticks_t ticks = g_clock.millis();
if(ticks != m_lastPhaseTicks && !m_isComplete) {
int elapsedTicks = (int)(ticks - m_lastPhaseTicks);
if(elapsedTicks >= m_currentDuration) {
int phase = 0;
if(m_loopCount < 0)
phase = getPingPongPhase();
else
phase = getLoopPhase();
if(m_phase != phase) {
int duration = getPhaseDuration(phase) - (elapsedTicks - m_currentDuration);
if(duration < 0 && !m_async) {
calculateSynchronous();
} else {
m_phase = phase;
m_currentDuration = std::max<int>(0, duration);
}
} else
m_isComplete = true;
} else
m_currentDuration -= elapsedTicks;
m_lastPhaseTicks = ticks;
}
return m_phase;
}
int Animator::getStartPhase()
{
if(m_startPhase > -1)
return m_startPhase;
return (int)stdext::random_range(0, (long)m_animationPhases);
}
void Animator::resetAnimation()
{
m_isComplete = false;
m_currentDirection = AnimDirForward;
m_currentLoop = 0;
setPhase(AnimPhaseAutomatic);
}
int Animator::getPingPongPhase()
{
int count = m_currentDirection == AnimDirForward ? 1 : -1;
int nextPhase = m_phase + count;
if(nextPhase < 0 || nextPhase >= m_animationPhases) {
m_currentDirection = m_currentDirection == AnimDirForward ? AnimDirBackward : AnimDirForward;
count *= -1;
}
return m_phase + count;
}
int Animator::getLoopPhase()
{
int nextPhase = m_phase + 1;
if(nextPhase < m_animationPhases)
return nextPhase;
if(m_loopCount == 0)
return 0;
if(m_currentLoop < (m_loopCount - 1)) {
m_currentLoop++;
return 0;
}
return m_phase;
}
int Animator::getPhaseDuration(int phase)
{
assert(phase < (int)m_phaseDurations.size());
std::tuple<int, int> data = m_phaseDurations.at(phase);
int min = std::get<0>(data);
int max = std::get<1>(data);
if(min == max) return min;
return (int)stdext::random_range((long)min, (long)max);
}
void Animator::calculateSynchronous()
{
int totalDuration = 0;
for(int i = 0; i < m_animationPhases; i++)
totalDuration += getPhaseDuration(i);
ticks_t ticks = g_clock.millis();
int elapsedTicks = (int)(ticks % totalDuration);
int totalTime = 0;
for(int i = 0; i < m_animationPhases; i++) {
int duration = getPhaseDuration(i);
if(elapsedTicks >= totalTime && elapsedTicks < totalTime + duration) {
m_phase = i;
m_currentDuration = duration - (elapsedTicks - totalTime);
break;
}
totalTime += duration;
}
m_lastPhaseTicks = ticks;
}

83
src/client/animator.h Normal file
View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
*
* 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.
*/
#ifndef ANIMATOR_H
#define ANIMATOR_H
#include "declarations.h"
#include <framework/core/declarations.h>
enum AnimationPhase : int16
{
AnimPhaseAutomatic = -1,
AnimPhaseRandom = 254,
AnimPhaseAsync = 255,
};
enum AnimationDirection : uint8
{
AnimDirForward = 0,
AnimDirBackward = 1
};
class Animator : public stdext::shared_object
{
public:
Animator();
void unserialize(int animationPhases, const FileStreamPtr& fin);
void serialize(const FileStreamPtr& fin);
void setPhase(int phase);
int getPhase();
int getStartPhase();
int getAnimationPhases() { return m_animationPhases; }
bool isAsync() { return m_async; }
bool isComplete() { return m_isComplete; }
void resetAnimation();
private:
int getPingPongPhase();
int getLoopPhase();
int getPhaseDuration(int phase);
void calculateSynchronous();
int m_animationPhases;
int m_startPhase;
int m_loopCount;
bool m_async;
std::vector< std::tuple<int, int> > m_phaseDurations;
int m_currentDuration;
AnimationDirection m_currentDirection;
int m_currentLoop;
ticks_t m_lastPhaseTicks;
bool m_isComplete;
int m_phase;
};
#endif

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -328,7 +328,9 @@ namespace Otc
MessageRVRChannel = 46, MessageRVRChannel = 46,
MessageRVRAnswer = 47, MessageRVRAnswer = 47,
MessageRVRContinue = 48, MessageRVRContinue = 48,
LastMessage = 49, MessageGameHighlight = 49,
MessageNpcFromStartBlock = 50,
LastMessage = 51,
MessageInvalid = 255 MessageInvalid = 255
}; };
@ -390,6 +392,17 @@ namespace Otc
GamePremiumExpiration = 57, GamePremiumExpiration = 57,
GameBrowseField = 58, GameBrowseField = 58,
GameEnhancedAnimations = 59, GameEnhancedAnimations = 59,
GameOGLInformation = 60,
GameMessageSizeCheck = 61,
GamePreviewState = 62,
GameLoginPacketEncryption = 63,
GameClientVersion = 64,
GameContentRevision = 65,
GameExperienceBonus = 66,
GameAuthenticator = 67,
GameUnjustifiedPoints = 68,
GameSessionKey = 69,
GameDeathType = 70,
LastGameFeature = 101 LastGameFeature = 101
}; };
@ -445,10 +458,19 @@ namespace Otc
LastSpeedFormula LastSpeedFormula
}; };
enum AnimationPhase { enum Blessings {
PhaseAutomatic = 0, BlessingNone = 0,
PhaseRandom = 254, BlessingAdventurer = 1,
PhaseAsync = 255 BlessingSpiritualShielding = 1 << 1,
BlessingEmbraceOfTibia = 1 << 2,
BlessingFireOfSuns = 1 << 3,
BlessingWisdomOfSolitude = 1 << 4,
BlessingSparkOfPhoenix = 1 << 5
};
enum DeathType {
DeathRegular = 0,
DeathBlessed = 1
}; };
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -696,6 +696,16 @@ void Creature::setSpeed(uint16 speed)
callLuaField("onSpeedChange", m_speed, oldSpeed); callLuaField("onSpeedChange", m_speed, oldSpeed);
} }
void Creature::setBaseSpeed(double baseSpeed)
{
if(m_baseSpeed != baseSpeed) {
double oldBaseSpeed = m_baseSpeed;
m_baseSpeed = baseSpeed;
callLuaField("onBaseSpeedChange", baseSpeed, oldBaseSpeed);
}
}
void Creature::setSkull(uint8 skull) void Creature::setSkull(uint8 skull)
{ {
m_skull = skull; m_skull = skull;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -58,6 +58,7 @@ public:
void setOutfitColor(const Color& color, int duration); void setOutfitColor(const Color& color, int duration);
void setLight(const Light& light) { m_light = light; } void setLight(const Light& light) { m_light = light; }
void setSpeed(uint16 speed); void setSpeed(uint16 speed);
void setBaseSpeed(double baseSpeed);
void setSkull(uint8 skull); void setSkull(uint8 skull);
void setShield(uint8 shield); void setShield(uint8 shield);
void setEmblem(uint8 emblem); void setEmblem(uint8 emblem);
@ -82,6 +83,7 @@ public:
Outfit getOutfit() { return m_outfit; } Outfit getOutfit() { return m_outfit; }
Light getLight() { return m_light; } Light getLight() { return m_light; }
uint16 getSpeed() { return m_speed; } uint16 getSpeed() { return m_speed; }
double getBaseSpeed() { return m_baseSpeed; }
uint8 getSkull() { return m_skull; } uint8 getSkull() { return m_skull; }
uint8 getShield() { return m_shield; } uint8 getShield() { return m_shield; }
uint8 getEmblem() { return m_emblem; } uint8 getEmblem() { return m_emblem; }
@ -147,6 +149,7 @@ protected:
Outfit m_outfit; Outfit m_outfit;
Light m_light; Light m_light;
int m_speed; int m_speed;
double m_baseSpeed;
uint8 m_skull; uint8 m_skull;
uint8 m_shield; uint8 m_shield;
uint8 m_emblem; uint8 m_emblem;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -45,6 +45,7 @@ class Effect;
class Missile; class Missile;
class AnimatedText; class AnimatedText;
class StaticText; class StaticText;
class Animator;
class ThingType; class ThingType;
class ItemType; class ItemType;
class House; class House;
@ -68,6 +69,7 @@ typedef stdext::shared_object_ptr<Effect> EffectPtr;
typedef stdext::shared_object_ptr<Missile> MissilePtr; typedef stdext::shared_object_ptr<Missile> MissilePtr;
typedef stdext::shared_object_ptr<AnimatedText> AnimatedTextPtr; typedef stdext::shared_object_ptr<AnimatedText> AnimatedTextPtr;
typedef stdext::shared_object_ptr<StaticText> StaticTextPtr; typedef stdext::shared_object_ptr<StaticText> StaticTextPtr;
typedef stdext::shared_object_ptr<Animator> AnimatorPtr;
typedef stdext::shared_object_ptr<ThingType> ThingTypePtr; typedef stdext::shared_object_ptr<ThingType> ThingTypePtr;
typedef stdext::shared_object_ptr<ItemType> ItemTypePtr; typedef stdext::shared_object_ptr<ItemType> ItemTypePtr;
typedef stdext::shared_object_ptr<House> HousePtr; typedef stdext::shared_object_ptr<House> HousePtr;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -84,6 +84,7 @@ void Game::resetGameStates()
m_localPlayer = nullptr; m_localPlayer = nullptr;
m_pingSent = 0; m_pingSent = 0;
m_pingReceived = 0; m_pingReceived = 0;
m_unjustifiedPoints = UnjustifiedPoints();
for(auto& it : m_containers) { for(auto& it : m_containers) {
const ContainerPtr& container = it.second; const ContainerPtr& container = it.second;
@ -155,6 +156,11 @@ void Game::processLoginWait(const std::string& message, int time)
g_lua.callGlobalField("g_game", "onLoginWait", message, time); g_lua.callGlobalField("g_game", "onLoginWait", message, time);
} }
void Game::processLoginToken(bool unknown)
{
g_lua.callGlobalField("g_game", "onLoginToken", unknown);
}
void Game::processLogin() void Game::processLogin()
{ {
g_lua.callGlobalField("g_game", "onLogin"); g_lua.callGlobalField("g_game", "onLogin");
@ -222,12 +228,12 @@ void Game::processGameEnd()
g_map.cleanDynamicThings(); g_map.cleanDynamicThings();
} }
void Game::processDeath(int penality) void Game::processDeath(int deathType, int penality)
{ {
m_dead = true; m_dead = true;
m_localPlayer->stopWalk(); m_localPlayer->stopWalk();
g_lua.callGlobalField("g_game", "onDeath", penality); g_lua.callGlobalField("g_game", "onDeath", deathType, penality);
} }
void Game::processGMActions(const std::vector<uint8>& actions) void Game::processGMActions(const std::vector<uint8>& actions)
@ -308,9 +314,6 @@ void Game::processCloseContainer(int containerId)
{ {
ContainerPtr container = getContainer(containerId); ContainerPtr container = getContainer(containerId);
if(!container) { if(!container) {
/* happens if you close and restart client with container opened
* g_logger.traceError("container not found");
*/
return; return;
} }
@ -322,7 +325,6 @@ void Game::processContainerAddItem(int containerId, const ItemPtr& item, int slo
{ {
ContainerPtr container = getContainer(containerId); ContainerPtr container = getContainer(containerId);
if(!container) { if(!container) {
g_logger.traceError("container not found");
return; return;
} }
@ -333,7 +335,6 @@ void Game::processContainerUpdateItem(int containerId, int slot, const ItemPtr&
{ {
ContainerPtr container = getContainer(containerId); ContainerPtr container = getContainer(containerId);
if(!container) { if(!container) {
g_logger.traceError("container not found");
return; return;
} }
@ -344,7 +345,6 @@ void Game::processContainerRemoveItem(int containerId, int slot, const ItemPtr&
{ {
ContainerPtr container = getContainer(containerId); ContainerPtr container = getContainer(containerId);
if(!container) { if(!container) {
g_logger.traceError("container not found");
return; return;
} }
@ -528,7 +528,7 @@ void Game::processWalkCancel(Otc::Direction direction)
m_localPlayer->cancelWalk(direction); m_localPlayer->cancelWalk(direction);
} }
void Game::loginWorld(const std::string& account, const std::string& password, const std::string& worldName, const std::string& worldHost, int worldPort, const std::string& characterName) void Game::loginWorld(const std::string& account, const std::string& password, const std::string& worldName, const std::string& worldHost, int worldPort, const std::string& characterName, const std::string& authenticatorToken, const std::string& sessionKey)
{ {
if(m_protocolGame || isOnline()) if(m_protocolGame || isOnline())
stdext::throw_exception("Unable to login into a world while already online or logging."); stdext::throw_exception("Unable to login into a world while already online or logging.");
@ -543,7 +543,7 @@ void Game::loginWorld(const std::string& account, const std::string& password, c
m_localPlayer->setName(characterName); m_localPlayer->setName(characterName);
m_protocolGame = ProtocolGamePtr(new ProtocolGame); m_protocolGame = ProtocolGamePtr(new ProtocolGame);
m_protocolGame->login(account, password, worldHost, (uint16)worldPort, characterName); m_protocolGame->login(account, password, worldHost, (uint16)worldPort, characterName, authenticatorToken, sessionKey);
m_characterName = characterName; m_characterName = characterName;
m_worldName = worldName; m_worldName = worldName;
} }
@ -854,7 +854,7 @@ void Game::useWith(const ItemPtr& item, const ThingPtr& toThing)
Position pos = item->getPosition(); Position pos = item->getPosition();
if(!pos.isValid()) // virtual item if(!pos.isValid()) // virtual item
pos = Position(0xFFFF, 0, 0); // means that is a item in inventory pos = Position(0xFFFF, 0, 0); // means that is an item in inventory
m_protocolGame->sendUseItemWith(pos, item->getId(), item->getStackPos(), toThing->getPosition(), toThing->getId(), toThing->getStackPos()); m_protocolGame->sendUseItemWith(pos, item->getId(), item->getStackPos(), toThing->getPosition(), toThing->getId(), toThing->getStackPos());
} }
@ -1204,6 +1204,31 @@ void Game::setPVPMode(Otc::PVPModes pvpMode)
g_lua.callGlobalField("g_game", "onPVPModeChange", pvpMode); g_lua.callGlobalField("g_game", "onPVPModeChange", pvpMode);
} }
void Game::setUnjustifiedPoints(UnjustifiedPoints unjustifiedPoints)
{
if(!canPerformGameAction())
return;
if(!getFeature(Otc::GameUnjustifiedPoints))
return;
if(m_unjustifiedPoints == unjustifiedPoints)
return;
m_unjustifiedPoints = unjustifiedPoints;
g_lua.callGlobalField("g_game", "onUnjustifiedPointsChange", unjustifiedPoints);
}
void Game::setOpenPvpSituations(int openPvpSituations)
{
if(!canPerformGameAction())
return;
if(m_openPvpSituations == openPvpSituations)
return;
m_openPvpSituations = openPvpSituations;
g_lua.callGlobalField("g_game", "onOpenPvpSituationsChange", openPvpSituations);
}
void Game::inspectNpcTrade(const ItemPtr& item) void Game::inspectNpcTrade(const ItemPtr& item)
{ {
if(!canPerformGameAction() || !item) if(!canPerformGameAction() || !item)
@ -1425,7 +1450,7 @@ void Game::setProtocolVersion(int version)
if(isOnline()) if(isOnline())
stdext::throw_exception("Unable to change protocol version while online"); stdext::throw_exception("Unable to change protocol version while online");
if(version != 0 && (version < 740 || version > 1051)) if(version != 0 && (version < 740 || version > 1076))
stdext::throw_exception(stdext::format("Protocol version %d not supported", version)); stdext::throw_exception(stdext::format("Protocol version %d not supported", version));
m_protocolVersion = version; m_protocolVersion = version;
@ -1443,7 +1468,7 @@ void Game::setClientVersion(int version)
if(isOnline()) if(isOnline())
stdext::throw_exception("Unable to change client version while online"); stdext::throw_exception("Unable to change client version while online");
if(version != 0 && (version < 740 || version > 1051)) if(version != 0 && (version < 740 || version > 1076))
stdext::throw_exception(stdext::format("Client version %d not supported", version)); stdext::throw_exception(stdext::format("Client version %d not supported", version));
m_features.reset(); m_features.reset();
@ -1452,6 +1477,7 @@ void Game::setClientVersion(int version)
if(version >= 770) { if(version >= 770) {
enableFeature(Otc::GameLooktypeU16); enableFeature(Otc::GameLooktypeU16);
enableFeature(Otc::GameMessageStatements); enableFeature(Otc::GameMessageStatements);
enableFeature(Otc::GameLoginPacketEncryption);
} }
if(version >= 780) { if(version >= 780) {
@ -1475,6 +1501,7 @@ void Game::setClientVersion(int version)
if(version >= 841) { if(version >= 841) {
enableFeature(Otc::GameChallengeOnLogin); enableFeature(Otc::GameChallengeOnLogin);
enableFeature(Otc::GameMessageSizeCheck);
} }
if(version >= 854) { if(version >= 854) {
@ -1523,6 +1550,11 @@ void Game::setClientVersion(int version)
enableFeature(Otc::GameAdditionalVipInfo); enableFeature(Otc::GameAdditionalVipInfo);
} }
if(version >= 980) {
enableFeature(Otc::GamePreviewState);
enableFeature(Otc::GameClientVersion);
}
if(version >= 981) { if(version >= 981) {
enableFeature(Otc::GameLoginPending); enableFeature(Otc::GameLoginPending);
enableFeature(Otc::GameNewSpeedLaw); enableFeature(Otc::GameNewSpeedLaw);
@ -1538,7 +1570,7 @@ void Game::setClientVersion(int version)
enableFeature(Otc::GamePVPMode); enableFeature(Otc::GamePVPMode);
} }
if (version >= 1035) { if(version >= 1035) {
enableFeature(Otc::GameDoubleSkills); enableFeature(Otc::GameDoubleSkills);
enableFeature(Otc::GameBaseSkillU16); enableFeature(Otc::GameBaseSkillU16);
} }
@ -1556,6 +1588,34 @@ void Game::setClientVersion(int version)
enableFeature(Otc::GameEnhancedAnimations); enableFeature(Otc::GameEnhancedAnimations);
} }
if(version >= 1053) {
enableFeature(Otc::GameUnjustifiedPoints);
}
if(version >= 1054) {
enableFeature(Otc::GameExperienceBonus);
}
if(version >= 1055) {
enableFeature(Otc::GameDeathType);
}
if(version >= 1061) {
enableFeature(Otc::GameOGLInformation);
}
if(version >= 1071) {
enableFeature(Otc::GameContentRevision);
}
if(version >= 1072) {
enableFeature(Otc::GameAuthenticator);
}
if(version >= 1074) {
enableFeature(Otc::GameSessionKey);
}
m_clientVersion = version; m_clientVersion = version;
g_lua.callGlobalField("g_game", "onClientVersionChange", version); g_lua.callGlobalField("g_game", "onClientVersionChange", version);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -36,6 +36,25 @@
#include <bitset> #include <bitset>
struct UnjustifiedPoints {
bool operator==(const UnjustifiedPoints& other) {
return killsDay == other.killsDay &&
killsDayRemaining == other.killsDayRemaining &&
killsWeek == other.killsWeek &&
killsWeekRemaining == other.killsWeekRemaining &&
killsMonth == other.killsMonth &&
killsMonthRemaining == other.killsMonthRemaining &&
skullTime == other.skullTime;
}
uint8 killsDay;
uint8 killsDayRemaining;
uint8 killsWeek;
uint8 killsWeekRemaining;
uint8 killsMonth;
uint8 killsMonthRemaining;
uint8 skullTime;
};
typedef std::tuple<std::string, uint, std::string, int, bool> Vip; typedef std::tuple<std::string, uint, std::string, int, bool> Vip;
//@bindsingleton g_game //@bindsingleton g_game
@ -60,13 +79,14 @@ protected:
void processLoginError(const std::string& error); void processLoginError(const std::string& error);
void processLoginAdvice(const std::string& message); void processLoginAdvice(const std::string& message);
void processLoginWait(const std::string& message, int time); void processLoginWait(const std::string& message, int time);
void processLoginToken(bool unknown);
void processLogin(); void processLogin();
void processPendingGame(); void processPendingGame();
void processEnterGame(); void processEnterGame();
void processGameStart(); void processGameStart();
void processGameEnd(); void processGameEnd();
void processDeath(int penality); void processDeath(int deathType, int penality);
void processGMActions(const std::vector<uint8>& actions); void processGMActions(const std::vector<uint8>& actions);
void processInventoryChange(int slot, const ItemPtr& item); void processInventoryChange(int slot, const ItemPtr& item);
@ -139,7 +159,7 @@ protected:
public: public:
// login related // login related
void loginWorld(const std::string& account, const std::string& password, const std::string& worldName, const std::string& worldHost, int worldPort, const std::string& characterName); void loginWorld(const std::string& account, const std::string& password, const std::string& worldName, const std::string& worldHost, int worldPort, const std::string& characterName, const std::string& authenticatorToken, const std::string& sessionKey);
void cancelLogin(); void cancelLogin();
void forceLogout(); void forceLogout();
void safeLogout(); void safeLogout();
@ -218,6 +238,12 @@ public:
bool isSafeFight() { return m_safeFight; } bool isSafeFight() { return m_safeFight; }
Otc::PVPModes getPVPMode() { return m_pvpMode; } Otc::PVPModes getPVPMode() { return m_pvpMode; }
// pvp related
void setUnjustifiedPoints(UnjustifiedPoints unjustifiedPoints);
UnjustifiedPoints getUnjustifiedPoints() { return m_unjustifiedPoints; };
void setOpenPvpSituations(int openPvpSitations);
int getOpenPvpSituations() { return m_openPvpSituations; }
// npc trade related // npc trade related
void inspectNpcTrade(const ItemPtr& item); void inspectNpcTrade(const ItemPtr& item);
void buyItem(const ItemPtr& item, int amount, bool ignoreCapacity, bool buyWithBackpack); void buyItem(const ItemPtr& item, int amount, bool ignoreCapacity, bool buyWithBackpack);
@ -304,6 +330,8 @@ public:
int getServerBeat() { return m_serverBeat; } int getServerBeat() { return m_serverBeat; }
void setCanReportBugs(bool enable) { m_canReportBugs = enable; } void setCanReportBugs(bool enable) { m_canReportBugs = enable; }
bool canReportBugs() { return m_canReportBugs; } bool canReportBugs() { return m_canReportBugs; }
void setExpertPvpMode(bool enable) { m_expertPvpMode = enable; }
bool getExpertPvpMode() { return m_expertPvpMode; }
LocalPlayerPtr getLocalPlayer() { return m_localPlayer; } LocalPlayerPtr getLocalPlayer() { return m_localPlayer; }
ProtocolGamePtr getProtocolGame() { return m_protocolGame; } ProtocolGamePtr getProtocolGame() { return m_protocolGame; }
std::string getCharacterName() { return m_characterName; } std::string getCharacterName() { return m_characterName; }
@ -333,6 +361,7 @@ private:
bool m_online; bool m_online;
bool m_denyBotCall; bool m_denyBotCall;
bool m_dead; bool m_dead;
bool m_expertPvpMode;
int m_serverBeat; int m_serverBeat;
ticks_t m_ping; ticks_t m_ping;
uint m_pingSent; uint m_pingSent;
@ -345,6 +374,8 @@ private:
Otc::ChaseModes m_chaseMode; Otc::ChaseModes m_chaseMode;
Otc::PVPModes m_pvpMode; Otc::PVPModes m_pvpMode;
Otc::Direction m_lastWalkDir; Otc::Direction m_lastWalkDir;
UnjustifiedPoints m_unjustifiedPoints;
int m_openPvpSituations;
bool m_safeFight; bool m_safeFight;
bool m_canReportBugs; bool m_canReportBugs;
std::vector<uint8> m_gmActions; std::vector<uint8> m_gmActions;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal
@ -382,6 +382,9 @@ int Item::calculateAnimationPhase(bool animate)
{ {
if(getAnimationPhases() > 1) { if(getAnimationPhases() > 1) {
if(animate) { if(animate) {
if(getAnimator() != nullptr)
return getAnimator()->getPhase();
if(m_async) if(m_async)
return (g_clock.millis() % (Otc::ITEM_TICKS_PER_FRAME * getAnimationPhases())) / Otc::ITEM_TICKS_PER_FRAME; return (g_clock.millis() % (Otc::ITEM_TICKS_PER_FRAME * getAnimationPhases())) / Otc::ITEM_TICKS_PER_FRAME;
else { else {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2010-2014 OTClient <https://github.com/edubart/otclient> * Copyright (c) 2010-2015 OTClient <https://github.com/edubart/otclient>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal * of this software and associated documentation files (the "Software"), to deal

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