MMORPG

Juan Mellado, 9 Octubre, 2010 - 10:50

Allods OnlineEl último capítulo de esta serie dedicada a los addons de Allods coincide con la actualización a la versión 1.1.02.58.1 del juego. Así, con todos esos numericos. Más conocida por "Rise of Gorluxor".

Llegado este punto no me queda mucho por decir. He cubierto todos los apartados que me había propuesto, completando los 10 artículos que calculé que podía llevarme la tarea. Aunque en este último post es bueno echar la vista atrás y repasar lo aprendido, que no ha sido poco.

Lua es sin lugar a dudas una de las piezas claves de todo este invento de los addons. Ya lo conocía de antes, pero como otra más de esas tecnologías que uno va encontrando por el camino y a la que nunca acaba de dedicar todo el tiempo que debería. En particular me llamó mucho la atención que Allods levantara una máquina virtual de Lua independiente para cada addon. Gracias a que Lua es un proyecto de código abierto me animé a bajarme los fuentes y echarles un vistazo para comprobar lo liviano que resulta todo el sistema, gracias a un lenguaje que en el fondo es bastante simple pero que cumple con creces las necesidades que se le plantean.

Otro factor clave desde el punto de vista del código de los addons es la programación orientada a eventos. A día de hoy sigue pareciendo una de las formas más naturales de informar de la ocurrencia de determinados sucesos externos a un programa, evitando tener que consumir recursos haciendo polling sobre algún tipo de estructura global. La contrapartida por supuesto es el número de eventos, que puede llegar a ser realmente grande si se pretende abarcar el enorme número de sucesos que pueden llegar ocurrir en el juego.

Un API bien definido que permita acceder y manipular todos los componentes principales del juego es otro punto importante a tener en cuenta. Aunque al igual que ocurre con los eventos, el número de funciones públicas se dispara. Afortunadamente, gracias al tipo table de Lua, que no deja de ser una tabla hash en los que sus elementos se referencian por su nombre, no es necesario definir múltiples estructuras o clases de datos, ya que se puede añadir cualquier atributo sin tener que modificar el código ya existente. Todo un acierto.

El hecho de que el código fuente interno de Allods, al menos en la parte que está escrita en Lua, utilice las mismas funciones del API que se exponen de forma pública es bastante interesante. Eso permite reemplazar funcionalidades completas del juego por las nuestras propias de forma bastante elegante.

La interface de usuario en Allods está resuelta mediante ficheros XML donde se describen las ventanas, los controles que contienen, y su aspecto en general. Esto es bastante común en muchas arquitecturas, y aunque tienden a generar muchos ficheros a veces dificiles de mantener manualmente, permite separar claramente la capa de presentación de la lógica de negocio. Aunque en este punto se echa en falta sin lugar a dudas algún tipo de editor visual que facilite la construcción de las interfaces.

Y hablando de echar en falta, donde más naufraga Allods en este asunto de los addons es en la falta de documentación. De acuerdo, el traductor de Google es nuestro amigo, pero aún así resulta complicado navegar por la documentación del API que se encuentra única y exclusivamente publicada en ruso. Y por no mencionar la diferencia entre las distintas versiones del juego, la rusa y las demás, con cambios en el API que hacen que los addons no funcionen correctamente en unas versiones y otras.

En cualquier caso, siempre hay que tener presente que Allods es un MMORPG gratuíto, aunque con un planteamiento del juego que invita en todo momento al jugador a pasarse por la tienda de items a realizar un micropago para conseguir algún tipo de mejora, o evitar las odiosas maldiciones que hacen que los objetos inviertan el valor de sus estadísticas. Cualquier característica que los desarrolladores incorporen al juego, como la posibilidad de escribir addons, se tendría que valorar en su justa medida teniendo en cuenta su gratuidad.

Happy coding!

Juan Mellado, 8 Octubre, 2010 - 16:40

