Guía de Lua
Con Lua cada servidor puede agregar sus propios sistemas sin tocar los ejecutables: premios, eventos, comandos, NPC, reglas de PvP, ventanas propias en el cliente y mucho más. IGK usa Lua 5.4 en el GameServer y en el cliente (PC, Android y navegador).
Qué se puede hacer
- Reaccionar a lo que pasa en el juego: entrar al juego, matar un monstruo o a un jugador, subir de nivel, mover o usar ítems, abrir el baúl, cambiar de mapa, revivir y más (27 eventos).
- Cambiar o bloquear acciones: prohibir el PvP en un mapa, que matar no deje PK, cambiar el daño de un golpe, impedir recoger o tirar un ítem, dar otros puntos por nivel, hacer el drop de un monstruo.
- Leer y cambiar el personaje: nivel, puntos, estadísticas, vida, maná, SD, PK, resets, VIP, monedas, buffs, party y guild (más de 300 funciones).
- Ítems: dar ítems con todas sus opciones, tirarlos al suelo, contarlos y quitarlos, mandarlos al Gremory Case.
- Monstruos: crearlos, borrarlos, invocaciones propias.
- Base de datos segura: consultas que no frenan el juego y no se pueden inyectar, y datos guardados sin escribir SQL.
- Paquetes propios entre el servidor y el cliente, y ventanas propias en el cliente (en las tres plataformas).
- Temporizadores y horarios: cada N segundos, a una hora, un día de la semana.
Dónde están y cómo se activan
| Qué | Dónde |
|---|---|
| Scripts del servidor | MuServer\Data\Script\ (lo usan el GameServer y el GameServerCS) |
| Archivo principal | Data\Script\ScriptMain.lua: carga el núcleo y tus scripts |
| Núcleo (no se edita) | System\ScriptCore.lua |
| Ayudas incluidas | Utils\Functions.lua, Utils\Cron.lua (temporizadores), Utils\Schedule.lua (horarios), Utils\Database.lua (base de datos) |
| Plantilla con todos los eventos | Scripts\TemplateScript.lua |
| Ejemplos listos | Scripts\Examples\ (vienen apagados) |
| Scripts del cliente | Data\IGK\Scripts\ en la carpeta del cliente (ver "Lua en el cliente") |
Activar un script: en ScriptMain.lua agrega una línea al final:
require('Scripts\\MiScript') -- un archivo: Data\Script\Scripts\MiScript.lua
requirefolder('Scripts\\MisEventos') -- todos los .lua de una carpeta, en orden alfabético
Recargar sin reiniciar: escribe /reload lua en el juego (solo los scripts) o /reload custom (los scripts y todos los sistemas custom). Al recargar, todo lo que el script tenía en memoria se pierde: lo que deba durar, guárdalo con DataSet o en la base.
Si falta ScriptMain.lua, no hay scripts y no pasa nada.
Seguridad y límites
- Encerrados: los scripts no pueden leer ni escribir archivos (salvo los
.inideData\ScriptconConfigSaveString), ni ejecutar programas, ni cargar librerías, ni código compilado.requiresolo carga archivos deData\Script. - Tiempo: si un evento tarda más de 3 segundos (10 al cargar), el script se corta con un error y el servidor sigue. Un
while true do endya no cuelga el GameServer. - Memoria: todos los scripts juntos pueden usar hasta 256 MB.
- Errores: un error se escribe en rojo en la consola, en el log del GameServer (
[Lua] ...) y enLOGS\LUA_LOG\<fecha>.txt, y el servidor sigue funcionando. En el panel IGK Host, la pestaña Lua de la consola muestra todos los errores con el archivo y la línea (un clic abre el archivo en esa línea) y tiene el botón Revisar todos los scripts. Al guardar un.luadesde el panel se revisa su sintaxis: si tiene un error, se marca la línea en el editor. Si falla un evento que puede bloquear algo (por ejemplo, "¿puede atacar?"), el juego hace lo normal: un script con errores nunca bloquea al juego. - Rendimiento: los eventos frecuentes (daño, ítems, movimiento) solo se llaman si algún script se enganchó a ellos. Engancha solo los que uses.
Cómo se escribe un script
Cada script se engancha a los eventos que le interesan con BridgeFunctionAttach('Evento', 'NombreDeTuFuncion'). Usa nombres de función únicos (por ejemplo, con el nombre del script delante), porque todos los scripts comparten el mismo espacio.
-- Data\Script\Scripts\Bienvenida.lua
BridgeFunctionAttach('OnCharacterEntry', 'Bienvenida_Entrar')
function Bienvenida_Entrar(aIndex)
NoticeSend(aIndex, 1, "Hola " .. GetObjectName(aIndex) .. ", bienvenido!")
end
aIndex es el número del jugador (o del monstruo) en el servidor. Casi todas las funciones lo reciben primero.
Respuestas: algunos eventos esperan una respuesta:
- Veto (OnCheckUserTarget, OnCheckUserKiller, OnUserItemPick, OnUserItemDrop, OnUserItemMove): devolver
0bloquea la acción;1o nada la deja pasar. - Tomar (OnCommandManager, OnNpcTalk, OnMonsterDieGiveItem): devolver
1significa "me encargué yo" y el juego no hace lo suyo;0o nada, sigue lo normal. - Valor (OnUserDamage, OnMonsterDamage, OnUserItemUse, OnUserLevelUp, OnUserMasterLevelUp): ver cada evento.
Eventos del servidor
| Evento | Argumentos | Cuándo | Respuesta |
|---|---|---|---|
| OnReadScript | — | Al cargar los scripts (inicio y recarga). Al iniciar, todavía no están cargados los ítems ni los monstruos: para crear monstruos usa un Timer.TimeOut | — |
| OnShutScript | — | Antes de recargar y al cerrar el GameServer | — |
| OnTimerThread | — | Cada 1 segundo | — |
| OnCommandManager | aIndex, code, arg | Un jugador usó un comando de Command.txt (code = su número + 1; arg = lo escrito después) | 1 = el script lo manejó |
| OnCommandDone | aIndex, code | Después de un comando que funcionó | — |
| OnCharacterEntry | aIndex | El personaje entró al juego (también al pasar de un GameServer a otro) | — |
| OnCharacterClose | aIndex | El personaje sale | — |
| OnNpcTalk | aIndex (NPC), bIndex (jugador) | Clic en un NPC | 1 = el script respondió |
| OnMonsterDie | aIndex (monstruo), bIndex (jugador) | Murió un monstruo (si lo mató una invocación, bIndex es el dueño) | — |
| OnUserDie | aIndex (jugador), bIndex (quien lo mató) | Murió un jugador | — |
| OnUserMove | aIndex, map | El jugador llegó a otro mapa | — |
| OnUserRespawn | aIndex, KillerType | Antes de revivir (0 normal, 1 Chaos Castle, 2 guerra de guild, 3 duelo). Se puede cambiar el lugar con SetObjectMap/MapX/MapY | — |
| OnCheckUserTarget | aIndex, bIndex | Un jugador quiere atacar a otro | 0 = no puede |
| OnCheckUserKiller | aIndex (asesino), bIndex (víctima) | Un jugador mató a otro | 0 = no queda PK |
| OnUserItemPick | aIndex, slot, map, x, y, ItemIndex, Level, Option1, Option2, Option3, Exc, Set | Recoger un ítem del suelo | 0 = no puede |
| OnUserItemDrop | aIndex, slot, map, x, y, ItemIndex, Level, Option1, Option2, Option3, Exc, Set | Tirar un ítem al suelo | 0 = no puede |
| OnUserItemMove | aIndex, SourceFlag, SourceSlot, TargetFlag, TargetSlot, ItemIndex, Level, Option1, Option2, Option3, Exc | Mover un ítem (flags: 0 inventario, 1 comercio, 2 baúl, 3 a 20 máquinas de mezcla) | 0 = no puede |
| OnUserItemUse | aIndex, SourceSlot, TargetSlot | Clic derecho o joya sobre un ítem | 0 = lo hace el juego; 1 actualizar el ítem; 2 borrarlo y actualizar el destino; 3 borrarlo; 4 bajar 1 de durabilidad; 5 nada; 6 bajar 1 sin que se acabe nunca |
| OnUserDamage | aIndex, bIndex, damage, skill, combo, effect | Un jugador (o su invocación) golpea a otro jugador, con el daño final | -1 falla, 0 igual, más = daño nuevo |
| OnMonsterDamage | igual | Un jugador (o su invocación) golpea a un monstruo | igual |
| OnUserLevelUp | aIndex, level | Cada nivel que sube | los puntos de ese nivel, o -1 para los del servidor |
| OnUserMasterLevelUp | aIndex, level | Cada nivel master | los puntos master, o -1 |
| OnOpenWarehouse | aIndex, number | Se abrió el baúl | — |
| OnCloseWarehouse | aIndex | Se cerró el baúl | — |
| OnMonsterDieGiveItem | aIndex (jugador), bIndex (monstruo), map, x, y | Antes del drop de un monstruo | 1 = el script hizo el drop |
| OnSQLAsyncResult | name, identification, aIndex | Llegó la respuesta de un CreateAsyncQuery | — |
| GameServerProtocol | aIndex, head, packetName | Llegó un paquete Lua del cliente | — |
effect en los golpes: 1 ignora defensa, 2 excelente, 3 crítico, 4 reflejo; se suman 64 doble, 256 triple y 128 combo.
Funciones del servidor
En las tablas, aIndex es el jugador o el monstruo. Las funciones que cambian algo de un jugador solo actúan si está en el juego. Cuando un argumento es opcional, va el valor por defecto entre paréntesis.
La lista completa de nombres (servidor y cliente), sacada del código, está en 17_LUA_LISTA.md.
Servidor y ayudas
| Función | Qué hace |
|---|---|
GetMaxIndex(), GetMinUserIndex(), GetMaxUserIndex(), GetMinMonsterIndex(), GetMaxMonsterIndex() | Los rangos de números de objetos (para recorrer a todos los jugadores: de GetMinUserIndex() a GetMaxUserIndex() - 1) |
GetGameServerCode(), GetGameServerVersion(), GetGameServerUpdate() | El código del GameServer y su versión |
GetGameServerCurUser(), GetGameServerMaxUser() | Jugadores conectados y el máximo |
GET_ITEM(grupo, número) | El número de un ítem (por ejemplo GET_ITEM(14, 13) = Jewel of Bless) |
GetMonsterName(clase) | El nombre de un monstruo de Monster.txt |
RandomGetNumber(n) | Un número al azar de 0 a n-1 |
LogPrint(texto), LogColor(color, texto) | Escribe en la consola y el log (colores 0 a 8: 1 rojo, 2 verde, 3 azul) |
LogError(texto) | Un error propio del script: sale en rojo y en la pestaña Lua del panel IGK Host, junto con los errores de Lua |
DumpStack() | Escribe en el log por dónde va el script (para buscar errores) |
requirefolder('Scripts\\Carpeta') | Carga todos los .lua de una carpeta |
Personaje: GetObject… y SetObject…
Para cada campo de esta lista existe GetObject<Campo>(aIndex) y, si no tiene asterisco, SetObject<Campo>(aIndex, valor). Por ejemplo GetObjectStrength(aIndex) y SetObjectStrength(aIndex, 1000).
Importante: los SetObject… solo cambian el valor en el servidor. Para que el jugador lo vea, después llama a la función de envío que corresponda (LevelUpSend, UserInfoSend, UserCalcAttribute, LifeSend, MoneySend, PKLevelSend...).
| Grupo | Campos |
|---|---|
| Nivel y puntos | Level, LevelUpPoint, Experience, NextExperience\*, MasterLevel, MasterPoint, Reset, MasterReset |
| Estadísticas | Strength, Dexterity, Vitality, Energy, Leadership y sus extras (por ítems y buffs): ExtraStrength, ExtraDexterity, ExtraVitality, ExtraEnergy, ExtraLeadership |
| Vida, maná, AG, SD | Live\*, Life, AddLife, MaxLife, Mana, AddMana, MaxMana, BP, AddBP, MaxBP, Shield, AddShield, MaxShield, Defense |
| PK y muertes | PKCount, PKLevel, PKTimer, Kills, Deads |
| Cuenta y estado | AccountLevel\* (VIP), Authority\, Change (transformación), ChangeUp\ (evolución), ChatLimitTime (minutos sin chat), OfflineFlag\* |
| Party, guild, gens | PartyNumber\, GuildNumber\, GuildStatus, GensFamily\, GensRank\, GensSymbol\, GensContribution\ |
| Combate | ShieldDamageReduction, ShieldGaugeRate, DecreaseShieldGaugeRate, IgnoreDefenseRate, IgnoreShieldGaugeRate, DamagePvP, DefensePvP, ResistIgnoreDefenseRate, ResistIgnoreShieldGaugeRate, ResistStunRate, DamageReflect, CriticalDamageRate, ResistCriticalDamageRate, ExcellentDamageRate, ResistExcellentDamageRate, DoubleDamage, ResistDoubleDamage, TripleDamage, PhysiSpeed, MagicSpeed, AttackSuccessRate, AttackSuccessRatePvP, DefenseSuccessRate, DefenseSuccessRatePvP, MoneyAmountDropRate |
| Recuperación | HPRecovery, MPRecovery, SDRecovery, HPRecoveryRate, MPRecoveryRate, SDRecoveryRate, DefensiveFullHPRestoreRate, DefensiveFullMPRestoreRate, DefensiveFullSDRestoreRate, OffensiveFullHPRestoreRate, OffensiveFullMPRestoreRate, OffensiveFullSDRestoreRate |
Otras funciones del personaje:
| Función | Qué hace |
|---|---|
GetObjectName(aIndex), GetObjectAccount(aIndex) | Nombre del personaje y de la cuenta |
GetObjectClass(aIndex) | La clase del personaje, o el número del monstruo/NPC |
GetObjectType(aIndex) | 1 jugador, 2 monstruo, 3 NPC |
GetObjectConnected(aIndex) | 1 si es un jugador en el juego |
GetObjectMap, GetObjectMapX, GetObjectMapY | Dónde está |
SetObjectMap, SetObjectMapX, SetObjectMapY | Cambian el valor sin mover al jugador (útil en OnUserRespawn); para mover usa MoveUser |
GetObjectMoney(aIndex), SetObjectMoney(aIndex, zen) | El zen (Set también se lo muestra) |
GetObjectAccountLevel, GetObjectAccountExpireDate | VIP y su vencimiento |
GetObjectIpAddress, GetObjectPersonalCode | IP del jugador y su código personal |
GetObjectTotalLevel | Nivel + nivel master |
GetObjectTotalStrength, GetObjectTotalDexterity, GetObjectTotalVitality, GetObjectTotalEnergy, GetObjectTotalLeadership, GetObjectTotalLife, GetObjectTotalMana, GetObjectTotalBP, GetObjectTotalShield | Base + extra |
GetObjectDefaultStrength, GetObjectDefaultDexterity, GetObjectDefaultVitality, GetObjectDefaultEnergy, GetObjectDefaultLeadership | Los puntos iniciales de su clase |
GetObjectDamageReduction(aIndex, tipo), SetObjectDamageReduction(aIndex, tipo, valor) | Reducción de daño por tipo |
GetObjectResistance(aIndex, tipo), GetObjectAddResistance(aIndex, tipo), SetObjectResistance(aIndex, tipo, valor), SetObjectAddResistance(aIndex, tipo, valor) | Resistencias: 0 hielo, 1 veneno, 2 rayo, 3 fuego, 4 tierra, 5 viento, 6 agua |
GetObjectInterface(aIndex) | La ventana de interacción abierta (0 ninguna) |
GetObjectIndexByName(nombre) | El aIndex de un personaje conectado, o -1 |
GetObjectLanguage(aIndex) | Siempre "Eng" (los textos del servidor son uno solo) |
AddObjectReset(aIndex, n), AddObjectMasterReset(aIndex, n) | Suman resets |
GetExpRate(aIndex), SetExpRate(aIndex, tasa) | La experiencia propia del jugador |
ObjectAddExperience(aIndex, exp, MaxLevelUp (1000)) | Da experiencia como un monstruo (sube de nivel y lo muestra) |
UserSetAccountLevel(aIndex, nivel, segundos) | Da VIP 0 a 3 por esa cantidad de segundos |
Monedas
| Función | Qué hace |
|---|---|
ObjectGetCoin(aIndex) | Devuelve WCoinC, WCoinP, GoblinPoint (tres valores) |
GetObjectCoin(aIndex, n) | Una moneda (1, 2 o 3) |
ObjectAddCoin(aIndex, c1, c2, c3) | Suma monedas |
ObjectSubCoin(aIndex, c1, c2, c3) | Cobra: devuelve 1 si tenía saldo y lo descontó, 0 si no alcanzaba (no cobra nada) |
CoinAdd(aIndex, c1, c2, c3) | Suma (valores negativos restan, sin revisar el saldo) |
Las monedas se guardan en la base con el procedimiento WZ_SetCoin (necesita Update31).
Enviar al cliente
| Función | Qué hace |
|---|---|
LevelUpSend(aIndex) | Nivel, puntos y experiencia |
MasterLevelUpSend(aIndex) | Nivel y puntos master |
UserInfoSend(aIndex) | Estadísticas del personaje y zen |
UserCalcAttribute(aIndex) | Recalcula todo (ítems, estadísticas, buffs) y lo envía |
LifeSend(aIndex) | Vida, maná, AG y SD |
MoneySend(aIndex [, zen]) | Envía el zen (con el segundo valor, primero lo cambia) |
PKLevelSend(aIndex [, nivel]) | El estado PK (1 héroe, 3 normal, 6 asesino) |
SkinChangeSend(aIndex, skin) | Transforma al jugador (0 = normal) |
FireworksSend(aIndex [, x, y]) | Fuegos artificiales |
UserActionSend(aIndex, bIndex, acción) | Un gesto del personaje |
Sesión
| Función | Qué hace |
|---|---|
UserDisconnect(aIndex) (o UserGameLogout) | Desconecta al jugador |
KillObject(aIndex) | Mata a un jugador o monstruo |
Inventario e ítems
| Función | Qué hace |
|---|---|
InventoryGetWearSize(), InventoryGetMainSize(), InventoryGetFullSize() | Tamaños del inventario |
InventoryGetItemTable(aIndex, slot) | Una tabla con todo el ítem: Index, Level, Durability, Option1 (skill), Option2 (suerte), Option3 (opción), NewOption (excelentes), SetOption, JewelOfHarmonyOption, ItemOptionEx, SocketOption1-5, SocketOptionBonus, Serial, IsPeriodic, BuyMoney. nil si está vacío |
InventoryGetItemIndex(aIndex, slot) | El número del ítem, o -1 |
InventoryGetItemCount(aIndex, ítem [, nivel]) | Cuántos tiene en el inventario (no cuenta lo equipado; los apilables cuentan unidades) |
InventoryDelItemIndex(aIndex, slot) | Borra el ítem de ese lugar |
InventoryDelItemCount(aIndex, ítem, nivel, suerte, skill, cantidad) | Quita esa cantidad (-1 = cualquiera), nunca de lo equipado. Devuelve cuántos quitó |
InventoryGetFreeSlotCount(aIndex) | Lugares libres |
InventoryCheckSpaceByItem(aIndex, ítem), InventoryCheckSpaceBySize(aIndex, ancho, alto) | 1 si entra |
InventoryDecreaseItemDurability(aIndex, slot, cantidad) | Baja durabilidad (a los apilables, unidades) |
CheckMaxStackItem(ítem [, nivel]) | Cuántos se apilan (0 = no se apila) |
ItemGive(aIndex, ítem, nivel, dur, skill, suerte, opción, exc) | Da un ítem; 1 si entraba |
ItemGive(aIndex, especial) | Da un ítem de la EventItemBag con ese valor especial |
ItemGiveEx(aIndex, ítem, nivel, dur, op1, op2, op3, exc, set, harmony, optEx, s1, s2, s3, s4, s5, bonus, segundos) | Da un ítem con todas las opciones (sockets: 255 = sin socket) |
ItemDrop(aIndex, map, x, y, especial) | Tira al suelo un ítem de la EventItemBag (del dueño aIndex) |
ItemDropEx(aIndex, map, x, y, ítem, nivel, dur, op1, op2, op3, exc, set, harmony, optEx, s1..s5, bonus, segundos) | Tira al suelo ese ítem |
MoneyDrop(zen, map, x, y) | Tira zen al suelo |
MapGetItemTable(map, slot) | El ítem del suelo en ese lugar (0 a 299), con X, Y y Owner; nil si no hay |
AddItemToGremoryCase(aIndex, tipo, origen, ítem, nivel, dur, op1, op2, op3, exc, set, harmony, optEx, s1..s5, bonus, días) | Manda un ítem al Gremory Case (tipo 1 cuenta, 2 personaje; origen 252 = scripts) |
GremoryCaseAdd(aIndex, tipo, ítem, nivel, dur, skill, suerte, opción, exc, días, origen) | La forma corta |
InventoryItemCount, InventoryItemDelete | Las formas cortas anteriores (siguen funcionando) |
Monstruos
| Función | Qué hace |
|---|---|
MonsterCreate(clase, map, x, y [, dir]) | Crea un monstruo o NPC; devuelve su aIndex o -1. Queda hasta que muere o MonsterDelete |
MonsterDelete(aIndex) | Lo borra |
MonsterSummonCreate(aIndex, clase, vida%, defensa%, éxitoDefensa%, daño%, éxitoAtaque%) | Una invocación que sigue al jugador (100 = igual que el monstruo) |
MonsterSummonDelete(aIndex) | Quita la invocación del jugador |
GetMonsterTopHitDamageUser(aIndex) | El jugador que más daño le hizo (el que se lleva el drop) |
Mapas e invasiones
| Función | Qué hace |
|---|---|
MoveUser(aIndex, puerta) | Mueve por una puerta de Gate.txt |
MoveUser(aIndex, map, x, y) o MoveUserEx(aIndex, map, x, y) | Mueve a ese lugar (también a otro GameServer) |
MapCheckAttr(map, x, y, atributo) | 1 si el lugar tiene ese atributo: 1 zona segura, 4 pared, 8 agujero |
InvasionGetState(i) | 0 libre, 1 esperando, 2 en curso (i = número de InvasionManager.dat) |
InvasionGetRemainTime(i), InvasionSetRemainTime(i, valor) | Tiempo que falta |
InvasionGetMonsterTable(i) | Los aIndex de sus monstruos vivos |
Buffs
| Función | Qué hace |
|---|---|
EffectAdd(aIndex, tipo, efecto, cantidad, v1, v2, v3, v4) | Agrega un buff (tipo 0: cantidad en segundos) |
EffectDel(aIndex, efecto), EffectCheck(aIndex, efecto), EffectClear(aIndex) | Quita, revisa o quita todos |
GetEffectTable(aIndex, efecto) | cantidad, v1, v2, v3, v4 (nada si no lo tiene) |
Party y guild
| Función | Qué hace |
|---|---|
PartyGetMemberCount(party), PartyGetMemberIndex(party, lugar 0-9) | Miembros (el número de party sale de GetObjectPartyNumber) |
PartyCreate(aIndex), PartyDelete(party), PartyAddMember(party, aIndex), PartyDelMember(party, aIndex) | Manejan la party y la muestran |
GetObjectGuildName, GetObjectGuildUnionName, GetObjectGuildMark (64 letras hex) | Datos de su guild |
GetObjectGuildRelationship(aIndex, bIndex) | 0 nada, 1 unión, 2 rival |
ObjectGetUserGuildInfo(aIndex) | Tabla: GuildNumber, GuildName, GuildCount, GuildTotalScore, GuildPlayScore, UserNumber, UserConnected |
ObjectGetGuildMembers(aIndex) | Los aIndex de los miembros conectados |
SetObjectGuildNotice(aIndex, texto) | Cambia el aviso de la guild |
Mensajes y comandos
| Función | Qué hace |
|---|---|
NoticeSend(aIndex, tipo, texto) | Mensaje a un jugador (0 dorado al centro, 1 azul del sistema) |
NoticeSendToAll(tipo, texto) | A todos los de este GameServer |
NoticeGlobalSend(tipo, texto) | A todos los GameServer del grupo |
MessageSend(aIndex, tipo, color, texto), MessageSendToAll(tipo, color, texto) | Con color |
ChatTargetSend(aIndexNPC, aIndexJugador, texto) | El NPC le habla al jugador (globo sobre el NPC) |
PostSend(tipo, estilo, nombre, texto) | Un mensaje en el chat global, como /post |
MessageGet(número) | Un texto de Message.txt |
MessageGetByLang(número, idioma), MessageGetByUserLang(número, aIndex) | Lo mismo que MessageGet (el servidor tiene un solo idioma de textos); existen para los scripts de otros servidores |
SendMessageAllByNumber(número, ...) | (Utils\Functions.lua) Manda a todos el texto de Message.txt con los valores dados (%d, %s) |
get_script_path() | (Utils\Functions.lua) La carpeta del script que la llama |
CommandSend(aIndex, "/comando args") | Ejecuta un comando como si lo escribiera el jugador (con sus precios y permisos) |
CommandCheckGameMasterLevel(aIndex, nivel) | 1 si es GM de ese nivel |
CommandGetArgString(arg, n), CommandGetArgNumber(arg, n) | La palabra o el número n (desde 0) de lo escrito tras un comando |
Comandos propios: agrega una fila en Data\Command.txt (por ejemplo con el índice 100, que está libre) con su precio, requisitos y nivel de GM, y en tu script maneja ese code (índice + 1 = 101) en OnCommandManager, devolviendo 1.
Configuración en archivos .ini
| Función | Qué hace |
|---|---|
ConfigReadNumber(sección, clave, archivo [, defecto]) | Lee un número de un .ini dentro de Data\Script |
ConfigReadString(sección, clave, archivo [, defecto]) | Lee un texto |
ConfigSaveString(sección, clave, valor, archivo) | Guarda (solo archivos .ini dentro de Data\Script) |
Temporizadores y horarios (Utils\Cron.lua y Utils\Schedule.lua)
| Función | Qué hace |
|---|---|
Timer.Interval(segundos, función, ...) | Repite cada N segundos; devuelve un id |
Timer.TimeOut(segundos, función, ...) | Una vez, dentro de N segundos |
Timer.Repeater(segundos, veces, función, ...) | N veces |
Timer.Cancel(id) | Cancela |
Schedule.SetHourAndMinute(hora, minuto, función, ...) | Todos los días a esa hora |
Schedule.SetDayOfWeek(día, hora, minuto, función, ...) | Un día de la semana (0 domingo) |
Schedule.SetDayAndHourAndMinute(díaDelMes, hora, minuto, función, ...) | Un día del mes |
Un temporizador cuya función falla se cancela y se avisa en el log. Al recargar los scripts, los temporizadores se pierden (se vuelven a crear en OnReadScript).
Base de datos
Las consultas van por el DataServer, en segundo plano: el juego no se frena esperando a la base. La respuesta llega un momento después a la función que le pases.
DBQuery("SELECT TOP 10 Name, cLevel FROM Character WHERE AccountID = ?", { GetObjectAccount(aIndex) },
function(ok, resultado)
if not ok then LogColor(1, resultado) return end
for _, fila in ipairs(resultado.rows) do
NoticeSend(aIndex, 1, fila.Name .. " nivel " .. fila.cLevel)
end
end)
- Los
?se cambian por los valores de la lista, con las comillas puestas por el servidor: un nombre que escribió un jugador nunca puede meter SQL propio. No armes el SQL pegando textos. - El resultado:
resultado.rows[i].Columna(número o texto;nilsi es NULL),resultado.count,resultado.columns,resultado.affected(filas cambiadas),resultado.truncated(había más de 100 filas o no entraban en la respuesta). - Qué se permite: leer (
SELECT) cualquier tabla; escribir (INSERT,UPDATE,DELETE,MERGE) solo en tablas cuyo nombre empieza conLua_. Nunca: varias sentencias, comentarios,EXEC,DROP,ALTER,CREATE,SELECT ... INTOy similares. - Escribir en las tablas del juego: solo si el dueño lo permite en
DataServer\Data\DataServer.ini, sección[DataServerInfo],LuaDatabaseWrite = 1(y se reinicia el DataServer). Úsalo con cuidado. - Tus tablas: créalas con un script SQL propio (
Update32en adelante, ver02_INSTALACION_SERVIDOR.md) con nombresLua_....
Datos guardados sin SQL (necesita DataBase\SQL\Update32 - Lua scripts data.sql, tabla Lua_Data):
| Función | Qué hace |
|---|---|
DataSet(ámbito, dueño, clave, valor) | Guarda un valor (texto o número) |
DataGet(ámbito, dueño, clave, function(valor) ... end) | Lo lee (nil si no existe) |
DataGetAll(ámbito, dueño, function(tabla) ... end) | Todos los de ese dueño: tabla[clave] = valor |
DataDelete(ámbito, dueño, clave) | Lo borra |
ámbito es un nombre corto (por ejemplo "global", "account", "character"); dueño es la cuenta, el personaje, o "" para algo global.
Forma de otros servidores (sigue funcionando): CreateAsyncQuery(nombre, sql, aIndex, 1) y, en el evento OnSQLAsyncResult(nombre, id, aIndex), QueryAsyncGetValue(nombre, columna [, fila]), QueryAsyncGetRowCount(nombre), QueryAsyncGetTable(nombre) y QueryAsyncDelete(nombre). ConnectQueryAsync() siempre devuelve 1 (la conexión es la del DataServer). Valen las mismas reglas de seguridad, pero aquí el SQL va armado a mano: no pegues textos de los jugadores.
No existen SQLQuery, SQLFetch y las demás funciones SQL que esperan la respuesta en el momento: frenarían el GameServer. Si un script las usa, el log avisa una vez que se cambie a DBQuery. Para que esos scripts no se caigan, SQLConnect, SQLDisconnect, SQLFetch, SQLGetNumber, SQLGetString, SQLGetResult, SQLGetSingle y SQLClose existen pero no hacen nada (devuelven 1, 0, 100 o ""). ExecuteQuery(sql) sí manda la consulta, por DBQuery, sin esperar la respuesta.
Paquetes Lua entre el servidor y el cliente
Un script del servidor y uno del cliente pueden mandarse datos propios.
| Función | Qué hace |
|---|---|
CreatePacket(nombre, head) | Crea un paquete vacío (nombre: letras, números y _, hasta 32; head: un número tuyo) |
SetBytePacket, SetWordPacket, SetDwordPacket(nombre, valor) | Agregan un número de 1, 2 o 4 bytes |
SetCharPacket(nombre, texto) | Agrega un texto de 10 letras |
SetCharPacketLength(nombre, texto, largo), SetHexPacketLength(nombre, hex, largo) | Texto o bytes de ese largo |
GetBytePacket, GetWordPacket, GetDwordPacket(nombre, posición) | Leen en esa posición (desde 0) |
GetCharPacket(nombre, posición), GetCharPacketLength(nombre, posición, largo), GetHexPacketLength(nombre, posición, largo) | Leen textos o bytes |
GetPacketSize(nombre) | El tamaño |
SendPacket(nombre, aIndex) | Lo manda a ese jugador (en el cliente: SendPacket(nombre) lo manda al servidor) |
ClearPacket(nombre) | Lo borra |
Hasta 4000 bytes por paquete. El servidor acepta hasta 30 paquetes Lua por segundo de cada jugador (los demás se descartan). Lo que llega del cliente puede venir modificado: comprueba siempre los valores en el servidor antes de dar algo.
Lua en el cliente (PC, Android y navegador)
El cliente también corre scripts Lua: sirven para ventanas propias, botones, avisos en pantalla y para hablar con los scripts del servidor. Es el mismo código en PC, Android y el navegador.
Dónde están
| Qué | Dónde |
|---|---|
| Scripts del cliente | Data\IGK\Scripts\ en la carpeta del juego (también dentro de los datos de Android y del navegador) |
| Archivo principal | Data\IGK\Scripts\ScriptMain.lua (por defecto solo carga el núcleo: no se ve nada) |
| Núcleo (no se edita) | System\ScriptCore.lua |
| Ventanas fáciles | Utils\Window.lua |
| Ejemplo | Scripts\Example.lua (apagado; se abre con la tecla J) |
| Copia maestra para el dueño | MainInfo\Scripts\ |
Se activan igual que en el servidor: require('Scripts\\MiVentana') o requirefolder('Scripts\\MisVentanas') en ScriptMain.lua. Los scripts se cargan la primera vez que aparece la pantalla de login. Para recargarlos, un script llama a ReloadClientScripts() (por ejemplo desde una tecla). Para que los jugadores los tengan, hay que repartirlos con el cliente: en PC con el resto de los archivos, en el navegador volviendo a correr WebPacker, y en Android en los datos del celular (parche o data.zip).
Los scripts del cliente son archivos de texto que el jugador puede ver y cambiar: nunca confíes en lo que manda un script del cliente; el servidor debe comprobarlo todo.
Seguridad y límites del cliente
- Mismo encierro que en el servidor: sin archivos, sin programas, sin librerías, sin internet, sin tocar la memoria del juego.
- Cada evento puede tardar como máximo 50 milisegundos (2 segundos al cargar). Si un evento se pasa 3 veces, se apaga hasta la próxima recarga y el juego sigue.
- Memoria: hasta 64 MB.
- Los errores van al
MuError.logdel cliente como[Lua] ..., cada error una sola vez. - Los eventos que nadie usa no cuestan nada: el cliente solo llama a los enganchados.
Eventos del cliente
| Evento | Argumentos | Cuándo | Respuesta |
|---|---|---|---|
| OnReadScript | — | Al cargar los scripts | — |
| OnShutScript | — | Antes de recargar y al cerrar el juego | — |
| MainProcThread | — | Cada cuadro (lógica) | — |
| MainInterfaceProcThread | — | Cada cuadro, al dibujar la interfaz: solo aquí se puede dibujar | — |
| KeyboardEvent | tecla | Al apretar una tecla (no mientras se escribe en el chat). Códigos de tecla de Windows: 0x41 = A, 0x70 = F1... | 1 = el juego no ve esa tecla (F12 no se puede tomar) |
| ScrollMouseEvent | delta | Rueda del ratón (positivo = arriba) | 1 = el juego no lo ve |
| LeftClickEvent / RightClickEvent | — | Al apretar el botón | 1 = el juego no ve ese clic |
| ClientProtocol | head, nombre | Llegó un paquete Lua del servidor | — |
| OnEnterGame / OnLeaveGame | — | El personaje entra al mundo o sale (al cambiar de GameServer: sale y entra) | — |
Los eventos de cada cuadro corren en la pantalla de login (2), de personajes (4) y en el juego (5).
Coordenadas
Los scripts dibujan en las mismas unidades que las ventanas del juego: una pantalla virtual de al menos 640 × 480 que el juego escala a cualquier resolución y al celular. El tamaño real es GetWindowWidth() × GetWindowHeight() (por ejemplo 1092.8 × 614.4 en 1366 × 768). X va de izquierda a derecha y Y de arriba abajo. El ratón usa las mismas unidades. En el celular un toque llega como clic izquierdo y no hay teclado.
Funciones del cliente
| Grupo | Funciones |
|---|---|
| Base | LogPrint(texto) / print / Console([color,] texto) al MuError.log; GetMainScene() (1 carga, 2 login, 3 pantalla de carga, 4 personajes, 5 juego, 6 video); GetLanguage() ("Spn"...); GetPlatform() ("PC", "Android", "Web"); IsMobile(); GetMainResolution() (ancho, alto en píxeles); GetWindowWidth(), GetWindowHeight(); GetTickCount(); ReloadClientScripts(); ScriptError(texto) (un error propio del script: al MuError.log una sola vez) |
| Ratón y teclas | MousePosX(), MousePosY(), MouseLButton(), MouseRButton(), CheckMouseIn(x, y, w, h), CheckPressedKey(tecla), CheckReleasedKey(tecla), CheckRepeatKey(tecla), LockPlayerWalk() / UnlockPlayerWalk() (los clics no mueven al personaje mientras está bloqueado) |
| Imágenes | LoadBitmap(ruta, id) / LoadPngImage(ruta, id) (ruta dentro de Data\, id de 40000 a 40499); RenderImage(id, x, y [, w, h]); RenderImage2(id, x, y, w, h, u, v, uw, vh [, alfa]) (un recorte de la imagen, en píxeles de la imagen); GetImageWidth(id), GetImageHeight(id) |
| Colores y formas | SetColor(r, g, b [, a]) (0 a 255); glColor4f(r, g, b [, a]) (0 a 1); DrawBar(x, y, w, h [, r, g, b, a]); EnableAlphaBlend() (brillo) / DisableAlphaBlend() |
| Texto | RenderText(x, y, texto [, ancho [, alineación]]) (0 izquierda, 1 centro, 2 derecha); SetTextColor(r, g, b [, a]); SetTextBg(r, g, b [, a]); SetFontType(0 normal, 1 negrita, 2 grande); GetTextWidth(texto); GetFontHeight(). Los textos van en UTF-8 (tildes y ñ funcionan) |
| Ventanas del juego | CheckWindowOpen(id), OpenWindow(id), CloseWindow(id) con constantes: WINDOW_INVENTORY, WINDOW_CHARACTER, WINDOW_PARTY, WINDOW_GUILD, WINDOW_FRIEND, WINDOW_MOVE, WINDOW_QUEST, WINDOW_COMMAND, WINDOW_PET, WINDOW_MENU, WINDOW_OPTION, WINDOW_HELP, WINDOW_QUICK_COMMAND, WINDOW_MASTER_LEVEL, WINDOW_GENS_RANKING, WINDOW_MINI_MAP, WINDOW_HELPER, WINDOW_CUSTOM_MENU, WINDOW_COMMAND_LIST, WINDOW_EVENT_TIME, WINDOW_RANKING. Solo para revisar: WINDOW_TRADE, WINDOW_STORAGE, WINDOW_CHAOS_MIX, WINDOW_NPC_SHOP, WINDOW_MY_SHOP, WINDOW_OTHER_SHOP, WINDOW_CHAT_INPUT, WINDOW_MESSAGEBOX, WINDOW_NPC_DIALOGUE, WINDOW_CASH_SHOP |
| El personaje | GetPlayerIndex(), GetPlayerName(), GetPlayerLevel(), GetPlayerClass() (0 DW, 1 DK, 2 Elfa, 3 MG, 4 DL, 5 SU, 6 RF), GetPlayerMap() / GetCharacterMap(), GetPlayerPosX(), GetPlayerPosY(), GetPlayerLife(), GetPlayerMaxLife(), GetPlayerMana(), GetPlayerMaxMana(), GetPlayerMoney(), GetTargetIndex(), GetTargetName() (fuera del juego devuelven 0, "" o -1) |
| Ítems | GetItemName(ítem [, nivel]) / GetNameByIndex; RenderItem3D(ítem, nivel, exc, x, y, w, h) (el ítem en 3D dentro de una ventana, solo en el juego) |
| Chat | SendPlayerChat(texto) (como si el jugador lo escribiera; un /comando va al servidor); SendMessageClient(texto) (una línea solo para él) |
| Paquetes | Los mismos que el servidor (CreatePacket, Set…Packet, Get…Packet, GetPacketSize, ClearPacket) y SendPacket(nombre) para mandarlo al servidor: solo en el juego, hasta 30 por segundo y 2048 bytes por paquete (el servidor puede mandar hasta 4000) |
| Carga | require('Carpeta\\Nombre'), requirefolder('Scripts\\Carpeta') (solo dentro de Data\IGK\Scripts) |
Una ventana en pocas líneas
Utils\Window.lua arma ventanas que se pueden arrastrar, con título, botón de cerrar, botones y líneas de texto: Window.New{ Title, X, Y, Width, Height, Key, Lines, OnDraw } crea una, y luego win:AddButton{ Text, X, Y, Width, OnClick }, win:SetLine(n, texto [, r, g, b]), win:Show(), win:Hide(), win:Toggle() y win:IsVisible(). Mira Scripts\Example.lua: una ventana que se abre con la tecla J, muestra el nombre y el nivel del personaje, y con un botón manda el paquete "IGKPing" al servidor y muestra la respuesta "IGKPong" (el script del servidor es Scripts\Examples\PingPong.lua).
BridgeFunctionAttach('MainInterfaceProcThread', 'MiAviso_Dibujar')
function MiAviso_Dibujar()
if GetMainScene() ~= 5 then return end
DrawBar(10, 60, 160, 20, 0, 0, 0, 160)
SetTextColor(255, 210, 0, 255)
RenderText(10, 64, "Evento a las 20:00", 160, 1)
end
Ejemplos incluidos
En Data\Script\Scripts\Examples\, apagados. Para usar uno: require('Scripts\\Examples\\Nombre') en ScriptMain.lua.
| Ejemplo | Qué hace |
|---|---|
| WelcomeMessage | Bienvenida al entrar y aviso cuando entra o sale un GM |
| BossReward | Monedas a quien mata ciertos jefes, con aviso a todos |
| PvPRules | Sin PvP en Lorencia; en Arena matar no deja PK |
| DailyLogin | Premio una vez por día al entrar (guardado en la base) |
| NpcExchanger | Un NPC que cambia 10 Jewel of Bless por 1 Jewel of Creation |
| HappyHour | De 20:00 a 21:00 cada monstruo da 1 WCoinC y al final gana quien mató más |
| PingPong | Responde al paquete del ejemplo del cliente |
Scripts de otros servidores
IGK usa los mismos nombres de eventos y funciones que la API Lua más usada en MU, así que muchos scripts funcionan con pocos cambios. Diferencias a revisar:
SQLQuery/SQLFetch(síncronas) no existen: usarDBQueryoCreateAsyncQuery.- Escribir en tablas del juego necesita
LuaDatabaseWrite = 1. MoveUseracepta(aIndex, puerta)y también(aIndex, map, x, y);ItemGiveacepta(aIndex, especial)y(aIndex, ítem, nivel, ...).- No hay funciones para leer archivos, internet ni el ID del hardware: son bloqueos de seguridad a propósito.
Preguntas frecuentes
P: Agregué un script y no hace nada.
R: Revisa que esté en ScriptMain.lua con require('Scripts\\Nombre'), que el nombre del archivo coincida y que el evento esté bien escrito en BridgeFunctionAttach. Después /reload lua y mira la consola del GameServer: un error de Lua sale en rojo con el archivo y la línea.
P: ¿Cómo veo los errores de mis scripts?
R: En la consola del GameServer y en su log (GameServer\LOGS\LOG\), en rojo con [Lua], el archivo y la línea. También puedes escribir tus propios mensajes con LogPrint o LogColor.
P: Después de mi script nadie puede usar comandos (o los NPC no hablan).
R: En IGK eso ya no pasa por olvidar return 0, pero sí si tu función devuelve 1 siempre. Devuelve 1 solo cuando tu script se encargó de ese comando o NPC.
P: Mi script se cortó con "the script ran for more than 3000 ms".
R: Algo en tu script tarda demasiado (un bucle que no termina o muy grande). Para tareas largas usa temporizadores (Timer.Interval) o hazlas por partes.
P: ¿Se pierden los datos de mi script al reiniciar?
R: Lo que esté en variables de Lua sí. Guarda lo importante con DataSet (tabla Lua_Data) o en una tabla Lua_... tuya con DBQuery, o en un .ini con ConfigSaveString.
P: DataGet o DBQuery no responden.
R: Revisa que el DataServer esté conectado, que ejecutaste Update32 - Lua scripts data.sql y mira el log del DataServer: una consulta rechazada aparece como [LuaQuery] Refused con el motivo.
P: Mi consulta dice "scripts may only change tables named Lua_...".
R: Por seguridad, los scripts solo escriben en tablas Lua_.... Crea tu tabla con ese prefijo, o permite escribir en las del juego con LuaDatabaseWrite = 1 en el DataServer.ini (bajo tu responsabilidad).
P: ¿Cómo hago un comando nuevo, por ejemplo /premio?
R: Agrega una fila en Data\Command.txt con un índice libre (el 100), el texto "/premio", sus precios y su nivel de GM. En tu script, en OnCommandManager, si code == 101 haz lo que quieras y devuelve 1.
P: ¿Puedo quitar el PvP de un mapa sin tocar el juego?
R: Sí: con OnCheckUserTarget devolviendo 0 en ese mapa. Mira el ejemplo PvPRules.lua.
P: ¿Puedo cambiar lo que tira un monstruo?
R: Sí: en OnMonsterDieGiveItem haz tu drop con ItemDropEx o MoneyDrop y devuelve 1; el drop normal no sale.
P: ¿Un script puede leer archivos o conectarse a internet?
R: No. Los scripts están encerrados por seguridad: solo leen y escriben archivos .ini dentro de Data\Script.
P: ¿Los scripts funcionan en el GameServerCS (Castle Siege)?
R: Sí, el GameServerCS usa la misma carpeta Data\Script. Si quieres que un script haga algo solo en uno de los dos, compara GetGameServerCode().
P: Hice una ventana en el cliente y los jugadores no la ven.
R: Los scripts del cliente van con el cliente: copia Data\IGK\Scripts en el cliente que repartes (PC), vuelve a correr WebPacker para el navegador y manda un parche de datos para Android. Revisa también que el script esté activado en Data\IGK\Scripts\ScriptMain.lua.
P: ¿Dónde veo los errores de los scripts del cliente?
R: En el MuError.log de la carpeta Log del juego (en Android, en la de la carpeta de datos), con [Lua], el archivo y la línea. Va cifrado: arrástralo sobre Encoder.exe y lee el MuError.txt que sale al lado.
P: Mi ventana del cliente funciona en PC pero en el celular el personaje camina cuando la toco.
R: En el celular, el joystick y los botones táctiles no pasan por los clics del ratón. Pon tu ventana lejos del joystick y de los botones de skills, o usa IsMobile() para ubicarla en otro lugar en el celular.