-- =========================================================================== -- GameWebCompose (CLIENTE) - ventana para escribir saludos largos a la radio -- -- Se abre SOLA cuando el jugador escribe /saludo (sin texto) en el chat: el servidor -- (GameWeb.lua v3) manda un paquete y esta ventana aparece. No tiene tecla propia. -- * Tipo de saludo: General / Jugador / Gremio (clic). Jugador y Gremio piden el nombre. -- * Tab cambia de casilla, Ctrl+V pega, Enter envia, Esc cancela. Tambien hay botones. -- * El servidor valida igual que el comando de chat (palabras, links, spam, 1 cada 5 min). -- Si rechaza, la ventana sigue abierta con el motivo en rojo y tu texto intacto. -- * Solo ASCII: sin tildes, la enie se escribe como n (lo mismo que acepta el servidor). -- -- Ubicacion: Data\Custom\Script\Scripts\GameWeb\GameWebCompose.lua -- (requirefolder('Scripts\\GameWeb') en el ScriptMain del cliente) -- Imagenes: las rk_*.ozt del ranking (ya instaladas). -- Cualquier problema queda en GameWebCompose.txt (junto al juego) si Debug = true. -- =========================================================================== local CFG = { Debug = false, Scene = 5, ReqName = "GWReq", ResName = "GWRes", EnterSends = true, -- Enter envia (si el chat del juego se abre solo, ponlo en false: Ctrl+Enter / boton) LockGame = true, -- intenta bloquear teclas/caminar mientras escribes PosFile = "GameWebCompose_pos.txt", ClearHoldSec = 2, -- manteniendo Borrar este tiempo (s) se vacia la casilla ReplyWaitS = 8, -- si el servidor no contesta, se avisa LogFile = "GameWebCompose.txt", } local PROTO_VERSION = 1 local IMG = { tl = 40190, tm = 40191, tr = 40192, ml = 40193, mm = 40194, mr = 40195, bl = 40196, bm = 40197, br = 40198 } local IMG_FILES = { [40190] = "rk_tl", [40191] = "rk_tm", [40192] = "rk_tr", [40193] = "rk_ml", [40194] = "rk_mm", [40195] = "rk_mr", [40196] = "rk_bl", [40197] = "rk_bm", [40198] = "rk_br" } local S = { open = false, loaded = false, kind = 0, -- 0 general, 1 jugador, 2 gremio dest = "", msg = "", -- borrador (se conserva si el servidor rechaza) field = 2, -- 1 = destinatario, 2 = mensaje maxChars = 220, minChars = 10, minutes = 5, status = "", statusErr = false, sending = false, sentAt = 0, clipWait = false, clipAt = 0, backSince = nil, backCleared = false, backNext = 0, locked = false, unlockUntil = 0, dx = 0, dy = 0, moving = false, grabX = 0, grabY = 0, } BridgeFunctionAttach('KeyboardEvent', 'GWC_Key') BridgeFunctionAttach('MainInterfaceProcThread', 'GWC_Draw') BridgeFunctionAttach('UpdateMouseEvent', 'GWC_Mouse') BridgeFunctionAttach('UpdateKeyEvent', 'GWC_UpdateKey') local function Log(msg) if not CFG.Debug then return end local ok, f = pcall(io.open, CFG.LogFile, "a") if ok and f then f:write(os.date("%H:%M:%S "), msg, "\n"); f:close() end end local function Held(code) if code == nil then return false end local ok, v = pcall(CheckRepeatKey, code) if ok and (v == 1 or v == true) then return true end local ok2, v2 = pcall(CheckPressedKey, code) return ok2 and (v2 == 1 or v2 == true) end -- --------------------------------------------------------------------------- -- Bloqueo del juego mientras se escribe (se intenta; si la funcion no existe, se sigue sin ella) -- --------------------------------------------------------------------------- -- Desbloqueo: UnlockPlayerWalk existe en el cliente; LockPlayerWalk(0) NO se usa (podia dejar el mouse bloqueado). local function Unlock() if type(SetUnlockInterfaces) == "function" then pcall(SetUnlockInterfaces) end if type(UnlockPlayerWalk) == "function" then pcall(UnlockPlayerWalk) end end local function Lock(on) if not CFG.LockGame then return end if on == S.locked then return end S.locked = on if on then if type(SetLockInterfaces) == "function" then pcall(SetLockInterfaces) end if type(LockPlayerWalk) == "function" then pcall(LockPlayerWalk, 1) end else S.unlockUntil = os.clock() + 1.5 -- refuerzo: se repite el desbloqueo un momento Unlock() end end -- Bloqueo total mientras la ventana esta abierta: cada cuadro se cierran las ventanas propias del -- cliente (item bank con J, etc.) y las del juego que se abran por tecla (inventario, personaje, mapa...). -- El chat no se toca (lo usas para escribir /saludo antes de abrir esta ventana). local BLOCK_UI = { UIInventory, UICharacter, UIWarehouse, UIShop, UIStore, UITrade, UIGuild, UIParty, UIQuest, UISkillTree, UIMoveList, UICashShop, UIFullMap, UIFriendList, UIExpandInventory, UIExpandWarehouse, UIPetInfo, UICommandWindow, UIHelp, UIOptions, UIFastMenu, UIMuHelper, UIGensInfo } local function KillGameWindows() if type(CloseAllCustomWindow) == "function" then pcall(CloseAllCustomWindow) end if type(CloseWindow) == "function" and type(CheckWindowOpen) == "function" then for _, id in pairs(BLOCK_UI) do local ok, v = pcall(CheckWindowOpen, id) if ok and v == 1 then pcall(CloseWindow, id) end end end end -- Posicion (se guarda; clic en la cabecera = tomar / soltar, clic derecho = restablecer) local function LoadPos() local ok, f = pcall(io.open, CFG.PosFile, "r") if ok and f then local a, b = f:read("*l"), f:read("*l") f:close() S.dx, S.dy = tonumber(a) or 0, tonumber(b) or 0 end end local function SavePos() local ok, f = pcall(io.open, CFG.PosFile, "w") if ok and f then f:write(tostring(S.dx), "\n", tostring(S.dy), "\n"); f:close() end end LoadPos() local function Close() S.open = false if S.moving then S.moving = false; SavePos() end Lock(false) end local function Open() if GetMainScene() ~= CFG.Scene then return end S.open, S.status, S.statusErr, S.sending = true, "", false, false if S.kind ~= 0 and S.dest == "" then S.field = 1 else S.field = 2 end Lock(true) Log("ventana abierta") end -- --------------------------------------------------------------------------- -- Teclado -> caracteres (teclado Espanol Latinoamerica; solo ASCII) -- --------------------------------------------------------------------------- local SHIFT_DIGITS = { [0] = "=", "!", "\"", "#", "$", "%", "&", "/", "(", ")" } local function CharFor(code, shift) if code >= 65 and code <= 90 then local c = string.char(code) return shift and c or string.lower(c) end if code >= 48 and code <= 57 then local d = code - 48 return shift and SHIFT_DIGITS[d] or tostring(d) end if code >= 96 and code <= 105 then return tostring(code - 96) end if code == 32 then return " " end if code == 190 then return shift and ":" or "." end if code == 188 then return shift and ";" or "," end if code == 189 then return shift and "_" or "-" end if code == 187 then return shift and "*" or "+" end if code == 219 then return shift and "?" or "'" end if code == 192 then return shift and "N" or "n" end return nil end local function Active() return S.field == 1 and "dest" or "msg" end local function Limit() return S.field == 1 and 40 or S.maxChars end local function Append(text) local k = Active() S[k] = string.sub(S[k] .. text, 1, Limit()) end -- --------------------------------------------------------------------------- -- Pegar (PowerShell oculto, como la radio) -- --------------------------------------------------------------------------- local function StartClipboard() pcall(os.remove, "GameWebCompose_clip.txt") local ok, f = pcall(io.open, "GameWebCompose_clip.ps1", "w") if not ok or not f then return end f:write("$t = [string](Get-Clipboard -Raw)\r\n[IO.File]::WriteAllText((Join-Path $PSScriptRoot 'GameWebCompose_clip.txt'), $t, [Text.Encoding]::ASCII)\r\n") f:close() pcall(io.popen, 'cmd /c start "" /min powershell -NoProfile -Sta -ExecutionPolicy Bypass -WindowStyle Hidden -File GameWebCompose_clip.ps1') S.clipWait, S.clipAt = true, os.clock() end local function PollClipboard() if not S.clipWait then return end local ok, f = pcall(io.open, "GameWebCompose_clip.txt", "r") if ok and f then local t = f:read("*a") or "" f:close() S.clipWait = false if S.open then Append((t:gsub("[%c]+", " "):gsub("[^\32-\126]", "?"))) end elseif os.clock() - S.clipAt > 6 then S.clipWait = false end end -- --------------------------------------------------------------------------- -- Enviar al servidor -- --------------------------------------------------------------------------- local function Send() if S.sending then return end local msg = S.msg:gsub("^%s+", ""):gsub("%s+$", "") local dest = S.dest:gsub("^%s+", ""):gsub("%s+$", "") if S.kind ~= 0 and dest == "" then S.status, S.statusErr, S.field = "Escribe el nombre del " .. (S.kind == 1 and "jugador." or "gremio."), true, 1 return end if #msg < S.minChars then S.status, S.statusErr = string.format("Muy corto: minimo %d letras.", S.minChars), true return end if S.kind == 0 then dest = "" end CreatePacket(CFG.ReqName, 1) SetBytePacket(CFG.ReqName, PROTO_VERSION) SetBytePacket(CFG.ReqName, S.kind) SetBytePacket(CFG.ReqName, #dest) for i = 1, #dest do SetBytePacket(CFG.ReqName, string.byte(dest, i)) end SetBytePacket(CFG.ReqName, #msg & 0xFF) SetBytePacket(CFG.ReqName, (#msg >> 8) & 0xFF) for i = 1, #msg do SetBytePacket(CFG.ReqName, string.byte(msg, i)) end SendPacket(CFG.ReqName) ClearPacket(CFG.ReqName) S.sending, S.sentAt = true, os.clock() S.status, S.statusErr = "Enviando...", false Log("enviado " .. #msg .. " letras") end -- --------------------------------------------------------------------------- -- Respuesta del servidor -- --------------------------------------------------------------------------- local function RB(name, idx) return math.floor(tonumber(GetBytePacket(name, idx)) or 0) & 0xFF end function GWC_OnPacket(Packet, PacketName) if PacketName ~= CFG.ResName then return false end local ok, err = pcall(function() local idx = 0 local function u8() local v = RB(PacketName, idx); idx = idx + 1; return v end if u8() ~= PROTO_VERSION then return end local op = u8() if op == 1 then local lo = u8(); S.maxChars = lo | (u8() << 8) S.minutes, S.minChars = u8(), u8() if S.maxChars < 20 or S.maxChars > 1000 then S.maxChars = 220 end Open() elseif op == 2 then S.msg, S.dest, S.kind = "", "", 0 Close() elseif op == 3 then local n = u8(); local t = {} for i = 1, math.min(n, 120) do t[i] = string.char(u8()) end S.sending, S.status, S.statusErr = false, table.concat(t), true end end) if not ok then Log("error leyendo respuesta: " .. tostring(err)) end ClearPacket(PacketName) return true end ProtocolFunctions.ClientProtocol(GWC_OnPacket) -- --------------------------------------------------------------------------- -- Entrada -- --------------------------------------------------------------------------- function GWC_Key(key) if not S.open then return end KillGameWindows() local code = key & 0xFFFF local mods = key & 0xFFFF0000 local shift = (mods & 65536) ~= 0 or Held(Keys.ShiftKey) or Held(Keys.LShiftKey) or Held(Keys.RShiftKey) local ctrl = (mods & 131072) ~= 0 or Held(Keys.ControlKey) or Held(Keys.LControlKey) or Held(Keys.RControlKey) if code == Keys.Escape then Close(); return end if S.sending then return end if code == Keys.Tab then if S.kind ~= 0 then S.field = (S.field == 1) and 2 or 1 end return end if code == Keys.Back then local k = Active() if ctrl then S[k] = "" -- Ctrl+Borrar: vacia la casilla entera else S[k] = string.sub(S[k], 1, -2) end S.backNext = 0 return end if code == Keys.Return then if S.field == 1 and S.msg == "" then S.field = 2 elseif CFG.EnterSends or ctrl then Send() end return end if ctrl and code == Keys.V then StartClipboard(); return end if ctrl then return end local c = CharFor(code, shift) if c then Append(c) end end -- --------------------------------------------------------------------------- -- Dibujo -- --------------------------------------------------------------------------- local W, H = 330, 214 local function Pos() if S.moving then S.dx = MousePosX() - S.grabX S.dy = MousePosY() - S.grabY end local bx, by = 60, 70 S.dx = math.max(-bx, math.min(GetWideX() - W - bx, S.dx)) S.dy = math.max(-by, math.min(470 - H - by, S.dy)) return bx + S.dx, by + S.dy end local function Img(id, x, y, w, h) RenderImage2(id, x, y, w, h, 0.0, 0.0, 1.0, 1.0, 1, 1, 1.0) end local function Frame(x, y, w, h) local c = 8 Img(IMG.tl, x, y, c, c); Img(IMG.tm, x + c, y, w - 2 * c, c); Img(IMG.tr, x + w - c, y, c, c) Img(IMG.ml, x, y + c, c, h - 2 * c); Img(IMG.mm, x + c, y + c, w - 2 * c, h - 2 * c); Img(IMG.mr, x + w - c, y + c, c, h - 2 * c) Img(IMG.bl, x, y + h - c, c, c); Img(IMG.bm, x + c, y + h - c, w - 2 * c, c); Img(IMG.br, x + w - c, y + h - c, c, c) end local function Text(x, y, s, w, align, r, g, b) SetTextColor(r, g, b, 255); RenderText5(x, y, s, w, align) 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 local Hit = {} local function Wrap(s, n) local out = {} local line = "" for word in s:gmatch("%S+%s*") do while #word > n do if #line > 0 then out[#out + 1] = line; line = "" end out[#out + 1] = string.sub(word, 1, n); word = string.sub(word, n + 1) end if #line + #word > n then out[#out + 1] = line; line = word else line = line .. word end end out[#out + 1] = line return out end function GWC_Draw() if not S.loaded then S.loaded = true for id, f in pairs(IMG_FILES) do LoadBitmap("Custom\\ScriptImages\\" .. f .. ".tga", id) end end PollClipboard() if not S.open then if S.unlockUntil > 0 then if os.clock() < S.unlockUntil then Unlock() else S.unlockUntil = 0 end end return end KillGameWindows() if GetMainScene() ~= CFG.Scene then Close(); return end -- Borrar mantenido (el juego avisa una sola vez por pulsacion, asi que se mide aqui): -- desde 0.4 s va borrando letra por letra y a los 2 s vacia la casilla entera. if not S.sending then if Held(Keys.Back) then local now = os.clock() S.backSince = S.backSince or now if not S.backCleared then if now - S.backSince >= CFG.ClearHoldSec then S[Active()] = "" S.backCleared = true elseif now - S.backSince >= 0.4 and now >= S.backNext then local k = Active() S[k] = string.sub(S[k], 1, -2) S.backNext = now + 0.05 end end else S.backSince, S.backCleared = nil, nil end end local x, y = Pos() local mx, my = MousePosX(), MousePosY() Frame(x, y, W, H) SetFontType(1); SetTextBg(0, 0, 0, 0) Text(x, y + 10, "SALUDO A LA RADIO", W, ALIGN_CENTER, 255, 215, 90) -- tipo local kinds = { "General", "Jugador", "Gremio" } local cx = x + 14 Hit.kind = {} for i, label in ipairs(kinds) do local r = { x = cx, y = y + 28, w = 64, h = 18 } Hit.kind[i] = r Frame(r.x, r.y, r.w, r.h) local on, hov = (S.kind == i - 1), Inside(r, mx, my) Text(r.x, r.y + 5, label, r.w, ALIGN_CENTER, (on or hov) and 255 or 190, on and 215 or ((hov and 255) or 190), on and 90 or ((hov and 255) or 200)) cx = cx + 70 end -- destinatario local cur = (math.floor(os.clock() * 2) % 2 == 0) and "|" or " " Hit.dest = { x = x + 14, y = y + 52, w = W - 28, h = 20 } Frame(Hit.dest.x, Hit.dest.y, Hit.dest.w, Hit.dest.h) if S.kind ~= 0 then if S.dest == "" and S.field ~= 1 then Text(Hit.dest.x + 8, Hit.dest.y + 6, (S.kind == 1) and "Nombre del jugador" or "Nombre del gremio", Hit.dest.w - 16, ALIGN_LEFT, 120, 120, 130) else Text(Hit.dest.x + 8, Hit.dest.y + 6, S.dest .. ((S.field == 1) and cur or ""), Hit.dest.w - 16, ALIGN_LEFT, 255, 255, 255) end else Text(Hit.dest.x + 8, Hit.dest.y + 6, "Para toda la comunidad", Hit.dest.w - 16, ALIGN_LEFT, 120, 120, 130) end -- mensaje Hit.msg = { x = x + 14, y = y + 78, w = W - 28, h = 66 } Frame(Hit.msg.x, Hit.msg.y, Hit.msg.w, Hit.msg.h) if S.msg == "" and S.field ~= 2 then Text(Hit.msg.x + 8, Hit.msg.y + 7, "Escribe tu saludo...", Hit.msg.w - 16, ALIGN_LEFT, 120, 120, 130) else local lines = Wrap(S.msg .. ((S.field == 2) and cur or ""), 44) local ty = Hit.msg.y + 7 for i = math.max(1, #lines - 3), #lines do Text(Hit.msg.x + 8, ty, lines[i], Hit.msg.w - 16, ALIGN_LEFT, 255, 255, 255) ty = ty + 13 end end -- pie: contador / estado Text(x + 14, y + 150, string.format("%d / %d - un saludo cada %d min", #S.msg, S.maxChars, S.minutes), W - 28, ALIGN_LEFT, 160, 150, 120) if S.status ~= "" then if S.statusErr then Text(x + 14, y + 164, S.status, W - 28, ALIGN_LEFT, 255, 110, 100) else Text(x + 14, y + 164, S.status, W - 28, ALIGN_LEFT, 130, 255, 150) end else Text(x + 14, y + 164, "Borrar 2 s = vaciar todo - Esc", W - 28, ALIGN_LEFT, 130, 130, 140) end -- botones Hit.send = { x = x + W - 154, y = y + 184, w = 66, h = 20 } Hit.cancel = { x = x + W - 82, y = y + 184, w = 66, h = 20 } Frame(Hit.send.x, Hit.send.y, Hit.send.w, Hit.send.h) Frame(Hit.cancel.x, Hit.cancel.y, Hit.cancel.w, Hit.cancel.h) local hs = Inside(Hit.send, mx, my) and not S.sending Text(Hit.send.x, Hit.send.y + 6, S.sending and "..." or "Enviar", Hit.send.w, ALIGN_CENTER, hs and 255 or 120, 255, hs and 255 or 140) Text(Hit.cancel.x, Hit.cancel.y + 6, "Cancelar", Hit.cancel.w, ALIGN_CENTER, Inside(Hit.cancel, mx, my) and 255 or 200, 200, 200) Hit.head = { x = x, y = y, w = W, h = 24 } Hit.win = { x = x, y = y, w = W, h = H } if Inside(Hit.head, mx, my) and not S.moving then Text(x, y + 10, "SALUDO A LA RADIO", W, ALIGN_CENTER, 255, 235, 150) end end function GWC_Mouse() if not S.open then return end local mx, my = MousePosX(), MousePosY() -- Ventana tomada: cualquier clic la suelta if S.moving then if CheckReleasedKey(Keys.LButton) == 1 then S.moving = false; SavePos() end return end if CheckReleasedKey(Keys.RButton) == 1 and Hit.head and Inside(Hit.head, mx, my) then S.dx, S.dy = 0, 0 SavePos() return end if CheckReleasedKey(Keys.LButton) ~= 1 then return end if Hit.head and Inside(Hit.head, mx, my) then S.moving = true S.grabX, S.grabY = mx - S.dx, my - S.dy return end for i, r in ipairs(Hit.kind or {}) do if Inside(r, mx, my) then S.kind = i - 1 S.field = (S.kind ~= 0) and 1 or 2 return end end if Hit.dest and Inside(Hit.dest, mx, my) and S.kind ~= 0 then S.field = 1; return end if Hit.msg and Inside(Hit.msg, mx, my) then S.field = 2; return end if Hit.send and Inside(Hit.send, mx, my) then Send(); return end if Hit.cancel and Inside(Hit.cancel, mx, my) then Close(); return end end function GWC_UpdateKey() if S.open and (S.moving or (Hit.win and Inside(Hit.win, MousePosX(), MousePosY()))) then pcall(DisableClickClient) end end Console(1, "[GameWebCompose] cargado - se abre con /saludo")