Para ir acabando con esta serie dedicada a los addon de Allods voy a poner un par de bloques de código que he encontrado útiles. Cosas sencillas que suelen ser necesarias hacerlas de vez en cuando.

Ocultar la Interface
Allods tiene la típica opción para grabar a disco un screenshot ("pantallazo"), a modo de instantánea del juego. Y es bastante habitual que antes de hacerlo los jugadores prefieran quitar la interface gráfica del juego, para que se vea la mayor parte del mundo virtual posible, en vez de los marcos, ventanas o barras de acciones. Por ello, si implementamos un addon que muestre algún tipo de ventana o control gráfico, tenemos que hacer que se oculte automáticamente como hace el resto de addons del juego.

Cuando el usuario solicita que se oculte o restaure la interface del juego (combinación ALT+Z por defecto), Allods lanza el evento "SCRIPT_TOGGLE_UI" con un parámetro de tipo boolean que indica si la interface se está ocultando (false) o restaurando (true). En consecuencia, resulta muy sencillo ocultar o mostrar las ventanas de nuestros addons, consiguiendo un acabado más profesional de los mismos.

Como siempre, lo primero es registrar un manejador para el evento:

common.RegisterEventHandler(OnToggleUI, "SCRIPT_TOGGLE_UI")

Y en la función del manejador ocultar o mostrar nuestra interface:

function OnToggleUI(params)
  formulario:Show(params.visible)
end

Evidentemente, en el código del ejemplo se supone que en el addon hay declarada una variable global "formulario" que contiene una referencia a la ventana principal del addon. Si hubiera varias ventanas independientes habría que realizar la misma operación con todas ellas de forma individual.

Guardar Información
Una necesidad bastante habitual de algunos addons es guardar información, como por ejemplo los parámetros de configuración seleccionados por el jugador, para que se recuerden entre partida y partida. Algunos juegos de este tipo guardan las opciones del jugador en el servidor, e incluso permiten que los addons guarden las suyas propias, de forma que da igual la máquina desde la que un jugador ejecuta el juego, siempre lo ve con su configuración personalizada. Pero en Allods la configuración se guarda en local, en la máquina donde se juega.

Lógicamente, por temas de seguridad, Allods no permite que el código de un addon acceda libremente al disco duro de la máquina donde se ejecuta el juego. La máquina virtual de Lua que se utiliza está "capada" en ese sentido. Pero si deja la posibilidad de que se escriba y lea del fichero de configuración de opciones globales del juego. Dicho fichero tiene por nombre "user.cfg", y se encuentra en la carpeta "Personal" del directorio donde se encuentra instalado el juego. Es un fichero de texto ordinario, por lo que se puede abrir y examinar con cualquier editor.

Para escribir y leer del fichero hay que utilizar las funciones "SetGlobalConfigSection(section, table)" y "GetGlobalConfigSection(section)" respectivamente. Siendo "section" el nombre de la etiqueta bajo la cual queremos grabar los valores contenidos en "table", que ha de ser una variable de tipo table de Lua.

El siguiente código de ejemplo crea una tabla con un valor y la graba:

local tabla = {}
tabla["variable1"] = "prueba"
common.SetGlobalConfigSection("TABLA", tabla)

El resultado de la ejecución del código anterior es la actualización del fichero "user.cfg", al que se le añaden las siguientes líneas dentro de la sección "global":

...
table_begin global

table_begin ScriptLocal_TABLA

  table_begin data
   variable1 = l"prueba"
  table_end data

  remote_version = -1
table_end ScriptLocal_TABLA
...

El proceso contrario, para leer del fichero, es bien sencillo:

tabla = common.GetGlobalConfigSection("TABLA") or {}

El viejo truco de añadir "or {}" al final sirve para que si en el fichero no se encuentra la tabla pedida entonces se devuelva una tabla vacía, en vez de un valor nulo.

Aunque útil, esta forma de proceder tiene el incoveniente de que el fichero puede llegar a tener un tamaño bastante grande si todos los addons se dedican a escribir en él de forma indiscriminada. Y de igual forma, con el paso del tiempo, puede llegar a contener mucha información inútil, ya que no se depura cuando se desinstalan los addons.

