diff --git a/README.md b/README.md index 37f57a8..3f90089 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OTClientV8 -Tibia client design for versions 7.40 - 10.99 +Tibia client design 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 @@ -42,7 +42,11 @@ It's based on https://github.com/edubart/otclient and it's not backward compatib # Paid version The difference between paid version and this one is that the 1st one comes with c++ sources and has better support. You may need c++ source if you want to add some more advanced modifications, better encryption, bot protection or some other things. The free version doesn't offer technical support, you need to follow tutorials and in case of any bug or problem you should submit an issue on github. Visit http://otclient.ovh if you want to know more about paid version and other extra services. -# Quick Start +# Quick Start for players + +Download whole repository and run binary. + +# Quick Start for server owners Open `init.lua` and edit: diff --git a/api/websockets/app.ts b/api/websockets/app.ts deleted file mode 100644 index 5b1f008..0000000 --- a/api/websockets/app.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { App } from 'uWebSockets.js'; -import * as Login from './login'; -const config = require("./config.json"); - -let Sessions = new Set(); -let Clients = {}; -let QuickLogin = {}; - -App({ - // options for ssl - key_file_name: 'key.pem', - cert_file_name: 'cert.pem' -}).ws('/*', { - compression: 0, - maxPayloadLength: 64 * 1024, - idleTimeout: 10, - open: (ws, req) => { - ws.uid = null; - Sessions.add(ws); - }, - close: (ws, code, message) => { - if (ws.uid && Clients[ws.uid] == ws) { - delete Clients[ws.uid]; - delete QuickLogin[ws.short_code]; - delete QuickLogin[ws.full_code]; - } - Sessions.delete(ws); - }, - message: (ws, message, isBinary) => { - try { - let data = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(message))); - if (data["type"] == "init") { - if (ws.uid || typeof (data["uid"]) != "string" || data["uid"].length < 10) { - return ws.end(1, "Invalid init message"); // already has an uid or uid is invalid - } - ws.uid = data["uid"]; - ws.version = data["version"] - if (Clients[ws.uid]) { - Clients[ws.uid].close(); - } - ws.short_code = "XXXX"; - ws.full_code = "Login on server otclient.ovh. XXXX"; - Clients[ws.uid] = ws; - QuickLogin[ws.short_code] = ws; - QuickLogin[ws.full_code] = ws; - return ws.send(JSON.stringify({ - "type": "quick_login", - "code": ws.short_code, - "qrcode": ws.full_code, - "message": "" - })); - } - if (!ws.uid) { - return ws.end(2, "Missing uid"); - } - if (data["type"] == "login") { - return Login.login(ws, data["account"], data["password"]); - } - } catch (e) { - try { - return ws.end(3, "Exception"); - } catch (e) {} - } - } -}).any('/login', (res, req) => { - let buffer: string = ""; - res.onData((chunk, last) => { - try { - buffer += String.fromCharCode.apply(null, new Uint8Array(chunk)); - if (!last) { - return; - } - const data = JSON.parse(buffer); - const code = data["code"]; - const client = QuickLogin[code]; - if (!client) { - return res.end("Invalid code"); - } - Login.quickLogin(res, client, data); - } catch (e) { - res.end("Exception"); - } - }); - - res.onAborted(() => { - return res.end("Aborted"); - }); -}).any('/*', (res, req) => { - res.end('404'); -}).listen(config.port, (listenSocket) => { - if (listenSocket) { - console.log(`Listening to port ${config.port}`); - } else { - console.log(`Error, can't listen on port ${config.port}`) - } -}); \ No newline at end of file diff --git a/api/websockets/config.json b/api/websockets/config.json deleted file mode 100644 index a474eb6..0000000 --- a/api/websockets/config.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "port": 88, - "sql": { - "host": "otclient.ovh", - "user": "otclient", - "password": "otclient", - "database": "otclient" - }, - "maxLogins": 10, - "blockTime": 60, - "hash": "sha1", - "serverName": "OTClientV8", - "serverIP": "otclient.ovh", - "serverPort": 7172, - "version": 1099, - "things": { - "sprites": [ "1099/Tibia.spr", "63d38646597649a55a8be463d6c0fb49" ], - "data": [ "1099/Tibia.dat", "ae7157cfff42f14583d6363e77044df7" ] - }, - "customProtocol": null, - "options": { - "allowFullView": true - }, - "features": { - "22": true, - "25": true, - "30": true, - "80": true, - "90": true, - "95": true - }, - "proxies": { - - }, - "rsa": "109120132967399429278860960508995541528237502902798129123468757937266291492576446330739696001110603907230888610072655818825358503429057592827629436413108566029093628212635953836686562675849720620786279431090218017681061521755056710823876476444260558147179707119674283982419152118103759076030616683978566631413" -} \ No newline at end of file diff --git a/api/websockets/login.ts b/api/websockets/login.ts deleted file mode 100644 index 8303161..0000000 --- a/api/websockets/login.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { HttpResponse, WebSocket } from 'uWebSockets.js'; -import * as mysql from 'mysql2/promise'; -import * as crypto from 'crypto'; -const config = require("./config.json"); - -function hash(algorithm: string, data: string): string { - return crypto.createHash(algorithm).update(data).digest("hex"); -} - -function time(): number { - return new Date().getTime(); -} - -export async function login(ws: WebSocket, login: string, password: string) { - let sql : mysql.Connection = null; - try { - sql = await mysql.createConnection({ - host: config.sql.host, - user: config.sql.user, - password: config.sql.password, - database: config.sql.database - }); - - let hash_password = password - if (config.hash == "md5") { - hash_password = hash("md5", password); - } else if (config.hash == "sha1") { - hash_password = hash("sha1", password); - } - - const [accounts, accountFields] = await sql.execute('SELECT * FROM `accounts` where `name` = ? and `password` = ?', [login, hash_password]); - if (accounts.length != 1) { - await sql.end(); - return ws.send(JSON.stringify({"type": "login", "error": "Invalid account/password"}), false); - } - const account = accounts[0]; - const [players, playersFields] = await sql.execute('SELECT * FROM `players` where `account_id` = ?', [account.id]); - await sql.end(); - - let response = { - "type": "login", - "error": "", - "rsa": config.rsa, - "version": config.version, - "things": config.things, - "customProtocol": config.customProtocol, - "session": "", - "characters": [], - "account": {}, - "options": config.options, - "features": config.features, - "proxies": config.proxies - } - - response["session"] = `${login}\n${password}\n\n${time()}`; - - response["account"]["status"] = 0; // 0=ok, 1=frozen, 2=supsended - response["account"]["subStatus"] = 1; // 0=free, 1=premium - response["account"]["premDays"] = 65535; - - for (let i = 0; i < players.length; ++i) { - response.characters.push({ - "name": players[i].name, - "worldName": config.serverName, - "worldIp": config.serverIP, - "worldPort": config.serverPort - }) - } - - console.log(response); - ws.send(JSON.stringify(response), false); - } catch (e) { - try { - await sql.end() - } catch (e) { }; - try { - ws.end(5, "Login exception"); - } catch (e) { }; - } -} - -export async function quickLogin(res : HttpResponse, ws: WebSocket, data: any) { - -} \ No newline at end of file diff --git a/api/websockets/package.json b/api/websockets/package.json deleted file mode 100644 index 4772a83..0000000 --- a/api/websockets/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "otcv8socks", - "version": "1.0.0", - "description": "Websockets server for otclientv8", - "main": "app.js", - "author": { - "name": "" - }, - "scripts": { - "build": "tsc --build", - "clean": "tsc --build --clean" - }, - "devDependencies": { - "@types/mysql2": "github:types/mysql2", - "@types/node": "^8.0.14", - "typescript": "^3.2.2" - }, - "dependencies": { - "mysql2": "^2.0.1", - "uWebSockets.js": "github:uNetworking/uWebSockets.js#v16.4.0" - } -} diff --git a/api/websockets/tsconfig.json b/api/websockets/tsconfig.json deleted file mode 100644 index 8cc8017..0000000 --- a/api/websockets/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "lib": ["es6"], - "sourceMap": true - }, - "exclude": [ - "node_modules" - ] -} diff --git a/api/websockets/typings.json b/api/websockets/typings.json deleted file mode 100644 index 0fe4f0b..0000000 --- a/api/websockets/typings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "mysql2": "registry:npm/mysql2#1.1.1+20170314181009" - } -} diff --git a/init.lua b/init.lua index 9e84eb3..95ba800 100644 --- a/init.lua +++ b/init.lua @@ -14,11 +14,11 @@ Services = { -- Servers accept http login url, websocket login url or ip:port:version Servers = { - OTClientV8 = "http://otclient.ovh/api/login.php", - OTClientV8Websocket = "wss://otclient.ovh:3000/", - OTClientV8proxy = "http://otclient.ovh/api/login.php?proxy=1", - OTClientV8c = "otclient.ovh:7171:1099:25:30:80:90", - OTClientV8Test = "http://otclient.ovh/api/login2.php", +-- OTClientV8 = "http://otclient.ovh/api/login.php", +-- OTClientV8Websocket = "wss://otclient.ovh:3000/", +-- OTClientV8proxy = "http://otclient.ovh/api/login.php?proxy=1", +-- OTClientV8ClassicWithFeatures = "otclient.ovh:7171:1099:25:30:80:90", +-- OTClientV8Classic = "otclient.ovh:7171:1099" } ALLOW_CUSTOM_SERVERS = true -- if true it shows option ANOTHER on server list -- CONFIG END diff --git a/modules/client_entergame/characterlist.lua b/modules/client_entergame/characterlist.lua index 7561010..3120c62 100644 --- a/modules/client_entergame/characterlist.lua +++ b/modules/client_entergame/characterlist.lua @@ -31,14 +31,6 @@ local function tryLogin(charInfo, tries) CharacterList.hide() - -- proxies for not http login users - if charInfo.worldHost == "0.0.0.0" and g_proxy then - g_proxy.clear() - -- g_proxy.addProxy(proxyHost, proxyPort, proxyPriority) - g_proxy.addProxy("163.172.147.135", 7162, 0) - g_proxy.addProxy("158.69.68.42", 7162, 0) - end - g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey) g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort) loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...')) @@ -243,9 +235,11 @@ function CharacterList.terminate() CharacterList = nil end -function CharacterList.create(characters, account, otui) +function CharacterList.create(characters, account, otui, websocket) 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 bc41acd..c4c2251 100644 --- a/modules/client_entergame/entergame.lua +++ b/modules/client_entergame/entergame.lua @@ -15,7 +15,7 @@ local serverSelector local clientVersionSelector local serverHostTextEdit local rememberPasswordBox -local protos = {"740", "760", "772", "800", "810", "854", "860", "1077", "1090", "1096", "1098", "1099", "1100"} +local protos = {"740", "760", "772", "792", "800", "810", "854", "860", "1077", "1090", "1096", "1098", "1099", "1100"} local webSocket local webSocketLoginPacket @@ -53,10 +53,14 @@ local function onCharacterList(protocol, characters, account, otui) loadBox:destroy() loadBox = nil end - - CharacterList.create(characters, account, otui) + + CharacterList.create(characters, account, otui, webSocket) CharacterList.show() + if webSocket then + webSocket = nil + end + g_settings.save() end @@ -76,18 +80,22 @@ end local function validateThings(things) local incorrectThings = "" + local missingFiles = false + local versionForMissingFiles = 0 if things ~= nil then local thingsNode = {} for thingtype, thingdata in pairs(things) do thingsNode[thingtype] = thingdata[1] if not g_resources.fileExists("/data/things/" .. thingdata[1]) then - correctThings = false incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n" - end - local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower() - if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then - if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version - incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n" + missingFiles = true + versionForMissingFiles = thingdata[1]:split("/")[1] + else + local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower() + if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then + if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version + incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n" + end end end end @@ -95,6 +103,12 @@ local function validateThings(things) else g_settings.setNode("things", {}) end + if missingFiles then + + incorrectThings = incorrectThings .. "\nYou should open data/things and create directory " .. versionForMissingFiles .. + ".\nIn this directory (data/things/" .. versionForMissingFiles .. ") you should put missing\nfiles (Tibia.dat and Tibia.spr) " .. + "from correct Tibia version." + end return incorrectThings end @@ -177,10 +191,6 @@ local function onHTTPResult(data, err) end end - if webSocket then - webSocket:close() - webSocket = nil - end onCharacterList(nil, characters, account, nil) end @@ -310,22 +320,16 @@ function EnterGame.checkWebsocket() if webSocket then webSocket:close() webSocket = nil - newLogin.code:setText("") end return end if webSocket then if webSocket.url == url then - if newLogin:isHidden() and newLogin.code:getText():len() > 1 then - newLogin:show() - newLogin:raise() - end return end webSocket:close() webSocket = nil end - newLogin.code:setText("") webSocket = HTTP.WebSocketJSON(url, { onOpen = function(message, webSocketId) if webSocket and webSocket.id == webSocketId then @@ -338,8 +342,8 @@ function EnterGame.checkWebsocket() webSocketLoginPacket = nil EnterGame.hide() onHTTPResult(message, nil) - elseif message.type == "quick_login" and message.code and message.qrcode then - EnterGame.showNewLogin(message.code, message.qrcode) + elseif message.type == "quick_login" and message.qrcode then + EnterGame.showNewLogin(message.qrcode) end end end, @@ -365,10 +369,15 @@ function EnterGame.hideNewLogin() newLogin:hide() end -function EnterGame.showNewLogin(code, qrcode) +function EnterGame.showNewLogin(qrcode) if enterGame:isHidden() then return end - newLogin.code:setText(code) - newLogin.qrcode:setQRCode(qrcode, 1) + 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() @@ -449,7 +458,7 @@ function EnterGame.doLogin() local incorrectThings = validateThings(things) if #incorrectThings > 0 then - g_logger.info(incorrectThings) + g_logger.error(incorrectThings) if Updater then return Updater.updateThings(things, incorrectThings) else diff --git a/modules/client_entergame/entergame_new.otui b/modules/client_entergame/entergame_new.otui index 8e59bbe..8f4e83e 100644 --- a/modules/client_entergame/entergame_new.otui +++ b/modules/client_entergame/entergame_new.otui @@ -3,11 +3,11 @@ StaticWindow anchors.verticalCenter: parent.verticalCenter margin-right: 20 id: newLoginPanel - width: 230 - height: 350 + width: 240 + height: 320 !text: tr('Quick Login & Registration') - Label + UIButton id: qrcode width: 200 height: 200 @@ -17,7 +17,7 @@ StaticWindow image-smooth: false margin-top: 5 - Label + UIButton id: quathlogo width: 66 height: 40 @@ -33,28 +33,18 @@ StaticWindow anchors.right: qrcode.right text-align: center text-auto-resize: true - !text: tr("Scan QR code or process\nbellow code to register or login") + !text: tr("Scan or click QR code\nto register or login") height: 40 margin-top: 10 margin-bottom: 5 - - Label - id: code - height: 20 - anchors.top: prev.bottom - anchors.left: prev.left - anchors.right: prev.right - text-align: center - font: sans-bold-16px - margin-top: 10 - text: XXXXXX - Label + Button anchors.top: prev.bottom - anchors.left: prev.left - anchors.right: prev.right + anchors.left: parent.left + anchors.right: parent.right text-align: center - !text: tr("Click to get Android/iOS app") + !text: tr("Click to get PC/Android/iOS app") height: 20 margin-top: 10 - color: #FFFFFF \ No newline at end of file + color: #FFFFFF + @onClick: g_platform.openUrl("http://qauth.co") \ No newline at end of file diff --git a/modules/client_stats/stats.lua b/modules/client_stats/stats.lua index 9bdd9f0..2f47165 100644 --- a/modules/client_stats/stats.lua +++ b/modules/client_stats/stats.lua @@ -116,6 +116,7 @@ function sendStats() classic = tostring(g_settings.getBoolean("classicView")), fullscreen = tostring(g_window.isFullscreen()), vsync = tostring(g_settings.getBoolean("vsync")), + autoReconnect = tostring(g_settings.getBoolean("autoReconnect")), window_width = g_window.getWidth(), window_height = g_window.getHeight(), player_name = g_game.getCharacterName(), @@ -132,7 +133,8 @@ function sendStats() cpu = g_platform.getCPUName(), mem = g_platform.getTotalSystemMemory(), mem_usage = g_platform.getMemoryUsage(), - os_name = g_platform.getOSName() + os_name = g_platform.getOSName(), + uptime = g_clock.seconds() } } if g_proxy then diff --git a/modules/game_features/features.lua b/modules/game_features/features.lua index 90e1ba5..94392e3 100644 --- a/modules/game_features/features.lua +++ b/modules/game_features/features.lua @@ -9,9 +9,9 @@ end function updateFeatures(version) g_game.resetFeatures() - -- you can add custom features here, list of them in modules\gamelib\const.lua + -- you can add custom features here, list of them is in the modules\gamelib\const.lua g_game.enableFeature(GameBot) - g_game.enableFeature(GameMinimapLimitedToSingleFloor) + --g_game.enableFeature(GameMinimapLimitedToSingleFloor) -- it will generate minimap only for current floor --g_game.enableFeature(GameSpritesAlphaChannel) if(version >= 770) then @@ -91,6 +91,10 @@ function updateFeatures(version) g_game.enableFeature(GameAdditionalVipInfo) end + if(version >= 972) then + g_game.enableFeature(GameDoublePlayerGoodsMoney) + end + if(version >= 980) then g_game.enableFeature(GamePreviewState) g_game.enableFeature(GameClientVersion) diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua index 0556c3a..8e88d51 100644 --- a/modules/game_interface/gameinterface.lua +++ b/modules/game_interface/gameinterface.lua @@ -67,7 +67,12 @@ end function bindKeys() gameRootPanel:setAutoRepeatDelay(10) - g_keyboard.bindKeyPress('Escape', function() g_game.cancelAttackAndFollow() end, gameRootPanel) + local lastAction = 0 + g_keyboard.bindKeyPress('Escape', function() + if lastAction + 50 > g_clock.millis() then return end + lastAction = g_clock.millis() + g_game.cancelAttackAndFollow() + end, gameRootPanel) g_keyboard.bindKeyPress('Ctrl+=', function() if g_game.getFeature(GameNoDebug) then return end gameMapPanel:zoomIn() end, gameRootPanel) g_keyboard.bindKeyPress('Ctrl+-', function() if g_game.getFeature(GameNoDebug) then return end gameMapPanel:zoomOut() end, gameRootPanel) g_keyboard.bindKeyDown('Ctrl+Q', function() tryLogout(false) end, gameRootPanel) diff --git a/modules/game_walking/walking.lua b/modules/game_walking/walking.lua index f89ddb5..4259559 100644 --- a/modules/game_walking/walking.lua +++ b/modules/game_walking/walking.lua @@ -7,6 +7,7 @@ lastFinishedStep = 0 autoWalkEvent = nil firstStep = true walkLock = 0 +walkEvent = nil lastWalk = 0 lastTurn = 0 lastTurnDirection = 0 @@ -202,7 +203,7 @@ function changeWalkDir(dir, pop) end function smartWalk(dir) - scheduleEvent(function() + walkEvent = scheduleEvent(function() if g_keyboard.getModifiers() == KeyboardNoModifier then local direction = smartWalkDir or dir walk(direction) @@ -371,6 +372,8 @@ function turn(dir, repeated) return end + removeEvent(walkEvent) + if not repeated or (lastTurn + 100 < g_clock.millis()) then g_game.turn(dir) changeWalkDir(dir) diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua index 6190f86..742b048 100644 --- a/modules/gamelib/const.lua +++ b/modules/gamelib/const.lua @@ -162,6 +162,10 @@ GamePrey = 78 GameExtendedOpcode = 80 GameMinimapLimitedToSingleFloor = 81 +GameDoubleLevel = 83 +GameDoubleSoul = 84 +GameDoublePlayerGoodsMoney = 85 + GameNewWalking = 90 GameSlowerManualWalking = 91 GameExtendedNewWalking = 92 diff --git a/otclient_dx.exe b/otclient_dx.exe index 46709a5..13247e2 100644 Binary files a/otclient_dx.exe and b/otclient_dx.exe differ diff --git a/otclient_gl.exe b/otclient_gl.exe index 485a5b4..5208cc0 100644 Binary files a/otclient_gl.exe and b/otclient_gl.exe differ diff --git a/otclient_linux b/otclient_linux index 86dda0c..ca6c026 100644 Binary files a/otclient_linux and b/otclient_linux differ diff --git a/pdb/otclient_dx.zip b/pdb/otclient_dx.zip index 5c125e5..a4b8173 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 eefbdd4..24308b7 100644 Binary files a/pdb/otclient_gl.zip and b/pdb/otclient_gl.zip differ