From cf8b21263d22039a9916fec66ba0a275e8b907ed Mon Sep 17 00:00:00 2001 From: OTCv8 Date: Thu, 28 Nov 2019 12:14:43 +0100 Subject: [PATCH] Added websocket login server and botserver --- api/websockets/app.ts | 190 +++++++++++------------ api/websockets/config.json | 70 ++++----- api/websockets/login.ts | 166 ++++++++++---------- api/websockets/package.json | 44 +++--- api/websockets/tsconfig.json | 22 +-- modules/corelib/http.lua | 11 +- modules/game_bot/bot.lua | 8 +- modules/game_bot/executor.lua | 4 +- modules/game_bot/functions/main.lua | 25 +-- modules/game_bot/functions/player.lua | 3 + modules/game_bot/functions/server.lua | 88 +++++++++++ modules/game_interface/gameinterface.lua | 3 +- 12 files changed, 371 insertions(+), 263 deletions(-) create mode 100644 modules/game_bot/functions/server.lua diff --git a/api/websockets/app.ts b/api/websockets/app.ts index 272e2a4..5b1f008 100644 --- a/api/websockets/app.ts +++ b/api/websockets/app.ts @@ -1,96 +1,96 @@ -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}`) - } +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 index 37b5ef6..a474eb6 100644 --- a/api/websockets/config.json +++ b/api/websockets/config.json @@ -1,36 +1,36 @@ -{ - "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" +{ + "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 index 1c64011..8303161 100644 --- a/api/websockets/login.ts +++ b/api/websockets/login.ts @@ -1,84 +1,84 @@ -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) { - +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 index cb6e0a3..4772a83 100644 --- a/api/websockets/package.json +++ b/api/websockets/package.json @@ -1,22 +1,22 @@ -{ - "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" - } -} +{ + "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 index c4d5e95..8cc8017 100644 --- a/api/websockets/tsconfig.json +++ b/api/websockets/tsconfig.json @@ -1,11 +1,11 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "lib": ["es6"], - "sourceMap": true - }, - "exclude": [ - "node_modules" - ] -} +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "lib": ["es6"], + "sourceMap": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/modules/corelib/http.lua b/modules/corelib/http.lua index 36c01aa..7103725 100644 --- a/modules/corelib/http.lua +++ b/modules/corelib/http.lua @@ -56,8 +56,11 @@ function HTTP.downloadImage(url, callback) return operation end -function HTTP.webSocket(url, callbacks, jsonWebsocket) - local operation = g_http.ws(url, HTTP.websocketTimeout) +function HTTP.webSocket(url, callbacks, timeout, jsonWebsocket) + if not timeout or timeout < 1 then + timeout = HTTP.websocketTimeout + end + local operation = g_http.ws(url, timeout) HTTP.operations[operation] = {type="ws", json=jsonWebsocket, url=url, callbacks=callbacks} return { id = operation, @@ -75,8 +78,8 @@ function HTTP.webSocket(url, callbacks, jsonWebsocket) end HTTP.WebSocket = HTTP.webSocket -function HTTP.webSocketJSON(url, callbacks) - return HTTP.webSocket(url, callbacks, true) +function HTTP.webSocketJSON(url, callbacks, timeout) + return HTTP.webSocket(url, callbacks, timeout, true) end HTTP.WebSocketJSON = HTTP.webSocketJSON diff --git a/modules/game_bot/bot.lua b/modules/game_bot/bot.lua index f899e84..a34f870 100644 --- a/modules/game_bot/bot.lua +++ b/modules/game_bot/bot.lua @@ -8,6 +8,7 @@ configEditorText = nil configList = nil botTabs = nil botPanel = nil +local botWebSockets = {} local botMessages = nil local configCopy = "" local enableButton = nil @@ -343,6 +344,11 @@ function clearConfig() 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 @@ -385,7 +391,7 @@ function refreshConfig() end errorOccured = false g_game.enableTileThingLuaCallback(false) - local status, result = pcall(function() return executeBot(config.script, config.storage, botTabs, botMsgCallback, saveConfig) end) + 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)) diff --git a/modules/game_bot/executor.lua b/modules/game_bot/executor.lua index 20eef95..dac63c9 100644 --- a/modules/game_bot/executor.lua +++ b/modules/game_bot/executor.lua @@ -1,8 +1,9 @@ -function executeBot(config, storage, tabs, msgCallback, saveConfigCallback) +function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, websockets) local context = {} context.tabs = tabs context.panel = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel context.saveConfig = saveConfigCallback + context._websockets = websockets context.storage = storage if context.storage._macros == nil then @@ -65,6 +66,7 @@ function executeBot(config, storage, tabs, msgCallback, saveConfigCallback) context.StaticText = StaticText context.Config = Config context.HTTP = HTTP + context.modules = modules -- log functions context.info = function(text) return msgCallback("info", tostring(text)) end diff --git a/modules/game_bot/functions/main.lua b/modules/game_bot/functions/main.lua index 4a87464..f6f078e 100644 --- a/modules/game_bot/functions/main.lua +++ b/modules/game_bot/functions/main.lua @@ -3,13 +3,10 @@ local context = G.botContext -- MAIN BOT FUNCTION -- macro(timeout, callback) -- macro(timeout, name, callback) +-- macro(timeout, name, callback, parent) -- macro(timeout, name, hotkey, callback) -- macro(timeout, name, hotkey, callback, parent) context.macro = function(timeout, name, hotkey, callback, parent) - if not parent then - parent = context.panel - end - if type(timeout) ~= 'number' or timeout < 1 then error("Invalid timeout for macro: " .. tostring(timeout)) end @@ -18,8 +15,9 @@ context.macro = function(timeout, name, hotkey, callback, parent) name = "" hotkey = "" elseif type(hotkey) == 'function' then + parent = callback callback = hotkey - hotkey = "" + hotkey = "" elseif type(callback) ~= 'function' then error("Invalid callback for macro: " .. tostring(callback)) end @@ -29,6 +27,9 @@ context.macro = function(timeout, name, hotkey, callback, parent) if type(name) ~= 'string' or type(hotkey) ~= 'string' then error("Invalid name or hotkey for macro") end + if not parent then + parent = context.panel + end if hotkey:len() > 0 then hotkey = retranslateKeyComboDesc(hotkey) end @@ -63,16 +64,18 @@ context.macro = function(timeout, name, hotkey, callback, parent) end -- hotkey(keys, callback) +-- hotkey(keys, callback, parent) -- hotkey(keys, name, callback) -- hotkey(keys, name, callback, parent) -context.hotkey = function(keys, name, callback, single, parent) - if not parent then - parent = context.panel - end +context.hotkey = function(keys, name, callback, parent, single) if type(name) == 'function' then + parent = callback callback = name name = "" end + if not parent then + parent = context.panel + end keys = retranslateKeyComboDesc(keys) if not keys or #keys == 0 then return context.error("Invalid hotkey keys " .. tostring(name)) @@ -107,14 +110,16 @@ context.hotkey = function(keys, name, callback, single, parent) end -- singlehotkey(keys, callback) +-- singlehotkey(keys, callback, parent) -- singlehotkey(keys, name, callback) -- singlehotkey(keys, name, callback, parent) context.singlehotkey = function(keys, name, callback, parent) if type(name) == 'function' then + parent = callback callback = name name = "" end - return context.hotkey(keys, name, callback, true, parent) + return context.hotkey(keys, name, callback, parent, true) end -- schedule(timeout, callback) diff --git a/modules/game_bot/functions/player.lua b/modules/game_bot/functions/player.lua index e8f7615..f7a89f3 100644 --- a/modules/game_bot/functions/player.lua +++ b/modules/game_bot/functions/player.lua @@ -92,6 +92,9 @@ context.sayNPC = context.talkNpc context.talkNPC = context.talkNpc context.saySpell = function(text, lastSpellTimeout) + if not text or text:len() < 1 then + return + end if context.lastSpell == nil then context.lastSpell = 0 end diff --git a/modules/game_bot/functions/server.lua b/modules/game_bot/functions/server.lua new file mode 100644 index 0000000..5174e4e --- /dev/null +++ b/modules/game_bot/functions/server.lua @@ -0,0 +1,88 @@ +local context = G.botContext + +context.BotServer = {} +context.BotServer.url = "ws://bot.otclient.ovh:8000/" +context.BotServer.timeout = 3 +context.BotServer._callbacks = {} +context.BotServer._lastMessageId = 0 +context.BotServer._wasConnected = true -- show first warning + +context.BotServer.init = function(name, channel) + if not channel or not name or channel:len() < 1 or name:len() < 1 then + return context.error("Invalid params for BotServer.init") + end + if context.BotServer._websocket then + return context.error("BotServer is already initialized") + end + context.BotServer._websocket = HTTP.WebSocketJSON(context.BotServer.url, { + onMessage = function(message, socketId) + if not context._websockets[socketId] then + return g_http.cancel(socketId) + end + if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then + return g_http.cancel(socketId) + end + context.BotServer._wasConnected = true + if message["type"] == "ping" then + return context.BotServer._websocket.send({type="ping"}) + end + if message["type"] == "message" then + context.BotServer._lastMessageId = message["id"] + local topics = context.BotServer._callbacks[message["topic"]] + if topics then + for i=1,#topics do + topics[i](message["name"], message["message"], message["topic"]) + end + end + topics = context.BotServer._callbacks["*"] + if topics then + for i=1,#topics do + topics[i](message["name"], message["message"], message["topic"]) + end + end + return + end + end, + onClose = function(message, socketId) + if not context._websockets[socketId] then + return + end + context._websockets[socketId] = nil + if not context.BotServer._websocket or context.BotServer._websocket.id ~= socketId then + return + end + if context.BotServer._wasConnected then + context.warn("BotServer disconnected") + end + context.BotServer._wasConnected = false + context.BotServer._websocket = nil + context.BotServer.init(name, channel) + end + }, context.BotServer.timeout) + context._websockets[context.BotServer._websocket.id] = 1 + context.BotServer._websocket.send({type="init", name=name, channel=channel, lastMessage=context.BotServer._lastMessageId}) +end + +context.BotServer.terminate = function() + if context.BotServer._websocket then + context.BotServer._websocket:close() + context.BotServer._websocket = nil + end +end + +context.BotServer.listen = function(topic, callback) -- callback = function(name, message, topic) -- message is parsed json = table + if not context.BotServer._websocket then + return context.error("BotServer is not initialized") + end + if not context.BotServer._callbacks[topic] then + context.BotServer._callbacks[topic] = {} + end + table.insert(context.BotServer._callbacks[topic], callback) +end + +context.BotServer.send = function(topic, message) + if not context.BotServer._websocket then + return context.error("BotServer is not initialized") + end + context.BotServer._websocket.send({type="message", topic=topic, message=message}) +end diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua index be347c5..0556c3a 100644 --- a/modules/game_interface/gameinterface.lua +++ b/modules/game_interface/gameinterface.lua @@ -871,7 +871,8 @@ function refreshViewMode() gameBottomPanel:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft) bottomSplitter:addAnchor(AnchorLeft, 'gameLeftPanels', AnchorRight) bottomSplitter:addAnchor(AnchorRight, 'gameRightPanels', AnchorLeft) - + bottomSplitter:setMarginLeft(0) + modules.client_topmenu.getTopMenu():setImageColor('white') gameBottomPanel:setImageColor('white') g_game.changeMapAwareRange(19, 15)