Juan Mellado, 1 Octubre, 2010 - 16:15

En este artículo se analiza un addon de Allods llamado "AutoSellGreyAddon". Su función es la de vender todos los objetos grises que lleva en la bolsa nuestro personaje, de forma automática, en el momento que se abra una ventana de diálogo con un vendedor. El autor utiliza el alias de Valltron, así que vayan para él todos los correspondientes créditos.

El addon se compone de sólo dos ficheros: el obligatorio "AddonDesc.(UIAddon).xdb", y un script llamado "ScriptAutoSell.lua". No hay más. Ni formularios, ni imágenes, ni nada más. Aunque la verdad es que no lo necesita.

El código del script empieza con la habitual llamada a la función "Init", que en ese caso se encarga de registrar dos eventos. El evento "EVENT_TALK_STARTED", que se lanza cuando se inicia una conversación con cualquier NPC del juego, no necesariamente vendedor. Y el evento "EVENT_TALK_STOPPED", que se lanza cuando se termina una conversación.

function Init()
  common.RegisterEventHandler(OnTalkStarted, "EVENT_TALK_STARTED")
  common.RegisterEventHandler(OnTalkStopped, "EVENT_TALK_STOPPED")
end

Al iniciarse una conversación con un NPC el juego invoca a la función "OnTalkStarted", donde se registra el evento "EVENT_VENDOR_LIST_UPDATED", que es el que Allods lanza cuando un vendedor solicita a nuestro personaje la lista de objetos que tiene en la bolsa disponibles para la venta. Y por su parte, al terminar una conversación, se invoca a la función "OnTalkStopped" que elimina el registro de dicho evento "EVENT_VENDOR_LIST_UPDATED".

function OnTalkStarted()
  common.RegisterEventHandler(OnVendorListUpdated, "EVENT_VENDOR_LIST_UPDATED")
end
function OnTalkStopped()
  common.UnRegisterEventHandler(OnVendorListUpdated, "EVENT_VENDOR_LIST_UPDATED")
end

Finalmente, la función "OnVendorListUpdated" es la que realmente hace todo el trabajo en una serie de pasos muy claros:
- Obtiene el número de objetos que tiene nuestro avatar en la bolsa
- Recorre cada objeto que hay en la bolsa
- Y si el objeto tiene una calidad inferior a 1 (gris) lo vende
- Una vez acabada la venta elimina el registro del evento

function OnVendorListUpdated()
  local currentBagSize = avatar.InventoryGetBaseBagSlotCount()
  for slotIndex = 0, currentBagSize - 1 do
    local itemId = avatar.GetInventoryItemId(slotIndex)
    if itemId then
      local itemInfo = avatar.GetItemInfo(itemId)
      if itemInfo.quality <= 1 then
        avatar.Sell(slotIndex, itemInfo.stackCount)
      end
    end
  end
  common.UnRegisterEventHandler(OnVendorListUpdated, "EVENT_VENDOR_LIST_UPDATED")
end

La función "avatar.InventoryGetBaseBagSlotCount" es la que obtiene el número de objetos de la bolsa nuestro personaje. La función "avatar.GetInventoryItemId(slotIndex)" es la que obtiene el ID de un objeto concreto de la bolsa dada su posición dentro de la misma, que recordemos que puede ser de una casilla que está vacía, por lo que es necesario comprobar que ha devuelto un ID de un objeto válido. La función "avatar.GetItemInfo(itemId)" es la que obtiene el detalle de los atributos del objeto dado, siendo el atributo "quality" el que indica el nivel del objeto, aunque este es sólo uno de las varias decenas de atributos que devuelve la función y merecen la pena echar un vistazo en la documentación. Finalmente, la función "avatar.Sell(slotIndex, itemInfo.stackCount)" es la que vende el objeto de la bolsa dada su posición, y en su cantidad total además.

