diff --git a/README.md b/README.md index 3f90089..bc341b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OTClientV8 -Tibia client design for versions 7.40 - 11.00 +Tibia client designed for versions 7.40 - 11.00. It's based on https://github.com/edubart/otclient and it's not backward compatible. ## DISCORD: https://discord.gg/feySup6 diff --git a/init.lua b/init.lua index 95ba800..963479c 100644 --- a/init.lua +++ b/init.lua @@ -5,7 +5,7 @@ APP_VERSION = 1337 -- client version for updater and login to identify outd -- If you don't use updater or other service, set it to updater = "" Services = { website = "http://otclient.ovh", -- currently not used - updater = "http://otclient.ovh/api/updater.php", + updater = "", news = "http://otclient.ovh/api/news.php", stats = "", crash = "http://otclient.ovh/api/crash.php", diff --git a/modules/client_entergame/characterlist.lua b/modules/client_entergame/characterlist.lua index 3120c62..b976509 100644 --- a/modules/client_entergame/characterlist.lua +++ b/modules/client_entergame/characterlist.lua @@ -235,11 +235,8 @@ function CharacterList.terminate() CharacterList = nil end -function CharacterList.create(characters, account, otui, websocket) +function CharacterList.create(characters, account, otui) if not otui then otui = 'characterlist' end - if websocket then - websocket:close() - end if charactersWindow then charactersWindow:destroy() end diff --git a/modules/client_entergame/entergame.lua b/modules/client_entergame/entergame.lua index c4c2251..bf10bce 100644 --- a/modules/client_entergame/entergame.lua +++ b/modules/client_entergame/entergame.lua @@ -17,9 +17,6 @@ local serverHostTextEdit local rememberPasswordBox local protos = {"740", "760", "772", "792", "800", "810", "854", "860", "1077", "1090", "1096", "1098", "1099", "1100"} -local webSocket -local webSocketLoginPacket - -- private functions local function onProtocolError(protocol, message, errorCode) if errorCode then @@ -54,13 +51,9 @@ local function onCharacterList(protocol, characters, account, otui) loadBox = nil end - CharacterList.create(characters, account, otui, webSocket) + CharacterList.create(characters, account, otui) CharacterList.show() - if webSocket then - webSocket = nil - end - g_settings.save() end @@ -137,10 +130,6 @@ local function onHTTPResult(data, err) if #incorrectThings > 0 then g_logger.info(incorrectThings) if Updater then - if webSocket then - webSocket:close() - webSocket = nil - end return Updater.updateThings(things, incorrectThings) else return EnterGame.onError(incorrectThings) @@ -186,7 +175,7 @@ local function onHTTPResult(data, err) g_proxy.clear() if proxies then for i, proxy in ipairs(proxies) do - g_proxy.addProxy(tonumber(proxy["localPort"]), proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"])) + g_proxy.addProxy(proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"])) end end end @@ -198,7 +187,6 @@ end -- public functions function EnterGame.init() enterGame = g_ui.displayUI('entergame') - newLogin = g_ui.displayUI('entergame_new') serverSelectorPanel = enterGame:getChildById('serverSelectorPanel') customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel') @@ -260,15 +248,7 @@ end function EnterGame.terminate() g_keyboard.unbindKeyDown('Ctrl+G') - if webSocket then - webSocket.close() - webSocket = nil - end - enterGame:destroy() - if newLogin then - newLogin:destroy() - end if loadBox then loadBox:destroy() loadBox = nil @@ -288,12 +268,10 @@ function EnterGame.show() enterGame:raise() enterGame:focus() enterGame:getChildById('accountNameTextEdit'):focus() - EnterGame.checkWebsocket() end function EnterGame.hide() enterGame:hide() - newLogin:hide() end function EnterGame.openWindow() @@ -313,80 +291,8 @@ function EnterGame.clearAccountFields() g_settings.remove('password') end -function EnterGame.checkWebsocket() - if enterGame:isHidden() then return end - local url = serverHostTextEdit:getText() - if url:find("ws://") == nil and url:find("wss://") == nil then - if webSocket then - webSocket:close() - webSocket = nil - end - return - end - if webSocket then - if webSocket.url == url then - return - end - webSocket:close() - webSocket = nil - end - webSocket = HTTP.WebSocketJSON(url, { - onOpen = function(message, webSocketId) - if webSocket and webSocket.id == webSocketId then - webSocket.send({type="init", uid=G.UUID, version=APP_VERSION}) - end - end, - onMessage = function(message, webSocketId) - if webSocket and webSocket.id == webSocketId then - if message.type == "login" then - webSocketLoginPacket = nil - EnterGame.hide() - onHTTPResult(message, nil) - elseif message.type == "quick_login" and message.qrcode then - EnterGame.showNewLogin(message.qrcode) - end - end - end, - onClose = function(message, webSocketId) - if webSocket and webSocket.id == webSocketId then - webSocket = nil - if webSocketLoginPacket then - webSocketLoginPacket = nil - onHTTPResult(nil, "WebSocket disconnected") - end - EnterGame.checkWebsocket() -- reconnect - end - end, - onError = function(message, webSocketId) - if webSocket and webSocket.id == webSocketId then - -- handle error - end - end - }) -end - -function EnterGame.hideNewLogin() - newLogin:hide() -end - -function EnterGame.showNewLogin(qrcode) - if enterGame:isHidden() then return end - newLogin.qrcode:setQRCode("https://quath.co/0/" .. qrcode, 1) - newLogin.qrcode:setEnabled(true) - local clickFunction = function() - g_platform.openUrl("qauth://" .. qrcode) - end - newLogin.qrcode.onClick = clickFunction - newLogin.quathlogo.onClick = clickFunction - if newLogin:isHidden() then - newLogin:show() - newLogin:raise() - end -end - function EnterGame.onServerChange() server = serverSelector:getText() - EnterGame.hideNewLogin() if server == tr("Another") then if not customServerSelectorPanel:isOn() then serverHostTextEdit:setText("") @@ -399,7 +305,6 @@ function EnterGame.onServerChange() end if Servers and Servers[server] ~= nil then serverHostTextEdit:setText(Servers[server]) - EnterGame.checkWebsocket() end end @@ -509,36 +414,6 @@ function EnterGame.doLogin() end end - -function EnterGame.doLoginWs() - -- PREVIEW, need to implement websocket reconnect and error handling - if G.host == nil or G.host:len() < 10 then - return EnterGame.onError("Invalid server url: " .. G.host) - end - if not webSocket then - return EnterGame.onError("There's no websocket connection to: " .. G.host) - end - - loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...')) - connect(loadBox, { onCancel = function(msgbox) - loadBox = nil - webSocketLoginPacket = nil - EnterGame.show() - end }) - - local data = { - type = "login", - account = G.account, - password = G.password, - token = G.authenticatorToken, - version = APP_VERSION, - uid = G.UUID - } - webSocketLoginPacket = data - webSocket.send(data) - EnterGame.hide() -end - function EnterGame.doLoginHttp() if G.host == nil or G.host:len() < 10 then return EnterGame.onError("Invalid server url: " .. G.host) diff --git a/modules/client_locales/locales.lua b/modules/client_locales/locales.lua index 0a4b41e..107c5a8 100644 --- a/modules/client_locales/locales.lua +++ b/modules/client_locales/locales.lua @@ -6,6 +6,10 @@ local installedLocales local currentLocale function sendLocale(localeName) + if not g_game.getFeature(GameExtendedOpcode) then + return + end + local protocolGame = g_game.getProtocolGame() if protocolGame then protocolGame:sendExtendedOpcode(ExtendedIds.Locale, localeName) diff --git a/modules/client_options/game.otui b/modules/client_options/game.otui index 86b7739..12cef0a 100644 --- a/modules/client_options/game.otui +++ b/modules/client_options/game.otui @@ -84,6 +84,25 @@ Panel minimum: 0 maximum: 300 + Label + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + id: walkCtrlTurnDelayLabel + margin-top: 10 + @onSetup: | + local value = modules.client_options.getOption('walkTurnDelay') + self:setText(tr('Walk delay after ctrl turn: %s ms', value)) + + OptionScrollbar + id: walkCtrlTurnDelay + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-top: 3 + minimum: 0 + maximum: 300 + Label anchors.top: prev.bottom anchors.left: parent.left diff --git a/modules/client_options/options.lua b/modules/client_options/options.lua index 7bb146f..529b454 100644 --- a/modules/client_options/options.lua +++ b/modules/client_options/options.lua @@ -48,7 +48,8 @@ local defaultOptions = { walkFirstStepDelay = 200, walkTurnDelay = 100, walkStairsDelay = 50, - walkTeleportDelay = 200 + walkTeleportDelay = 200, + walkCtrlTurnDelay = 150 } local optionsWindow @@ -315,6 +316,8 @@ function setOption(key, value, force) generalPanel:getChildById('walkStairsDelayLabel'):setText(tr('Walk delay after floor change: %s ms', value)) elseif key == 'walkTeleportDelay' then generalPanel:getChildById('walkTeleportDelayLabel'):setText(tr('Walk delay after teleport: %s ms', value)) + elseif key == 'walkCtrlTurnDelay' then + generalPanel:getChildById('walkCtrlTurnDelayLabel'):setText(tr('Walk delay after ctrl turn: %s ms', value)) end -- change value for keybind updates diff --git a/modules/client_stats/stats.lua b/modules/client_stats/stats.lua index 2f47165..53959ad 100644 --- a/modules/client_stats/stats.lua +++ b/modules/client_stats/stats.lua @@ -134,6 +134,7 @@ function sendStats() mem = g_platform.getTotalSystemMemory(), mem_usage = g_platform.getMemoryUsage(), os_name = g_platform.getOSName(), + platform = g_window.getPlatformType(), uptime = g_clock.seconds() } } diff --git a/modules/client_terminal/terminal.lua b/modules/client_terminal/terminal.lua index 584e3f9..1f1246c 100644 --- a/modules/client_terminal/terminal.lua +++ b/modules/client_terminal/terminal.lua @@ -140,7 +140,7 @@ function init() terminalWindow.onDoubleClick = popWindow - --terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle) + terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle) g_keyboard.bindKeyDown('Ctrl+T', toggle) commandHistory = g_settings.getList('terminal-history') diff --git a/modules/corelib/http.lua b/modules/corelib/http.lua index 7103725..96e81c6 100644 --- a/modules/corelib/http.lua +++ b/modules/corelib/http.lua @@ -7,18 +7,27 @@ HTTP = { } function HTTP.get(url, callback) + if not g_http or not g_http.get then + return error("HTTP.get is not supported") + end local operation = g_http.get(url, HTTP.timeout) HTTP.operations[operation] = {type="get", url=url, callback=callback} return operation end function HTTP.getJSON(url, callback) + if not g_http or not g_http.get then + return error("HTTP.getJSON is not supported") + end local operation = g_http.get(url, HTTP.timeout) HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback} return operation end function HTTP.post(url, data, callback) + if not g_http or not g_http.post then + return error("HTTP.post is not supported") + end if type(data) == "table" then data = json.encode(data) end @@ -28,6 +37,9 @@ function HTTP.post(url, data, callback) end function HTTP.postJSON(url, data, callback) + if not g_http or not g_http.post then + return error("HTTP.postJSON is not supported") + end if type(data) == "table" then data = json.encode(data) end @@ -37,12 +49,18 @@ function HTTP.postJSON(url, data, callback) end function HTTP.download(url, file, callback, progressCallback) + if not g_http or not g_http.download then + return error("HTTP.download is not supported") + end local operation = g_http.download(url, file, HTTP.timeout) HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback} return operation end function HTTP.downloadImage(url, callback) + if not g_http or not g_http.download then + return error("HTTP.downloadImage is not supported") + end if HTTP.images[url] ~= nil then if callback then callback('/downloads/' .. HTTP.images[url], nil) @@ -57,6 +75,9 @@ function HTTP.downloadImage(url, callback) end function HTTP.webSocket(url, callbacks, timeout, jsonWebsocket) + if not g_http or not g_http.ws then + return error("WebSocket is not supported") + end if not timeout or timeout < 1 then timeout = HTTP.websocketTimeout end @@ -84,6 +105,9 @@ end HTTP.WebSocketJSON = HTTP.webSocketJSON function HTTP.cancel(operationId) + if not g_http or not g_http.cancel then + return + end return g_http.cancel(operationId) end diff --git a/modules/corelib/ui/uicombobox.lua b/modules/corelib/ui/uicombobox.lua index d3cd16e..b1c0992 100644 --- a/modules/corelib/ui/uicombobox.lua +++ b/modules/corelib/ui/uicombobox.lua @@ -19,6 +19,10 @@ function UIComboBox:clearOptions() self:clearText() end +function UIComboBox:clear() + return self:clearOptions() +end + function UIComboBox:getOptionsCount() return #self.options end @@ -108,12 +112,6 @@ function UIComboBox:removeOption(text) end end -function UIComboBox:clear() - self.options = {} - self.currentIndex = -1 - self:setText("") -end - function UIComboBox:onMousePress(mousePos, mouseButton) local menu if self.menuScroll then diff --git a/modules/game_bot/bot.lua b/modules/game_bot/bot.lua index d523372..d4b49e8 100644 --- a/modules/game_bot/bot.lua +++ b/modules/game_bot/bot.lua @@ -1,86 +1,37 @@ botWindow = nil botButton = nil -botConfigFile = nil -botConfig = nil contentsPanel = nil -configWindow = nil -configEditorText = nil -configList = nil -botTabs = nil -botPanel = nil +editWindow = nil + +local checkEvent = nil + +local botStorage = {} +local botStorageFile = nil local botWebSockets = {} local botMessages = nil -local configCopy = "" +local botTabs = nil +local botExecutor = nil + +local configList = nil local enableButton = nil local executeEvent = nil -local checkMsgsEvent = nil -local errorOccured = false local statusLabel = nil -local compiledConfig = nil -local configTab = nil -local tabs = {"main", "panels", "macros", "hotkeys", "callbacks", "other"} -local mainTab = nil -local activeTab = nil -local editorText = {"", ""} function init() - dofile("defaultconfig") dofile("executor") g_ui.importStyle("ui/basic.otui") g_ui.importStyle("ui/panels.otui") + g_ui.importStyle("ui/config.otui") connect(g_game, { onGameStart = online, onGameEnd = offline, - onTalk = botOnTalk, - onTextMessage = botOnTextMessage, - onUse = botOnUse, - onUseWith = botOnUseWith, - onChannelList = botChannelList, - onOpenChannel = botOpenChannel, - onCloseChannel = botCloseChannel, - onChannelEvent = botChannelEvent }) - - connect(rootWidget, { onKeyDown = botKeyDown, - onKeyUp = botKeyUp, - onKeyPress = botKeyPress }) - - connect(Tile, { onAddThing = botAddThing, onRemoveThing = botRemoveThing }) - - connect(Creature, { - onAppear = botCreatureAppear, - onDisappear = botCreatureDisappear, - onPositionChange = botCreaturePositionChange, - onHealthPercentChange = botCraetureHealthPercentChange - }) - connect(LocalPlayer, { - onPositionChange = botCreaturePositionChange, - onHealthPercentChange = botCraetureHealthPercentChange - }) - connect(Container, { onOpen = botContainerOpen, - onClose = botContainerClose, - onUpdateItem = botContainerUpdateItem }) - connect(g_map, { onMissle = botOnMissle }) - botConfigFile = g_configs.create("/bot.otml") - local config = botConfigFile:get("config") - if config ~= nil and config:len() > 10 then - local status, result = pcall(function() return json.decode(config) end) - if not status then - g_logger.error("Error: bot config parse error: " .. result .. "\n" .. config) - end - botConfig = result - else - botConfig = botDefaultConfig - end + initCallbacks() - botConfig.configs[1].name = botDefaultConfig.configs[1].name - botConfig.configs[1].script = botDefaultConfig.configs[1].script - - botButton = modules.client_topmenu.addRightGameToggleButton('botButton', - tr('Bot'), '/images/topbuttons/bot', toggle) + botButton = modules.client_topmenu.addRightGameToggleButton('botButton', tr('Bot'), '/images/topbuttons/bot', toggle) botButton:setOn(false) botButton:hide() @@ -93,107 +44,188 @@ function init() statusLabel = contentsPanel.statusLabel botMessages = contentsPanel.messages botTabs = contentsPanel.botTabs - botPanel = contentsPanel.botPanel - botTabs:setContentWidget(botPanel) - - configWindow = g_ui.displayUI('config') - configWindow:hide() - - configEditorText = configWindow.text - configTab = configWindow.configTab - - configTab.onTabChange = editorTabChanged - - for i=1,#botConfig.configs do - if botConfig.configs[i].name ~= nil then - configList:addOption(botConfig.configs[i].name) - else - configList:addOption("Config #" .. i) - end - end - if type(botConfig.selectedConfig) == 'number' then - configList:setCurrentIndex(botConfig.selectedConfig) - end - configList.onOptionChange = modules.game_bot.refreshConfig - - mainTab = configTab:addTab("all") - for k, v in ipairs(tabs) do - configTab:addTab(v, nil, nil) - end + botTabs:setContentWidget(contentsPanel.botPanel) + editWindow = g_ui.displayUI('edit') + editWindow:hide() + if g_game.isOnline() then + clear() online() end end -function saveConfig() - local status, result = pcall(function() - botConfigFile:set("config", json.encode(botConfig)) - botConfigFile:save() - end) - if not status then - errorOccured = true - -- try to fix it - local extraInfo = "" - for i = 1, #botConfig.configs do - if botConfig.configs[i].storage then - local status, result = pcall(function() json.encode(botConfig.configs[i].storage) end) - if not status then - botConfig.configs[i].storage = nil - extraInfo = extraInfo .. "\nLocal storage of config " .. i .. " has been erased due to invalid data" - end - end - end - statusLabel:setText("Error while saving config and user storage:\n" .. result .. extraInfo .. "\n\n" .. "Try to restart bot") - return false - end - return true -end - function terminate() - saveConfig() - clearConfig() - - disconnect(rootWidget, { onKeyDown = botKeyDown, - onKeyUp = botKeyUp, - onKeyPress = botKeyPress }) + save() + clear() disconnect(g_game, { onGameStart = online, onGameEnd = offline, - onTalk = botOnTalk, - onTextMessage = botOnTextMessage, - onUse = botOnUse, - onUseWith = botOnUseWith, - onChannelList = botChannelList, - onOpenChannel = botOpenChannel, - onCloseChannel = botCloseChannel, - onChannelEvent = botChannelEvent }) - disconnect(Tile, { onAddThing = botAddThing, onRemoveThing = botRemoveThing }) + terminateCallbacks() - disconnect(Creature, { - onAppear = botCreatureAppear, - onDisappear =botCreatureDisappear, - onPositionChange = botCreaturePositionChange, - onHealthPercentChange = botCraetureHealthPercentChange - }) - disconnect(LocalPlayer, { - onPositionChange = botCreaturePositionChange, - onHealthPercentChange = botCraetureHealthPercentChange - }) - disconnect(Container, { onOpen = botContainerOpen, - onClose = botContainerClose, - onUpdateItem = botContainerUpdateItem }) - disconnect(g_map, { onMissle = botOnMissle }) + removeEvent(checkEvent) - removeEvent(executeEvent) - removeEvent(checkMsgsEvent) + editWindow:destroy() botWindow:destroy() - botButton:destroy() - configWindow:destroy() + botButton:destroy() +end + +function clear() + botExecutor = nil + removeEvent(checkEvent) + + -- optimization, callback is not used when not needed + g_game.enableTileThingLuaCallback(false) + + botTabs:clearTabs() + botTabs:setOn(false) + + botMessages:destroyChildren() + botMessages:updateLayout() + + for i, socket in pairs(botWebSockets) do + g_http.cancel(socket) + end + botWebSockets = {} + + for i, widget in pairs(g_ui.getRootWidget():getChildren()) do + if widget.botWidget then + widget:destroy() + end + end + + local gameMapPanel = modules.game_interface.getMapPanel() + if gameMapPanel then + gameMapPanel:unlockVisibleFloor() + end + + if g_sounds then + g_sounds.getChannel(SoundChannels.Bot):stop() + end +end + + +function refresh() + save() + clear() + + -- create bot dir + if not g_resources.directoryExists("/bot") then + g_resources.makeDir("/bot") + if not g_resources.directoryExists("/bot") then + return onError("Can't create bot directory in " .. g_resources.getWriteDir()) + end + end + + -- get list of configs + local configs = g_resources.listDirectoryFiles("/bot", false, false) + if #configs == 0 then + createDefaultConfig() + configs = g_resources.listDirectoryFiles("/bot", false, false) + end + + -- clean + configList.onOptionChange = nil + enableButton.onClick = nil + configList:clearOptions() + + -- select active config based on settings + local settings = g_settings.getNode('bot') or {} + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + if settings[index] == nil then + settings[index] = { + enabled=false, + config="" + } + end + + -- init list and buttons + for i=1,#configs do + configList:addOption(configs[i]) + end + configList:setCurrentOption(settings[index].config) + if configList:getCurrentOption().text ~= settings[index].config then + settings[index].enabled = false + end + + enableButton:setOn(settings[index].enabled) + + configList.onOptionChange = function(widget) + settings[index].config = widget:getCurrentOption().text + settings[index].enabled = false + g_settings.setNode('bot', settings) + g_settings.save() + refresh() + end + + enableButton.onClick = function(widget) + settings[index].enabled = not settings[index].enabled + g_settings.setNode('bot', settings) + g_settings.save() + refresh() + end + + if not g_game.isOnline() or not settings[index].enabled then + statusLabel:setOn(true) + statusLabel:setText("Status: disabled") + return + end + + local configName = settings[index].config + + -- storage + botStorage = {} + botStorageFile = "/bot/" .. configName .. "/storage.json" + if g_resources.fileExists(botStorageFile) then + local status, result = pcall(function() + return json.decode(g_resources.readFileContents(botStorageFile)) + end) + if not status then + return onError("Error while reading storage (" .. botStorageFile .. "). To fix this problem you can delete storage.json. Details: " .. result) + end + botStorage = result + end + + -- run script + local status, result = pcall(function() + return executeBot(configName, botStorage, botTabs, message, save, botWebSockets) end + ) + if not status then + return onError(result) + end + + statusLabel:setOn(false) + botExecutor = result + check() +end + +function save() + if not botExecutor then + return + end + + local settings = g_settings.getNode('bot') or {} + local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion() + if settings[index] == nil then + return + end + + local status, result = pcall(function() + return json.encode(botStorage) + end) + if not status then + return onError("Error while saving bot storage. Storage won't be saved. Details: " .. result) + end + + if result:len() > 100 * 1024 * 1024 then + return onError("Storage file is too big, above 100MB, it won't be saved") + end + + g_resources.writeFileContents(botStorageFile, result) end function onMiniWindowClose() @@ -212,228 +244,63 @@ end function online() botButton:show() - updateEnabled() - if botConfig.enabled then - scheduleEvent(refreshConfig, 20) - else - clearConfig() - end - if executeEvent == nil then - executeEvent = scheduleEvent(executeConfig, 200) - checkMsgsEvent = scheduleEvent(checkMsgs, 200) - end + scheduleEvent(refresh, 20) end function offline() + save() + clear() botButton:hide() - configWindow:hide() - clearConfig() - removeEvent(executeEvent) - removeEvent(checkMsgsEvent) - executeEvent = nil - checkMsgsEvent = nil + editWindow:hide() end -function toggleBot() - botConfig.enabled = not botConfig.enabled - if botConfig.enabled then - refreshConfig() - else - clearConfig() - end - updateEnabled() +function onError(message) + statusLabel:setOn(true) + statusLabel:setText("Error:\n" .. message) + g_logger.error("[BOT] " .. message) end -function updateEnabled() - if botConfig.enabled then - enableButton:setText(tr('On')) - enableButton:setColor('#00AA00FF') - else - enableButton:setText(tr('Off')) - enableButton:setColor('#FF0000FF') - statusLabel:setText(tr("Status: disabled")) - end - errorOccured = false +function edit() + editWindow:show() + editWindow:focus() + editWindow:raise() end -function editConfig() - local config = configList.currentIndex - configWindow:show() - configWindow:raise() - configWindow:focus() - editorText = {botConfig.configs[config].script or "", ""} - if #editorText[1] <= 2 then - editorText[1] = "--config name\n\n" - for k, v in ipairs(tabs) do - editorText[1] = editorText[1] .. "--#" .. v .. "\n\n" - end - end - configEditorText:setText(editorText[1]) - configEditorText:setEditable(true) - activeTab = mainTab - configTab:selectTab(mainTab) -end - -local function split2(str, delimiter) - local result = { } - local from = 1 - local delim_from, delim_to = string.find( str, delimiter, from, true) - if delim_from then - table.insert( result, string.sub( str, from , delim_from - 1 ) ) - from = delim_to + 1 - delim_from, delim_to = string.find( str, delimiter, from ) - table.insert( result, string.sub( str, from ) ) - else - table.insert(result, str) - table.insert(result, "") - end - return result -end - -function restoreMainTab() - if activeTab == mainTab then - editorText = {configEditorText:getText(), ""} - return - end - local currentText = configEditorText:getText() - if #currentText > 0 and currentText:sub(#currentText, #currentText) ~= '\n' then - currentText = currentText .. '\n' - end - editorText = {editorText[1] .. "--#" .. activeTab:getText():lower() .. "\n" .. currentText .. editorText[2], ""} - configEditorText:setText(editorText[1]) -end - -function editorTabChanged(holder, tab) - if activeTab == tab then - return - end - restoreMainTab() - activeTab = tab - if tab == mainTab then - return - end - - local splitted = split2(editorText[1], "--#" .. activeTab:getText():lower() .. "\n") - local splitted2 = split2(splitted[2], "--#") - if splitted2[2]:len() > 1 then - splitted2[2] = "--#" .. splitted2[2] - end - editorText = {splitted[1], splitted2[2]} - configEditorText:setText(splitted2[1]) -end - -function saveEditedConfig() - restoreMainTab() - local config = configList.currentIndex - local text = configEditorText:getText() - configWindow:hide() - botConfig.configs[config].script = text - if text:len() > 3 and text:sub(1,2) == '--' and text:sub(3,3) ~= '#' then - local delim_from, delim_to = string.find( text, "\n", 3, true) - if delim_from then - botConfig.configs[config].name = string.sub( text, 3 , delim_from - 1 ):trim() - configList:updateCurrentOption(botConfig.configs[config].name) - end - end - refreshConfig() -end - -function clearConfig() - compiledConfig = nil - - botTabs:clearTabs() - botTabs:setOn(false) - - botMessages:destroyChildren() - botMessages:updateLayout() - - for socket in pairs(botWebSockets) do - g_http.cancel(socket) - botWebSockets[socket] = nil - end - - for i, widget in pairs(g_ui.getRootWidget():getChildren()) do - if widget.botWidget then - widget:destroy() +function createDefaultConfig() + if not g_resources.directoryExists("/bot/default_config") then + g_resources.makeDir("/bot/default_config") + if not g_resources.directoryExists("/bot/default_config") then + return onError("Can't create default_config directory in " .. g_resources.getWriteDir()) end end - local gameMapPanel = modules.game_interface.getMapPanel() - if gameMapPanel then - gameMapPanel:unlockVisibleFloor() - end - if g_sounds then - local botSoundChannel = g_sounds.getChannel(SoundChannels.Bot) - botSoundChannel:stop() - end -end -function refreshConfig() - configWindow:hide() - - botConfig.selectedConfig = configList.currentIndex - if not botConfig.enabled then - return - end - - if not saveConfig() then - clearConfig() - return - end - - clearConfig() - - local config = botConfig.configs[configList.currentIndex] - if not config.storage then - config.storage = {} - end - if config.script == nil or config.script:len() < 5 then - errorOccured = true - statusLabel:setText(tr("Error: empty config")) - return - end - errorOccured = false - g_game.enableTileThingLuaCallback(false) - local status, result = pcall(function() return executeBot(config.script, config.storage, botTabs, botMsgCallback, saveConfig, botWebSockets) end) - if not status then - errorOccured = true - statusLabel:setText("Error: " .. tostring(result)) - return - end - compiledConfig = result - statusLabel:setText(tr("Status: working")) -end - -function executeConfig() - executeEvent = scheduleEvent(executeConfig, 25) - if compiledConfig == nil then - return - end - if not botConfig.enabled or errorOccured then - if not errorOccured then - statusLabel:setText(tr("Status: disabled")) + local defaultConfigFiles = g_resources.listDirectoryFiles("default_config", true, false) + for i, file in ipairs(defaultConfigFiles) do + local baseName = file:split("/") + baseName = baseName[#baseName] + local contents = g_resources.readFileContents(file) + if contents:len() > 0 then + g_resources.writeFileContents("/bot/default_config/" .. baseName, contents) end - return end - local status, result = pcall(function() return compiledConfig.script() end) - if not status then - errorOccured = true - statusLabel:setText("Error: " .. result) - return - end end -function botMsgCallback(category, msg) +-- Executor +function message(category, msg) local widget = g_ui.createWidget('BotLabel', botMessages) widget.added = g_clock.millis() if category == 'error' then widget:setText(msg) widget:setColor("red") + g_logger.error("[BOT] " .. msg) elseif category == 'warn' then widget:setText(msg) widget:setColor("yellow") + g_logger.warning("[BOT] " .. msg) elseif category == 'info' then widget:setText(msg) widget:setColor("white") + g_logger.info("[BOT] " .. msg) end if botMessages:getChildCount() > 5 then @@ -441,132 +308,233 @@ function botMsgCallback(category, msg) end end -function checkMsgs() - checkMsgsEvent = scheduleEvent(checkMsgs, 200) +function check() + removeEvent(checkEvent) + if not botExecutor then + return + end + + checkEvent = scheduleEvent(check, 25) + + local status, result = pcall(function() + return botExecutor.script() + end) + if not status then + botExecutor = nil -- critical + return onError(result) + end + + -- remove old messages local widget = botMessages:getFirstChild() if widget and widget.added + 5000 < g_clock.millis() then widget:destroy() end end +-- Callbacks +function initCallbacks() + connect(rootWidget, { + onKeyDown = botKeyDown, + onKeyUp = botKeyUp, + onKeyPress = botKeyPress + }) + + connect(g_game, { + onTalk = botOnTalk, + onTextMessage = botOnTextMessage, + onUse = botOnUse, + onUseWith = botOnUseWith, + onChannelList = botChannelList, + onOpenChannel = botOpenChannel, + onCloseChannel = botCloseChannel, + onChannelEvent = botChannelEvent + }) + + connect(Tile, { + onAddThing = botAddThing, + onRemoveThing = botRemoveThing + }) + + connect(Creature, { + onAppear = botCreatureAppear, + onDisappear = botCreatureDisappear, + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange + }) + + connect(LocalPlayer, { + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange + }) + + connect(Container, { + onOpen = botContainerOpen, + onClose = botContainerClose, + onUpdateItem = botContainerUpdateItem + }) + + connect(g_map, { + onMissle = botOnMissle + }) +end + +function terminateCallbacks() + disconnect(rootWidget, { + onKeyDown = botKeyDown, + onKeyUp = botKeyUp, + onKeyPress = botKeyPress + }) + + disconnect(g_game, { + onTalk = botOnTalk, + onTextMessage = botOnTextMessage, + onUse = botOnUse, + onUseWith = botOnUseWith, + onChannelList = botChannelList, + onOpenChannel = botOpenChannel, + onCloseChannel = botCloseChannel, + onChannelEvent = botChannelEvent + }) + + disconnect(Tile, { + onAddThing = botAddThing, + onRemoveThing = botRemoveThing + }) + + disconnect(Creature, { + onAppear = botCreatureAppear, + onDisappear = botCreatureDisappear, + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange + }) + + disconnect(LocalPlayer, { + onPositionChange = botCreaturePositionChange, + onHealthPercentChange = botCraetureHealthPercentChange + }) + + disconnect(Container, { + onOpen = botContainerOpen, + onClose = botContainerClose, + onUpdateItem = botContainerUpdateItem + }) + + disconnect(g_map, { + onMissle = botOnMissle + }) +end + function safeBotCall(func) local status, result = pcall(func) if not status then - errorOccured = true - statusLabel:setText("Error: " .. result) + onError(result) end - return false end function botKeyDown(widget, keyCode, keyboardModifiers) - if compiledConfig == nil then return false end + if botExecutor == nil then return false end if keyCode == KeyUnknown then return end - safeBotCall(function() compiledConfig.callbacks.onKeyDown(keyCode, keyboardModifiers) end) + safeBotCall(function() botExecutor.callbacks.onKeyDown(keyCode, keyboardModifiers) end) end function botKeyUp(widget, keyCode, keyboardModifiers) - if compiledConfig == nil then return false end + if botExecutor == nil then return false end if keyCode == KeyUnknown then return end - safeBotCall(function() compiledConfig.callbacks.onKeyUp(keyCode, keyboardModifiers) end) + safeBotCall(function() botExecutor.callbacks.onKeyUp(keyCode, keyboardModifiers) end) end function botKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks) - if compiledConfig == nil then return false end + if botExecutor == nil then return false end if keyCode == KeyUnknown then return end - safeBotCall(function() compiledConfig.callbacks.onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks) end) + safeBotCall(function() botExecutor.callbacks.onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks) end) end function botOnTalk(name, level, mode, text, channelId, pos) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onTalk(name, level, mode, text, channelId, pos) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onTalk(name, level, mode, text, channelId, pos) end) end function botOnTextMessage(mode, text) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onTextMessage(mode, text) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onTextMessage(mode, text) end) end function botAddThing(tile, thing) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onAddThing(tile, thing) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onAddThing(tile, thing) end) end function botRemoveThing(tile, thing) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onRemoveThing(tile, thing) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onRemoveThing(tile, thing) end) end function botCreatureAppear(creature) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onCreatureAppear(creature) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureAppear(creature) end) end function botCreatureDisappear(creature) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onCreatureDisappear(creature) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureDisappear(creature) end) end function botCreaturePositionChange(creature, newPos, oldPos) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onCreaturePositionChange(creature, newPos, oldPos) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreaturePositionChange(creature, newPos, oldPos) end) end function botCraetureHealthPercentChange(creature, healthPercent) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onCreatureHealthPercentChange(creature, healthPercent) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCreatureHealthPercentChange(creature, healthPercent) end) end function botOnUse(pos, itemId, stackPos, subType) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onUse(pos, itemId, stackPos, subType) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onUse(pos, itemId, stackPos, subType) end) end function botOnUseWith(pos, itemId, target, subType) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onUseWith(pos, itemId, target, subType) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onUseWith(pos, itemId, target, subType) end) end function botContainerOpen(container, previousContainer) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onContainerOpen(container, previousContainer) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerOpen(container, previousContainer) end) end function botContainerClose(container) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onContainerClose(container) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerClose(container) end) end function botContainerUpdateItem(container, slot, item) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onContainerUpdateItem(container, slot, item) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onContainerUpdateItem(container, slot, item) end) end function botOnMissle(missle) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onMissle(missle) end) -end - -function botOnMissle(missle) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onMissle(missle) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onMissle(missle) end) end function botChannelList(channels) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onChannelList(channels) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onChannelList(channels) end) end function botOpenChannel(channelId, name) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onOpenChannel(channelId, name) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onOpenChannel(channelId, name) end) end function botCloseChannel(channelId) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onCloseChannel(channelId) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onCloseChannel(channelId) end) end function botChannelEvent(channelId, name, event) - if compiledConfig == nil then return false end - safeBotCall(function() compiledConfig.callbacks.onChannelEvent(channelId, name, event) end) + if botExecutor == nil then return false end + safeBotCall(function() botExecutor.callbacks.onChannelEvent(channelId, name, event) end) end diff --git a/modules/game_bot/bot.otui b/modules/game_bot/bot.otui index 885be6c..b858cdf 100644 --- a/modules/game_bot/bot.otui +++ b/modules/game_bot/bot.otui @@ -8,16 +8,14 @@ MiniWindow &autoOpen: 10 MiniWindowContents - margin-left: 5 - margin-right: 3 - ComboBox id: config anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right margin-top: 2 - margin-right: 90 + margin-left: 2 + margin-right: 75 text-offset: 3 0 Button @@ -26,34 +24,52 @@ MiniWindow anchors.left: prev.right anchors.right: parent.right !text: tr('Edit') - @onClick: modules.game_bot.editConfig() - margin-left: 5 - margin-right: 45 + @onClick: modules.game_bot.edit() + margin-left: 3 + margin-right: 37 Button id: enableButton anchors.top: prev.top anchors.left: prev.right anchors.right: parent.right - @onClick: modules.game_bot.toggleBot() - margin-left: 5 + margin-left: 3 + margin-right: 2 + + $on: + text: On + color: #00AA00 + + $!on: + text: Off + color: #FF0000 Label id: statusLabel anchors.top: prev.bottom anchors.left: parent.left anchors.right: parent.right - margin-top: 3 - text-auto-resize: true - !text: tr('Status: waiting') - text-align: center text-wrap: true + text-auto-resize: true + text-align: center + !text: tr('Status: waiting') + margin-left: 3 + margin-right: 3 + + $on: + margin-top: 3 + + $!on: + text: + margin-top: -13 HorizontalSeparator anchors.left: parent.left anchors.right: parent.right anchors.top: prev.bottom margin-top: 3 + margin-left: 2 + margin-right: 2 Panel anchors.top: prev.bottom @@ -69,6 +85,8 @@ MiniWindow anchors.right: parent.right anchors.top: prev.bottom margin-top: 5 + margin-left: 2 + margin-right: 2 MoveableTabBar id: botTabs @@ -76,6 +94,8 @@ MiniWindow anchors.left: parent.left anchors.right: parent.right tab-spacing: 1 + margin-left: 1 + margin-right: 1 height: 20 movable: false diff --git a/modules/game_bot/configs.png b/modules/game_bot/configs.png new file mode 100644 index 0000000..1e7ff71 Binary files /dev/null and b/modules/game_bot/configs.png differ diff --git a/modules/game_bot/default_config/battle.lua b/modules/game_bot/default_config/battle.lua new file mode 100644 index 0000000..e4ebc2b --- /dev/null +++ b/modules/game_bot/default_config/battle.lua @@ -0,0 +1,8 @@ +local batTab = addTab("Batt") + +Panels.AttackSpell(batTab) +Panels.AttackItem(batTab) + +Panels.AttackLeaderTarget(batTab) +Panels.LimitFloor(batTab) +Panels.AntiPush(batTab) diff --git a/modules/game_bot/default_config/cavebot.lua b/modules/game_bot/default_config/cavebot.lua new file mode 100644 index 0000000..f24a93f --- /dev/null +++ b/modules/game_bot/default_config/cavebot.lua @@ -0,0 +1,8 @@ +local caveTab = addTab("Cave") + +local waypoints = Panels.Waypoints(caveTab) +local attacking = Panels.Attacking(caveTab) +local looting = Panels.Looting(caveTab) +addButton("tutorial", "Help & Tutorials", function() + g_platform.openUrl("https://github.com/OTCv8/otclientv8_bot") +end, caveTab) diff --git a/modules/game_bot/default_config/example.otui b/modules/game_bot/default_config/example.otui new file mode 100644 index 0000000..541f98b --- /dev/null +++ b/modules/game_bot/default_config/example.otui @@ -0,0 +1,5 @@ +ExampleLabel2 < Label + text: LOL + height: 200 + width: 50 + \ No newline at end of file diff --git a/modules/game_bot/default_config/hp.lua b/modules/game_bot/default_config/hp.lua new file mode 100644 index 0000000..beb9db8 --- /dev/null +++ b/modules/game_bot/default_config/hp.lua @@ -0,0 +1,16 @@ +local healTab = addTab("HP") + +Panels.Haste(healTab) +Panels.ManaShield(healTab) +Panels.AntiParalyze(healTab) +Panels.Health(healTab) +Panels.Health(healTab) +Panels.HealthItem(healTab) +Panels.HealthItem(healTab) +Panels.ManaItem(healTab) +Panels.ManaItem(healTab) +Panels.Equip(healTab) +Panels.Equip(healTab) +Panels.Equip(healTab) +Panels.Eating(healTab) + diff --git a/modules/game_bot/default_config/main.lua b/modules/game_bot/default_config/main.lua new file mode 100644 index 0000000..9bb12d1 --- /dev/null +++ b/modules/game_bot/default_config/main.lua @@ -0,0 +1,16 @@ +Panels.TradeMessage() +Panels.AutoStackItems() + +addButton("discord", "Discord & Help", function() + g_platform.openUrl("https://discord.gg/yhqBE4A") +end) + +addButton("forum", "Forum", function() + g_platform.openUrl("https://otland.net/forums/otclient.494/") +end) + +addButton("github", "Documentation", function() + g_platform.openUrl("https://github.com/OTCv8/otclientv8_bot") +end) + +addSeparator("sep") diff --git a/modules/game_bot/default_config/npc.lua b/modules/game_bot/default_config/npc.lua new file mode 100644 index 0000000..ff76c60 --- /dev/null +++ b/modules/game_bot/default_config/npc.lua @@ -0,0 +1,7 @@ +singlehotkey("f10", "npc buy and sell", function() + NPC.say("hi") + NPC.say("trade") + NPC.buy(3074, 2) -- wand of vortex + NPC.sell(3074, 1) + NPC.closeTrade() +end) \ No newline at end of file diff --git a/modules/game_bot/default_config/tools.lua b/modules/game_bot/default_config/tools.lua new file mode 100644 index 0000000..326b16e --- /dev/null +++ b/modules/game_bot/default_config/tools.lua @@ -0,0 +1,84 @@ +local toolsTab = addTab("Tools") + +macro(1000, "exchange money", function() + local containers = getContainers() + for i, container in pairs(containers) do + for j, item in ipairs(container:getItems()) do + if item:isStackable() and (item:getId() == 3035 or item:getId() == 3031) and item:getCount() == 100 then + g_game.use(item) + return + end + end + end +end) + +macro(1000, "this macro does nothing", "f7", function() + +end) + +macro(100, "debug pathfinding", nil, function() + for i, tile in ipairs(g_map.getTiles(posz())) do + tile:setText("") + end + local path = findEveryPath(pos(), 20, { + ignoreNonPathable = false + }) + local total = 0 + for i, p in pairs(path) do + local s = i:split(",") + local pos = {x=tonumber(s[1]), y=tonumber(s[2]), z=tonumber(s[3])} + local tile = g_map.getTile(pos) + if tile then + tile:setText(p[2]) + end + total = total + 1 + end +end) + +macro(1000, "speed hack", nil, function() + player:setSpeed(1000) +end) + +hotkey("f5", "example hotkey", function() + info("Wow, you clicked f5 hotkey") +end) + +singlehotkey("ctrl+f6", "singlehotkey", function() + info("Wow, you clicked f6 singlehotkey") + usewith(268, player) +end) + +singlehotkey("ctrl+f8", "play alarm", function() + playAlarm() +end) + +singlehotkey("ctrl+f9", "stop alarm", function() + stopSound() +end) + +local positionLabel = addLabel("positionLabel", "") +onPlayerPositionChange(function() + positionLabel:setText("Pos: " .. posx() .. "," .. posy() .. "," .. posz()) +end) + +local s = addSwitch("sdSound", "Play sound when using sd", function(widget) + storage.sdSound = not storage.sdSound + widget:setOn(storage.sdSound) +end) +s:setOn(storage.sdSound) + +onUseWith(function(pos, itemId) + if storage.sdSound and itemId == 3155 then + playSound("/sounds/magnum.ogg") + end +end) + +macro(100, "hide useless tiles", "", function() + for i, tile in ipairs(g_map.getTiles(posz())) do + if not tile:isWalkable(true) then + tile:setFill('black') + end + end +end) + +addLabel("mapinfo", "You can use ctrl + plus and ctrl + minus to zoom in / zoom out map") diff --git a/modules/game_bot/edit.otui b/modules/game_bot/edit.otui new file mode 100644 index 0000000..ed5bc86 --- /dev/null +++ b/modules/game_bot/edit.otui @@ -0,0 +1,121 @@ +MainWindow + id: editWindow + size: 550 580 + !text: tr("Config editor") + @onEscape: self:hide() + @onEnter: self:hide() + + Label + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Bot configs are stored in:") + + TextEdit + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + height: 20 + width: 400 + margin-top: 5 + editable: false + !text: g_resources.getWriteDir() .. "bot" + text-align: center + + Button + id: documentationButton + !text: tr('Click here to open bot directory') + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: prev.bottom + margin-top: 5 + width: 250 + @onClick: g_platform.openDir(g_resources.getWriteDir() .. "bot") + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Every directory in bot directory is treated as different config.\nTo create new config just create new directory.") + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.horizontalCenter: parent.horizontalCenter + height: 175 + image-source: configs.png + image-fixed-ratio: true + image-size: 500 175 + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("Inside config directory put .lua and .otui files.\nEvery file will be loaded and executed in alphabetical order, .otui first and then .lua.") + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.horizontalCenter: parent.horizontalCenter + height: 150 + image-source: scripts.png + image-fixed-ratio: true + image-size: 500 150 + + Label + margin-top: 5 + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-auto-resize: true + text-align: center + text-wrap: true + !text: tr("To reload configs just press On and Off in bot window.\nTo learn more about bot click Tutorials button.") + + Button + !text: tr('Tutorials') + anchors.bottom: parent.bottom + anchors.left: parent.left + width: 100 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?tutorials") + + Button + !text: tr('Scripts') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 10 + width: 100 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?scripts") + + Button + !text: tr('Forum') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 10 + width: 100 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?forum") + + Button + !text: tr('Discord') + anchors.bottom: parent.bottom + anchors.left: prev.right + margin-left: 10 + width: 100 + @onClick: g_platform.openUrl("http://otclient.ovh/bot.php?discord") + + Button + id: cancelButton + !text: tr('Close') + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 60 + @onClick: self:getParent():hide() diff --git a/modules/game_bot/executor.lua b/modules/game_bot/executor.lua index 56885bf..1cb7593 100644 --- a/modules/game_bot/executor.lua +++ b/modules/game_bot/executor.lua @@ -1,7 +1,27 @@ function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, websockets) + -- load lua and otui files + local configFiles = g_resources.listDirectoryFiles("/bot/" .. config, true, false) + local luaFiles = {} + local uiFiles = {} + for i, file in ipairs(configFiles) do + local ext = file:split(".") + if ext[#ext]:lower() == "lua" then + table.insert(luaFiles, file) + end + if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then + table.insert(uiFiles, file) + end + end + + if #luaFiles == 0 then + return error("Config (/bot/" .. config .. ") doesn't have lua files") + end + + -- init bot variables local context = {} + context.configDir = "/bot/".. config context.tabs = tabs - context.panel = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel + context.panel = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel.content context.saveConfig = saveConfigCallback context._websockets = websockets @@ -10,7 +30,7 @@ function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, webs context.storage._macros = {} -- active macros end - -- + -- macros, hotkeys, scheduler, callbacks context._macros = {} context._hotkeys = {} context._scheduler = {} @@ -87,8 +107,15 @@ function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, webs dofiles("panels") G.botContext = nil - -- run script - assert(load(config, nil, nil, context))() + -- run ui scripts + for i, file in ipairs(uiFiles) do + g_ui.importStyle(file) + end + + -- run lua script + for i, file in ipairs(luaFiles) do + assert(load(g_resources.readFileContents(file), file, nil, context))() + end return { script = function() diff --git a/modules/game_bot/functions/config.lua b/modules/game_bot/functions/config.lua new file mode 100644 index 0000000..f02befb --- /dev/null +++ b/modules/game_bot/functions/config.lua @@ -0,0 +1,113 @@ +--[[ +Config. create. load and save config file (.json) +Used by cavebot and other things +]]-- + +local context = G.botContext +context.Config = {} +local Config = context.Config + +Config.exist = function(dir) + return g_resources.directoryExists(context.configDir .. "/" .. dir) +end + +Config.create = function(dir) + g_resources.makeDir(context.configDir .. "/" .. dir) + return Config.exist(dir) +end + +Config.list = function(dir) + if not Config.exist(dir) then + if not Config.create(dir) then + return contex.error("Can't create config dir: " .. context.configDir .. "/" .. dir) + end + end + return g_resources.listDirectoryFiles(context.configDir .. "/" .. dir) +end + +Config.load = function(dir, name) + local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json" + if g_resources.fileExists(file) then -- load json + return json.decode(g_resources.readFileContents(file)) + end + file = context.configDir .. "/" .. dir .. "/" .. name .. ".cfg" + if g_resources.fileExists(file) then -- load cfg + return g_resources.readFileContents(file) + end + return context.error("Config " .. file .. " doesn't exist") +end + +Config.save = function(dir, name, value) + if not Config.exist(dir) then + if not Config.create(dir) then + return contex.error("Can't create config dir: " .. context.configDir .. "/" .. dir) + end + end + local file = context.configDir .. "/" .. dir .. "/" .. name + if type(value) == 'string' then -- cfg + g_resources.writeFileContents(file .. ".cfg", value) + elseif type(value) == 'table' then -- json + g_resources.writeFileContents(file .. ".json", json.encode(value)) + end + return context.error("Invalid config value type: " .. type(value)) +end + +Config.remove = function(dir, name) + local file = context.configDir .. "/" .. dir .. "/" .. name .. ".json" + if g_resources.fileExists(file) then + return g_resources.deleteFile(file) + end + file = context.configDir .. "/" .. dir .. "/" .. name .. ".cfg" + if g_resources.fileExists(file) then + return g_resources.deleteFile(file) + end +end + +-- setup is used for BotConfig widget +-- not done yet +Config.setup = function(dir, widget, callback) + local refresh = function() + -- + end + + widget.switch.onClick = function() + widget.switch:setOn(not widget.switch:isOn()) + end + + widget.add = function() + + end + + widget.edit = function() + + end + + widget.remove = function() + + end + + --local configs = Config.list(dir) + --widget.list. + + return { + isOn = function() + return widget.switch:isOn() + end, + isOff = function() + return not widget.switch:isOn() + end, + enable = function() + if not widget.switch:isOn() then + widget.switch:onClick() + end + end, + disable = function() + if widget.switch:isOn() then + widget.switch:onClick() + end + end, + save = function() + + end + } +end \ No newline at end of file diff --git a/modules/game_bot/functions/npc.lua b/modules/game_bot/functions/npc.lua new file mode 100644 index 0000000..bbcea4b --- /dev/null +++ b/modules/game_bot/functions/npc.lua @@ -0,0 +1,111 @@ +local context = G.botContext + +context.NPC = {} + +context.NPC.talk = function(text) + if g_game.getClientVersion() >= 810 then + g_game.talkChannel(11, 0, text) + else + return context.say(text) + end +end +context.NPC.say = context.NPC.talk + +context.NPC.isTrading = function() + return modules.game_npctrade.npcWindow and modules.game_npctrade.npcWindow:isVisible() +end +context.NPC.hasTrade = context.NPC.isTrading +context.NPC.hasTradeWindow = context.NPC.isTrading + + +context.NPC.getSellItems = function() + if not context.NPC.isTrading() then return {} end + local items = {} + for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.SELL]) do + table.insert(items, { + id = item.ptr:getId(), + name = item.name, + count = item.ptr:getCount(), + subType = item.ptr:getSubType(), + weight = item.weight / 100, + price = item.price + }) + end + return items +end + +context.NPC.getBuyItems = function() + if not context.NPC.isTrading() then return {} end + for i, item in ipairs(modules.game_npctrade.tradeItems[modules.game_npctrade.BUY]) do + table.insert(items, { + id = item.ptr:getId(), + name = item.name, + count = item.ptr:getCount(), + subType = item.ptr:getSubType(), + weight = item.weight / 100, + price = item.price + }) + end + return items +end + +context.NPC.getSellQuantity = function(item) + if not context.NPC.isTrading() then return 0 end + if type(item) == 'number' then + item = Item.create(item) + end + return modules.game_npctrade.getSellQuantity(item) +end + +context.NPC.canTradeItem = function(item) + if not context.NPC.isTrading() then return false end + if type(item) == 'number' then + item = Item.create(item) + end + return modules.game_npctrade.canTradeItem(item) +end + +context.NPC.sell = function(item, count, ignoreEquipped) + if type(item) == 'number' then + item = Item.create(item) + end + if count == 0 then + count = 1 + end + if count == nil or count == -1 then + count = context.NPC.getSellQuantity(item) + end + if ignoreEquipped == nil then + ignoreEquipped = true + end + g_game.sellItem(item, count, ignoreEquipped) +end + +context.NPC.buy = function(item, count, ignoreCapacity, withBackpack) + if type(item) == 'number' then + item = Item.create(item) + end + if count == nil or count <= 0 then + count = 1 + end + if ignoreCapacity == nil then + ignoreCapacity = false + end + if withBackpack == nil then + withBackpack = false + end + g_game.buyItem(item, count, ignoreCapacity, withBackpack) +end + +context.NPC.sellAll = function() + if not context.NPC.isTrading() then return false end + modules.game_npctrade.sellAll() +end + +context.NPC.closeTrade = function() + modules.game_npctrade.closeNpcTrade() +end +context.NPC.close = context.NPC.closeTrade +context.NPC.finish = context.NPC.closeTrade +context.NPC.endTrade = context.NPC.closeTrade +context.NPC.finishTrade = context.NPC.closeTrade \ No newline at end of file diff --git a/modules/game_bot/functions/ui.lua b/modules/game_bot/functions/ui.lua index 2120ff6..9a37c68 100644 --- a/modules/game_bot/functions/ui.lua +++ b/modules/game_bot/functions/ui.lua @@ -11,7 +11,7 @@ end context.addTab = function(name) context.tabs:setOn(true) - return context.tabs:addTab(name, g_ui.createWidget('BotPanel')).tabPanel + return context.tabs:addTab(name, g_ui.createWidget('BotPanel')).tabPanel.content end context.addSwitch = function(id, text, onClickCallback, parent) diff --git a/modules/game_bot/scripts.png b/modules/game_bot/scripts.png new file mode 100644 index 0000000..640c0e9 Binary files /dev/null and b/modules/game_bot/scripts.png differ diff --git a/modules/game_bot/ui/basic.otui b/modules/game_bot/ui/basic.otui index 70d37dc..cc0a05d 100644 --- a/modules/game_bot/ui/basic.otui +++ b/modules/game_bot/ui/basic.otui @@ -39,8 +39,80 @@ BotSeparator < HorizontalSeparator margin-bottom: 3 BotPanel < Panel - layout: - type: verticalBox + ScrollablePanel + id: content + anchors.fill: parent + margin-right: 8 + margin-left: 1 + margin-bottom: 5 + vertical-scrollbar: botPanelScroll + layout: + type: verticalBox + + UIScrollBar + id: botPanelScroll + orientation: vertical + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + margin-bottom: 1 + step: 20 + width: 8 + image-source: /images/ui/scrollbar + image-clip: 39 0 13 65 + image-border: 1 + pixels-scroll: true + + UIButton + id: decrementButton + anchors.top: parent.top + anchors.left: parent.left + image-source: /images/ui/scrollbar + image-clip: 0 0 13 13 + image-color: #ffffffff + size: 8 8 + $hover: + image-clip: 13 0 13 13 + $pressed: + image-clip: 26 0 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: incrementButton + anchors.bottom: parent.bottom + anchors.right: parent.right + size: 8 8 + image-source: /images/ui/scrollbar + image-clip: 0 13 13 13 + image-color: #ffffffff + $hover: + image-clip: 13 13 13 13 + $pressed: + image-clip: 26 13 13 13 + $disabled: + image-color: #ffffff66 + + UIButton + id: sliderButton + anchors.centerIn: parent + size: 8 11 + image-source: /images/ui/scrollbar + image-clip: 0 26 13 13 + image-border: 2 + image-color: #ffffffff + $hover: + image-clip: 13 26 13 13 + $pressed: + image-clip: 26 26 13 13 + $disabled: + image-color: #ffffff66 + + Label + id: valueLabel + anchors.fill: parent + color: white + text-align: center CaveBotLabel < Label background-color: alpha diff --git a/modules/game_bot/ui/config.otui b/modules/game_bot/ui/config.otui new file mode 100644 index 0000000..efc787a --- /dev/null +++ b/modules/game_bot/ui/config.otui @@ -0,0 +1,51 @@ +BotConfig < Panel + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 40 + + ComboBox + id: list + anchors.top: parent.top + anchors.left: parent.left + text-offset: 3 0 + width: 130 + + Button + id: switch + anchors.top: prev.top + anchors.left: prev.right + anchors.right: parent.right + margin-left: 5 + $on: + text: On + color: #00AA00 + + $!on: + text: Off + color: #FF0000 + + Button + margin-top: 1 + id: add + anchors.top: prev.bottom + anchors.left: parent.left + text: Add + width: 58 + height: 17 + + Button + id: edit + anchors.top: prev.top + anchors.horizontalCenter: parent.horizontalCenter + text: Edit + width: 58 + height: 17 + + Button + id: remove + anchors.top: prev.top + anchors.right: parent.right + text: Remove + width: 58 + height: 17 \ No newline at end of file diff --git a/modules/game_bot/ui/panels.otui b/modules/game_bot/ui/panels.otui index 9a0bb5f..e4cdc9b 100644 --- a/modules/game_bot/ui/panels.otui +++ b/modules/game_bot/ui/panels.otui @@ -112,7 +112,6 @@ ItemsRow < Panel id: item1 anchors.top: parent.top anchors.left: parent.left - margin-left: 3 BotItem id: item2 diff --git a/modules/game_console/console.lua b/modules/game_console/console.lua index 798280b..55dfe8a 100644 --- a/modules/game_console/console.lua +++ b/modules/game_console/console.lua @@ -898,6 +898,7 @@ end function sendCurrentMessage() local message = consoleTextEdit:getText() if #message == 0 then return end + if not isChatEnabled() then return end consoleTextEdit:clearText() -- send message @@ -1071,6 +1072,10 @@ function setIgnoreNpcMessages(ignore) end function navigateMessageHistory(step) + if not isChatEnabled() then + return + end + local numCommands = #messageHistory if numCommands > 0 then currentMessageIndex = math.min(math.max(currentMessageIndex + step, 0), numCommands) diff --git a/modules/game_containers/containers.lua b/modules/game_containers/containers.lua index 32807ce..028c89c 100644 --- a/modules/game_containers/containers.lua +++ b/modules/game_containers/containers.lua @@ -95,6 +95,9 @@ function onContainerOpen(container, previousContainer) containerWindow:hide() end containerWindow.onDrop = function(container, widget, mousePos) + if containerPanel:getChildByPos(mousePos) then + return false + end local child = containerPanel:getChildByIndex(-1) if child then child:onDrop(widget, mousePos, true) diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua index 8e88d51..898b1c5 100644 --- a/modules/game_interface/gameinterface.lua +++ b/modules/game_interface/gameinterface.lua @@ -285,10 +285,14 @@ function onUseWith(clickedWidget, mousePosition) if clickedWidget:getClassName() == 'UIGameMap' then local tile = clickedWidget:getTile(mousePosition) if tile then - if selectedThing:isFluidContainer() or selectedThing:isMultiUse() then - g_game.useWith(selectedThing, tile:getTopMultiUseThing(), selectedSubtype) + if selectedThing:isFluidContainer() or selectedThing:isMultiUse() then + if selectedThing:getId() == 3180 or selectedThing:getId() == 3156 then + -- special version for mwall + g_game.useWith(selectedThing, tile:getTopUseThing(), selectedSubtype) + else + g_game.useWith(selectedThing, tile:getTopMultiUseThingEx(clickedWidget:getPositionOffset(mousePosition)), selectedSubtype) + end else - print("normal") g_game.useWith(selectedThing, tile:getTopUseThing(), selectedSubtype) end end diff --git a/modules/game_npctrade/npctrade.lua b/modules/game_npctrade/npctrade.lua index d76be94..744b3f7 100644 --- a/modules/game_npctrade/npctrade.lua +++ b/modules/game_npctrade/npctrade.lua @@ -13,6 +13,7 @@ searchText = nil setupPanel = nil quantity = nil quantityScroll = nil +idLabel = nil nameLabel = nil priceLabel = nil moneyLabel = nil @@ -49,6 +50,7 @@ function init() setupPanel = npcWindow:recursiveGetChildById('setupPanel') quantityScroll = setupPanel:getChildById('quantityScroll') + idLabel = setupPanel:getChildById('id') nameLabel = setupPanel:getChildById('name') priceLabel = setupPanel:getChildById('price') moneyLabel = setupPanel:getChildById('money') @@ -119,6 +121,23 @@ end function hide() npcWindow:hide() + + local layout = itemsPanel:getLayout() + layout:disableUpdates() + + clearSelectedItem() + + searchText:clearText() + setupPanel:disable() + itemsPanel:destroyChildren() + + if radioItems then + radioItems:destroy() + radioItems = nil + end + + layout:enableUpdates() + layout:update() end function onItemBoxChecked(widget) @@ -226,6 +245,7 @@ function setShowYourCapacity(state) end function clearSelectedItem() + idLabel:clearText() nameLabel:clearText() weightLabel:clearText() priceLabel:clearText() @@ -288,6 +308,7 @@ function canTradeItem(item) end function refreshItem(item) + idLabel:setText(item.ptr:getId()) nameLabel:setText(item.name) weightLabel:setText(string.format('%.2f', item.weight) .. ' ' .. WEIGHT_UNIT) priceLabel:setText(formatCurrency(getItemPrice(item))) @@ -420,11 +441,11 @@ end function closeNpcTrade() g_game.closeNpcTrade() - hide() + addEvent(hide) end function onCloseNpcTrade() - hide() + addEvent(hide) end function onPlayerGoods(money, items) diff --git a/modules/game_npctrade/npctrade.otui b/modules/game_npctrade/npctrade.otui index 7c766a1..24e6615 100644 --- a/modules/game_npctrade/npctrade.otui +++ b/modules/game_npctrade/npctrade.otui @@ -101,13 +101,24 @@ MainWindow image-color: #ffffff88 Label - !text: tr('Name') .. ':' + !text: tr('Id') .. ':' anchors.left: parent.left anchors.top: parent.top margin-top: 5 margin-left: 5 width: 85 + NPCOfferLabel + id: id + + Label + !text: tr('Name') .. ':' + anchors.left: parent.left + anchors.top: prev.bottom + margin-top: 5 + margin-left: 5 + width: 85 + NPCOfferLabel id: name diff --git a/modules/game_viplist/viplist.lua b/modules/game_viplist/viplist.lua index 24e3625..8e830f7 100644 --- a/modules/game_viplist/viplist.lua +++ b/modules/game_viplist/viplist.lua @@ -243,9 +243,12 @@ function sortBy(state) refresh() end -function onAddVip(id, name, state, description, iconId, notify) - local vipList = vipWindow:getChildById('contentsPanel') +function onAddVip(id, name, state, description, iconId, notify) + if not name or name:len() == 0 then + return + end + local vipList = vipWindow:getChildById('contentsPanel') local childrenCount = vipList:getChildCount() for i=1,childrenCount do local child = vipList:getChildByIndex(i) diff --git a/modules/game_walking/walking.lua b/modules/game_walking/walking.lua index 4259559..762254c 100644 --- a/modules/game_walking/walking.lua +++ b/modules/game_walking/walking.lua @@ -383,7 +383,7 @@ function turn(dir, repeated) end lastTurnDirection = dir nextWalkDir = nil - player:lockWalk(150) + player:lockWalk(g_settings.getNumber('walkCtrlTurnDelay')) end end diff --git a/otclient_dx.exe b/otclient_dx.exe index 13247e2..e48ab1f 100644 Binary files a/otclient_dx.exe and b/otclient_dx.exe differ diff --git a/otclient_gl.exe b/otclient_gl.exe index 5208cc0..f285c66 100644 Binary files a/otclient_gl.exe and b/otclient_gl.exe differ diff --git a/otclient_linux b/otclient_linux index ca6c026..accdce1 100644 Binary files a/otclient_linux and b/otclient_linux differ diff --git a/pdb/otclient_dx.zip b/pdb/otclient_dx.zip index a4b8173..888a45a 100644 Binary files a/pdb/otclient_dx.zip and b/pdb/otclient_dx.zip differ diff --git a/pdb/otclient_gl.zip b/pdb/otclient_gl.zip index 24308b7..07d3304 100644 Binary files a/pdb/otclient_gl.zip and b/pdb/otclient_gl.zip differ