-- =========================================================================== -- ServerCheck (CLIENTE) v1 -- Tecla F9: abre/cierra una ventana con los datos de tu personaje segun el -- SERVIDOR y, al lado, lo que muestra el CLIENTE. Marca en rojo las diferencias. -- -- Ubicacion: Data\Custom\Script\Scripts\ServerCheck\ServerCheck.lua -- Carga: requirefolder('Scripts\\ServerCheck') en el ScriptMain del cliente. -- Requiere: System\ProtocolCore, Util\Defines y Util\KeysValue ya cargados. -- -- Solo ASCII en los textos: el motor mostro problemas de codificacion con tildes. -- =========================================================================== local CFG = { Key = Keys.F9, ReqName = "InfoReq", ResName = "InfoRes", MinIntervalSec = 2, -- no pedir mas seguido (el servidor tambien lo limita) TimeoutSec = 3, -- sin respuesta tras este tiempo -> aviso Scene = 5, -- GetMainScene() == 5: dentro del juego } local PROTO_VERSION = 1 -- ORDEN FIJO: igual que FIELDS en el servidor. -- client = nombre de funcion del cliente con la que comparar (nil = no hay) -- flag = true -> marcar en rojo si difiere (valores estables) local ROWS = { { key = "Level", label = "Nivel", client = "UserGetLevel", flag = true }, { key = "MasterLevel", label = "M.Nivel", client = "UserGetMasterLevel", flag = true }, { key = "Reset", label = "Reset", client = "GetViewReset", flag = true }, { key = "MasterReset", label = "M.Reset", client = "GetViewMasterReset", flag = true }, { key = "Money", label = "Zen", client = nil, flag = false }, { key = "Map", label = "Mapa", client = "UserGetMap", flag = true }, { key = "MapX", label = "X", client = nil, flag = false }, { key = "MapY", label = "Y", client = nil, flag = false }, { key = "Life", label = "Vida", client = "GetViewCurHP", flag = false }, { key = "MaxLife", label = "VidaMax", client = "GetViewMaxHP", flag = true }, { key = "Mana", label = "Mana", client = "GetViewCurMP", flag = false }, { key = "MaxMana", label = "ManaMax", client = "GetViewMaxMP", flag = true }, { key = "Strength", label = "Fuerza", client = "GetViewStrength", flag = true }, { key = "Dexterity", label = "Agilidad", client = "GetViewDexterity", flag = true }, { key = "Vitality", label = "Vitalid.", client = "GetViewVitality", flag = true }, { key = "Energy", label = "Energia", client = "GetViewEnergy", flag = true }, { key = "Leadership", label = "Comando", client = "GetViewLeadership", flag = true }, { key = "AccountLevel", label = "Cuenta", client = "GetViewAccountLevel", flag = true }, } local State = { open = false, data = nil, -- tabla key -> valor del servidor lastReq = 0, lastRecv = 0, waiting = false, badFmt = false, } -- Rectangulos de botones (se recalculan al dibujar, usan GetWideX). local Hit = { close = { x = 0, y = 0, w = 0, h = 0 }, refresh = { x = 0, y = 0, w = 0, h = 0 }, } BridgeFunctionAttach('KeyboardEvent', 'ServerCheck_Key') BridgeFunctionAttach('MainInterfaceProcThread', 'ServerCheck_Draw') BridgeFunctionAttach('UpdateMouseEvent', 'ServerCheck_Mouse') BridgeFunctionAttach('UpdateKeyEvent', 'ServerCheck_UpdateKey') -- --------------------------------------------------------------------------- -- Lectura de paquetes -- --------------------------------------------------------------------------- local function ReadByte(name, idx) local v = tonumber(GetBytePacket(name, idx)) or 0 return math.floor(v) & 0xFF -- tolera que el motor devuelva bytes con signo end local function Read32(name, idx) return ReadByte(name, idx) | (ReadByte(name, idx + 1) << 8) | (ReadByte(name, idx + 2) << 16) | (ReadByte(name, idx + 3) << 24) end local function ParseResponse(name) -- Diagnostico: primeros 12 bytes tal como los devuelve el motor. local dump = {} for i = 0, 11 do dump[#dump + 1] = tostring(ReadByte(name, i)) end -- Busca donde empieza nuestra cabecera (version, cantidad de campos) en los -- primeros 4 indices. Asi toleramos que el indice 0 del cliente no sea el -- mismo que el del servidor, y dejamos escrito en consola el offset real. local base = nil for off = 0, 3 do if ReadByte(name, off) == PROTO_VERSION and ReadByte(name, off + 1) == #ROWS then base = off break end end if base == nil then Console(2, "[ServerCheck] formato inesperado. bytes 0..11: " .. table.concat(dump, " ")) return nil end if base ~= 0 then Console(2, "[ServerCheck] cabecera encontrada en offset " .. base .. " | bytes 0..11: " .. table.concat(dump, " ")) end local data = {} local idx = base + 2 for i = 1, #ROWS do data[ROWS[i].key] = Read32(name, idx) idx = idx + 4 end return data end function ServerCheck_OnPacket(Packet, PacketName) if PacketName ~= CFG.ResName then return false end local ok, result = pcall(ParseResponse, PacketName) if ok and result then State.data = result State.lastRecv = os.time() State.badFmt = false else State.badFmt = true if not ok then Console(2, "[ServerCheck] error leyendo respuesta: " .. tostring(result)) end end State.waiting = false ClearPacket(PacketName) return true end ProtocolFunctions.ClientProtocol(ServerCheck_OnPacket) -- --------------------------------------------------------------------------- -- Pedido -- --------------------------------------------------------------------------- local function Request() if GetMainScene() ~= CFG.Scene then return end local now = os.time() if (now - State.lastReq) < CFG.MinIntervalSec then return end State.lastReq = now State.waiting = true CreatePacket(CFG.ReqName, 1) SetBytePacket(CFG.ReqName, PROTO_VERSION) SendPacket(CFG.ReqName) ClearPacket(CFG.ReqName) end local function OpenWin() if GetMainScene() ~= CFG.Scene then return end State.open = true Request() end local function CloseWin() State.open = false end -- --------------------------------------------------------------------------- -- Entrada -- --------------------------------------------------------------------------- function ServerCheck_Key(key) if CheckWindowOpen(UIChatWindow) == 1 then return end if key == CFG.Key then if State.open then CloseWin() else OpenWin() end return end if State.open and key == Keys.Escape then CloseWin() end end local function Inside(r, mx, my) return mx >= r.x and mx <= (r.x + r.w) and my >= r.y and my <= (r.y + r.h) end function ServerCheck_Mouse() if not State.open then return end if CheckReleasedKey(Keys.LButton) ~= 1 then return end local mx, my = MousePosX(), MousePosY() if Inside(Hit.close, mx, my) then CloseWin() elseif Inside(Hit.refresh, mx, my) then Request() end end function ServerCheck_UpdateKey() if not State.open then return end if CheckWindowOpen(UIChatWindow) == 1 then return end DisableClickClient() -- evita que un clic sobre la ventana mueva al personaje end -- --------------------------------------------------------------------------- -- Dibujo (mismas llamadas y recursos que NewWindow.lua, que ya funcionaba) -- --------------------------------------------------------------------------- local function Round(v) return math.floor((tonumber(v) or 0) + 0.5) end local function ClientValue(fnName) if fnName == nil then return nil end local fn = _G[fnName] if type(fn) ~= "function" then return nil end local ok, v = pcall(fn) if ok and tonumber(v) ~= nil then return Round(v) end return nil end function ServerCheck_Draw() if not State.open then return end -- Si se abre inventario o personaje (ocupan el mismo sitio), nos cerramos. if CheckWindowOpen(UIInventory) == 1 or CheckWindowOpen(UICharacter) == 1 then CloseWin() return end local x = GetWideX() - 275 RenderImage(31322, x, 0, 190, 428) RenderImage(31353, x, 0, 190, 64) RenderImage(31355, x, 64, 21, 320) RenderImage(31356, x + 169, 64, 21, 320) RenderImage(31357, x, 384, 190, 45) SetFontType(1) SetTextBg(0, 0, 0, 0) SetTextColor(255, 255, 255, 255) RenderText5(x, 12, "Servidor vs Cliente", 190, ALIGN_CENTER) -- Boton cerrar (area 14x14, igual que el ejemplo) Hit.close.x, Hit.close.y, Hit.close.w, Hit.close.h = x + 170, 6, 14, 14 if Inside(Hit.close, MousePosX(), MousePosY()) then DrawTooltip(x + 150, 20, "Cerrar") end -- Cabecera de columnas SetTextColor(200, 200, 120, 255) RenderText5(x + 12, 50, "Dato", 55, ALIGN_LEFT) RenderText5(x + 60, 50, "Servidor", 70, ALIGN_RIGHT) RenderText5(x + 130, 50, "Cliente", 48, ALIGN_RIGHT) -- Filas local y = 66 local diffs = 0 for i = 1, #ROWS do local row = ROWS[i] local srv = State.data and State.data[row.key] or nil local cli = ClientValue(row.client) local mismatch = row.flag and srv ~= nil and cli ~= nil and Round(srv) ~= cli if mismatch then diffs = diffs + 1 SetTextColor(255, 90, 90, 255) else SetTextColor(255, 255, 255, 255) end RenderText5(x + 12, y, row.label, 55, ALIGN_LEFT) RenderText5(x + 60, y, srv ~= nil and string.format("%d", srv) or "-", 70, ALIGN_RIGHT) RenderText5(x + 130, y, cli ~= nil and string.format("%d", cli) or "-", 48, ALIGN_RIGHT) y = y + 14 end -- Estado local status if State.badFmt then status = "Formato inesperado (ver consola)" elseif State.waiting and (os.time() - State.lastReq) >= CFG.TimeoutSec then status = "Sin respuesta (script GM?)" elseif State.waiting then status = "Esperando respuesta..." elseif State.data then status = string.format("Diferencias: %d (hace %ds)", diffs, os.time() - State.lastRecv) else status = "Sin datos" end SetTextColor(255, 255, 255, 255) RenderText5(x, 332, status, 190, ALIGN_CENTER) -- Boton refrescar (misma textura 31326 y coordenadas que el boton OK del ejemplo) Hit.refresh.x, Hit.refresh.y, Hit.refresh.w, Hit.refresh.h = x + 48, 350, 110, 25 if Inside(Hit.refresh, MousePosX(), MousePosY()) then RenderImage2(31326, x + 48, 350, 110, 25, 0, 0.2264566, 1.0, 0.2245212, 1, 1, 1.0) else RenderImage2(31326, x + 48, 350, 110, 25, 0, 0, 1.0, 0.2245212, 1, 1, 1.0) end SetTextColor(255, 255, 255, 255) RenderText5(x, 357, "Refrescar (F9 cierra)", 190, ALIGN_CENTER) end Console(1, "[ServerCheck] cargado - F9 abre la ventana")