Sin lugar a dudas este addon es muy claro ejemplo de sencillez y utilidad.

Juan Mellado, 24 Septiembre, 2010 - 16:23

Uno de los objetivos principales de los addons de Allods es sustituir la interface gráfica por defecto del juego por otra más personalizada que se adapte mejor a las necesidades de cada cada jugador, o que añada algún tipo de información u opción que facilite el desempeño de su rol. Allods trata todos los componentes de su interface de usuario como addons, incluidos los elementos propios internos que son parte del núcleo del juego, como el chat o el mapa por ejemplo. Y este tratamiento homogéneo es el que permite "descargar" un addon original del juego y sustituirlo por uno propio.

Para obtener todos los addons cargados en el juego se debe utilizar la función "GetStateManagedAddons". Esta función devuelve una tabla Lua en la que cada elemento representa un addon. Por ejemplo, con el siguiente código se escribe en el log un listado con todos los addons cargados en el juego:

...
local addons = common.GetStateManagedAddons()
for i = 0, GetTableSize(addons) - 1 do
  LogInfo(addons[i].name)
end
...

En mi versión europea actual del juego, la 1.1.00.62, aparecen listados los siguientes 79 addons:

Alchemy                                  ContextShipDevice
ArmorCraft                               ContextShipDeviceNavigator
BuffInfo                                 ContextShipDeviceOvertip
BuffInfoParty                            ContextShipHangar
BuffInfoPet                              ContextShipPlate
BuffInfoTarget                           ContextSplitstack
Castbar                                  ContextTalents
Chat                                     ContextTooltip
ContextActionbar                         ContextTooltipCompare
ContextActions                           ContextTutorial
ContextAEMarker                          ContextUniMessageBox
ContextAnnounceCustom                    CraftBag
ContextAstralHubMap                      Death
ContextAuction                           EnemyShipDamage
ContextCartographer2                     EscMenu
ContextChatLine                          Guilds
ContextCharacter                         InternalContextActions
ContextCompass                           IslandTimer
ContextDamageVisualization               LagMeter
ContextDepositeBox                       MageEnergyInstability2
ContextDragNDrop                         NecromancerBloodPool
ContextFXPlayer                          NecromancerPet2
ContextInspect                           Options
ContextItemMall                          PaladinShields
ContextLootBag                           PetCommandPoints
ContextMail                              Plates
ContextMounts                            PsionicContact2
ContextMultibag                          RollGreedNeed
ContextNpcTalk                           Sounds
ContextNpcTeleport                       Spellbook
ContextOvertip                           StalkerCartridgeBelt
ContextPartyPlate                        SubtitleShipInfo
ContextPinMenu2                          TabSelector
ContextPlayerTrade                       TalentInformer
ContextPOIMarker                         TargetSelection
ContextPopup                             VendorTrade
ContextQuestLog                          Warnings
ContextQuestTracker                      WarriorCombatAdvantage
ContextRaidPlate                         ZoneAnnounce
ContextRuneCombiner

Los nombres son bastante significativos, y como se observa, aparecen prácticamente todos los componentes internos del juego con algún tipo de interface gráfica.

Cuando se carga un addon propio este también aparece en la lista precedido por "UserAddon". Así, si se carga el addon "SampleInit", que viene de ejemplo con el juego, aparece en el listado de la siguiente guisa:

UserAddon/SampleInit

Conociendo los nombres de los addons, es posible cargarlos o descargarlos utilizando las funciones "StateLoadManagedAddon" y "StateUnloadManagedAddon" respectivamente. Aunque naturalmente no todos los addons pueden ser descargados, ya que algunos son demasiado básicos, y su ausencia dejaría el juego practicamente inutilizable.

Para descargar o cargar un addon, como el de la brújula por ejemplo ("ContextCompass"), basta una simple línea de código:

...
-- Descargar
common.StateUnloadManagedAddon("ContextCompass")
...
-- Cargar
common.StateLoadManagedAddon("ContextCompass")
...

La idea de todo esto es que si queremos hacer un addon propio que sustituya la brújula original, lo primero que tendremos que hacer en el código Lua de nuestro addon será descargar el addon original para que no se solape con el nuestro. Sencillo, ¿no?.

Dentro de los addons de ejemplo que vienen con el juego hay uno llamado "SampleZoneAnnounce" que sustituye al addon "ZoneAnnounce" original, que es el encargado de mostrar por pantalla los nombres de las zonas por las que van pasando los personajes.

Si tenemos curiosidad por saber donde está el código Lua de los addons internos del juego, estos se encuentran en los ficheros "LuaCompiledSystem.pak" y "LuaCompiledIngame.pak" del directorio "data/Packs" donde se encuentra instalado el juego. Desgraciadamente están compilados y distribuidos en formato binario, por lo que no se pueden examinar. En versiones más antiguas del juego se distribuían en formato de texto y si se podían examinar.

Juan Mellado, 18 Septiembre, 2010 - 10:31

En el zip que contiene la documentación oficial, que se distribuye con el propio juego, hay un directorio llamado "ResourceSystem". Dentro de dicho directorio hay un ejemplo de cada tipo de fichero .xdb que puede usarse para crear addons para Allods, incluyendo todos los tipos de controles que pueden utilizarse dentro de los formularios con los que se implementan las interfaces gráficas.

Desgraciadamente no hay mucha más documentación acerca de los controles y parámetros de configuración que admiten los formularios. Los ficheros que empiezan por "SampleDefault" contienen un ejemplo completo con todos los parámetros válidos para cado tipo de .xdb. Los ficheros que empiezan por "SampleDefaultExt" contienen además un ejemplo completo con todos los parámetros válidos para las opciones que admiten listas de valores. Son bastantes, y se echa de menos un IDE para construir formularios de forma gráfica y posteriormente exportarlos a .xdb. ¿Alguien se anima?

En cualquier caso, en base a esos ejemplos, y aparte de los ya vistos para los textos y las texturas, los controles disponibles son los siguientes:

- WidgetForm: Formulario. Este es el tipo que tienen las ventanas principales. Todos los addons tienen un fichero de este tipo para su formulario principal, independientemente de que luego implementen o no una interface gráfica.

- WidgetPanel: Panel. Los formularios normalmente se componen de uno o más de tipo de controles, los paneles sirven para agruparlos e ir formando una jerarquía.

- WidgetButton: Botón. Un clásico, poco más que añadir.

- WidgetTextView: Texto. Otro que no merece más comentarios.

- WidgetEditLine: Línea de edición. Este control presenta una línea de texto en la que se puede introducir valores manualmente por teclado. Permite configurar algunos aspectos básicos como el aspecto del cursor, la velocidad de parpadeo, o indicar si se va a utilizar para introducir passwords. En los ejemplos que vienen con el juego no se utiliza, y de hecho, ninguno de los tipos que siguen a continuación, por lo que prácticamente no existe ningún addon que implemente ninguno de estos tipos. Buscando por Internet apenas he encontrado un addon que utiliza este tipo, pero del resto que siguen ninguno.

- WidgetTextContainer: Contenedor de texto. Parece que su objetivo obviamente es contener texto, pero resulta difícil precisar su funcionamiento. Debería ser el típico control en el que se puede ir añadiendo o quitando líneas de texto libre con formato, como en la salida de una ventana de chat por ejemplo.

- WidgetScrollableContainer: Contenedor scrollable. Aparenta el típico control que se usa para contener a otros y que muestra una barra de scroll, horizontal o vertical, para mostrar los que quedan fuera. No queda claro si es necesario definir una barra de scroll propia o usa automáticamente una por defecto.

- WidgetSimpleTable: Tabla. Este debería ser el tipo de control que permita contruir tablas con varias filas y columnas para listar registros. Sólo tiene un atributo llamado "tableStep" que parece hacer referencia al número de columnas deseadas. Aunque también podría ser un control para definir un layout en forma de tabla.

- WidgetDiscreteScrollBar: Barra de scroll de valores discretos. Debe ser la típica barra de scroll que permite la elección de unos determinados valores concretos prefijados de antemano.

- WidgetGlideScrollBar: Barra de scroll. He de suponer que de valores continuos, sin la restricción de valores prefijados de la anterior. Aunque de hecho tiene los mismos atributos que la anterior. En el API curiosamente no se encuentran funciones para este tipo de control, pero si para el anterior.

- WidgetDiscreteSlider: Deslizador de valores discretos. Debería ser igual que las barras de scroll pero sin los botones de incremento y decremento, sólo el botón central.

- WidgetGlideSlider: Deslizador. Igual que el anterior, pero para valores continuos. De igual forma que con las barras de scroll, en el API sólo hay funciones para el tipo de valores discretos.

- WidgetConsole: Consola. Debería ser la típica ventana de introducción de comandos. Entre sus atributos hace referencia a un "EditLine", por lo que no queda claro que valor añadido tiene frente a una línea de introducción de textos ordinario, a menos claro que sea capaz de procesar los comandos introducidos.

- WidgetConsoleOutput: Consola de salida. Aparenta ser la salida con los resultados de la ejecución de comandos introducidos por consola. Al igual que en el anterior tipo, entre sus atributos se hace referencia a un "textContainer", por lo que arroja algunas dudas acerca de su diferencia con una salida de texto ordinaria.

- WidgetControl3D: Control 3D. Este tiene una pinta muy interesante, una pena que no exista documentación. Aparenta ser un control que permite visualizar una entidad del juego en tres dimensiones, como la ventana de información del personaje que muestra a nuestro avatar en miniatura e incluso nos permite rotarlo.

El único addon que viene de ejemplo con el juego y que define un formulario algo elaborado es el llamado "SampleReactionHandler", aunque en la práctica apenas tiene un panel con una textura de fondo, dos textos y un botón. No obstante, aparte del propio formulario en si mismo, este addon tiene cosas interesantes.

La primera cosa interesante es ver como se construye el botón, que hace uso del prototipo que se encuentra en el directorio "data/Mods/SampleCommon/Button". Dentro de ese directorio hay ni más ni menos que 23 ficheros (aunque los cuatro .tga originales no son realmente necesarios). Todos esos ficheros definen el aspecto de un botón en cada uno de los estados en los que puede estar: normal, seleccionado, presionado o deshabilitado. Y dan una idea de la enorme cantidad de trabajo que hace falta para definir completamente cualquier control. Realmente se echa en falta un IDE que simplifique el trabajo, o al menos una librería completa de este tipo de controles prototipo para usarlos como plantillas.

La segunda cosa interesante de ese addon de ejemplo es ver el script Lua. Los controles registran reacciones en vez de eventos. ¿Y qué quiere decir esto en la práctica? Pues que en vez de subscribir el addon a eventos del mundo virtual utilizando la función "RegisterEventHandler", hay que utilizar la función "RegisterReactionHandler" para subscribirlo a reacciones de la interface gráfica. Poca cosa en realidad, simplemente que los desarrolladores decidieron diferenciar entre ambos tipos de sucesos. No existe un evento "botón presionado", sino que en cada "WidgetButton" se define el nombre de una "Reaction" y es a ese nombre al que se subscribe el addon. Por lo demás es igual, cuando se produce el suceso se llama a la función indicada en el script con los parámetros adecuados en función del tipo de suceso.

Por último, comentar que toda la interface gráfica propia del juego utiliza estos mismos controles que podemos utilizar nosotros para crear nuestros propios addons. Desgraciadamente todos los ficheros .xdb de los formularios (personaje, correo, comercio, subasta, ...) no están includos dentro del juego en formato de texto, sino en formato binario, dentro del fichero pack.bin en "data/Packs/Bin.pack", y no se pueden examinar.