Volviendo al tema de la escasa documentación sobre las librerías del juego, de la que disponemos. En este capítulo, vamos a explorar el proceso de investigación de una librería “.dll” y cómo empezar a buscarnos la vida para aprender a modificar el juego.
El objetivo de la modificación de ejemplo que se va a desarrollar en este tutorial es que cuando se inicie un mapa nuevo, estén desbloqueadas las calles básicas de doble sentido y las calles básicas de sentido único. Actualmente, en el juego hasta que no construyes, al menos una calle básica de doble sentido, no se desbloquea la construcción de la calle básica de sentido único.
Para esta tarea tenemos varias opciones, la más común es buscar en la documentación pertinente de las librerías del juego. Esto lo vamos a ver con más detalle en próximas secciones, así que vamos a considerar que no encontramos nada en la documentación que nos ayude en nuestra tarea.
Otra forma es, descompilar las propias librerías del juego y, con mucha paciencia, recorrer todas las clases y funciones que contenga hasta dar con algo interesante. En próximos capítulos intentaremos analizar cada una de las librerías del juego para ver sus funcionalidades.
Por fin, para el caso que nos ocupa vamos a buscar en foros y modificaciones hechas por otros a ver qué encontramos. Y resulta que ya hay una modificación hecha que realiza la tarea que queremos. Como el objetivo es aprender, vamos a usar dicha modificación, la vamos a descompilar y vamos a hacer nuestro ejemplo copiandonos, pero intentando entender qué está sucediendo.
2.1. Nuestro modelo a seguir.
La modificación que nos va a servir de guía para este capítulo es:
- Unlock Basic Roads: http://smods.ru/archives/1733
El enlace no es para activar la modificación desde steam, si no que es para descargar la librería “.dll” directamente y así nos ahorramos un par de pasos. Además en este caso, tenemos la suerte que en el archivo comprimido que nos descargamos también se encuentra un archivo con el código fuente sin compilar.
Dentro del archivo comprimido nos encontramos con el archivo “UnlockBasicRoads.dll” que es la modificación compilada. También nos encontramos con la carpeta “Source” que contiene los códigos fuente. En dicha carpeta están, el archivo “UnlockBasicRoads.cs” que es el código fuente en c# de la modificación y la carpeta “ChirpLogger”.
¿Una carpeta “ChirpLogger”? bueno pues resulta que esta modificación hace uso del código de otra modificación que permite usar a Chirper como una consola y así enviarle mensajes. Cuando se desbloquean las calles, la modificación envía un texto al pajarito azul y este nos lo muestra como un mensaje del juego.
Tampoco es una mala práctica, antes de empezar con el código, instalar la modificación en el juego, probarla en un mapa nuevo y observar su comportamiento. No te olvides de desinstalarla antes de probar nuestra modificación.
2.2. “Espiando” la librería.
A pesar de tener el código fuente, se va a intentar ilustrar el proceso de descompilar una librería y observar su contenido. Como ya se vio en el capítulo anterior, el programa que se va a usar es (ILSpy) http://ilspy.net/ . En la página del programa, te descargas los binarios y ejecutas el archivo “IL.exe” para abrir el programa (no necesita ser instalado). Una vez abierto el programa, en la barra izquierda, seleccionamos todos los elementos ya cargados y con la tecla Supr. los eliminamos para que no nos estorben mucho. Posteriormente arrastramos nuestro archivo “UnlockBasicRoads.dll” a la barra izquierda recién vaciada.
Tras lo cual, vemos cómo aparece el nombre de la librería y su versión. Expandimos su contenido y nos encontramos con 5 nuevos apartados:
- - References
- {} -
- {} ChirpLogger
- {} ChirpLogger.Internal
- {} UnlockBasicRoads
El apartado de “References” es una buena guía para saber qué librerías del juego tenemos que referenciar en nuestro proyecto. Expandimos su contenido y nos encontramos con “ICities” y con “mscorlib”. La segunda es una librería del sistema y no necesitaremos referenciarla manualmente, pero la primera sí es una librería del juego y sí tendremos que referenciarla en nuestro proyecto.
Los demás apartados, con las “{}” delante, hacen referencia a espacios de nombres dentro de la librería, vamos a expandir “{} UnlockBasicRoads” que de momento es el único que nos interesa. Nos encontramos que este espacio de nombres contiene dos clases “UnlockBasicRoads” y “UnlockBasicRoadsMilestones”. Seleccionamos la primera y vemos como en la sección derecha del programa aparece el código en c# de esa clase. Que es el siguiente:
- Código: Seleccionar todo
using ICities;
using System;
namespace UnlockBasicRoads {
public class UnlockBasicRoads : IUserMod {
public string Name {
get { return "Unlock Basic Roads"; }
}
public string Description {
get { return "Unlocks Basic Roads without the need to place single road first"; }
}
}
}
Como ya vimos en el capítulo anterior esta es la clase que se encarga de definir la librería como una modificación apta para Cities Skylines. Pasemos pues a observar la siguiente clase, la seleccionamos y nos encontramos con:
- Código: Seleccionar todo
using ChirpLogger;
using ICities;
using System;
namespace UnlockBasicRoads {
public class UnlockBasicRoadsMilestones : MilestonesExtensionBase {
public override void OnRefreshMilestones() {
base.get_milestonesManager().UnlockMilestone("Basic Road Created");
ChirpLog.Debug("Basic Road Created");
}
}
}
Muy bien, vamos a observar con más detalle este código, tenemos la clase “UnlockBasicRoadsMilestones” que hereda el comportamiento del módulo “MilestonesExtensionBase”. Con esta herencia tenemos la opción de sobreescribir la función “OnRefreshMilestones()” y tener acceso a “...UnlockMilestone(“...”);”
Todo esto viene a ser, el módulo “MilestonesExtensionBase” nos da acceso a ciertas funcionalidades sobre los objetivos del juego, por ejemplo, el primer objetivo del juego cuando creas un mapa nuevo es crear una calle básica de doble sentido, cuando se cumpla ese objetivo, el juego desbloqueará la calle básica de sentido único. Sobreescribiendo la función “OnRefreshMilestones()” podemos hacer que nuestra modificación ejecute el código que nosotros queramos cada vez que se actualice la lista de objetivos del juego. En la modificación que estamos analizando,
nos encontramos con:
- Código: Seleccionar todo
base.get_milestonesManager().UnlockMilestone("Basic Road Created");
Esta llamada a la función “.UnlockMilestones(“Basic Road Created");” le dice al gestor de objetivos del juego que considere como cumplido el objetivo “Basic Road Created” y a partir de ahí, el motor del juego se encargará de desbloquear la calle de sentido único.
La siguiente línea de código:
- Código: Seleccionar todo
ChirpLog.Debug("Basic Road Created");
Le envía un mensaje de texto a Chiper a través de las funcionalidades implementadas en el espacio de nombres “ChirpLogger”.
De todo esto que hemos visto, lo primero que me viene a la mente es eliminar el uso de “ChirpLogger” y lo segundo es que al sobreescribir la función “OnRefreshMilestones()”, cada vez que el juego quiera actualizar el estado de los objetivos del nivel, nosotros le estamos diciendo que “Basic Road Created" está cumplido. La duda que me asalta es, si no bastaría con decirle una vez que “Basic Road Created” está cumplido. Pero eso lo exploraremos en el siguiente apartado.
2.3. Implementando.
Es momento de programar, abrimos nuestro Visual Studio, creamos un nuevo proyecto (C# - Class Library), y le enlazamos la librería “ICities.dll” (Project>Add Reference).
En nuestro código C# comenzamos poniendo las directivas:
- Código: Seleccionar todo
using ICities;
using System;
A continuación la definición del espacio de nombres que va a usar nuestra modificación:
- Código: Seleccionar todo
namespace DesbloqueaCallesBase {
}
Dentro del cuerpo del nuevo espacio de nombres añadiremos la clase que va a configurar nuestra modificación.
- Código: Seleccionar todo
public class ModInfo : IUserMod {
public string Name {
get { return "Desbloquea calles base"; }
}
public string Description {
get { return "Modificación que desbloquea la calle básica de
sentido único"; }
}
}
Bien, ya tenemos lo básico para que el juego acepte nuestra librería. Ahora toca pensar, en qué momento, a lo largo de toda la ejecución del juego es más lógico que se le diga al gestor de objetivos que el objetivo “Basic Road Created" ha sido cumplido.
En la modificación de ejemplo, se hacía sobreescribiendo la función “OnRefreshMilestones()” del módulo “MilestonesExtensionBase” de la librería “ICities”, así que ahora sí vamos a ir a la documentación existente a buscar lo que podamos sobre el módulo mencionado. En la wiki del juego nos encontramos la sección (Mod Api) http://www.skylineswiki.com/Modding_API y nos dirigimos al apartado (“2.9.3 MilestonesExtensionBase”) http://www.skylineswiki.com/Modding_API#MilestonesExtensionBase encontrando lo siguiente:
IMilestones milestonesManager { get; set; }
Thread: Any
Gets the milestone manager interface
---------------------------------------------------------------
Acceso al gestor de objetivos del juego con el método get(). Se puede ejecutar en cualquier hilo del juego, es decir, desde el menú principal, desde una partida iniciada, etc.
void OnRefreshMilestones();
Thread: Simulation
Called every time the game checks updates the user progression status
---------------------------------------------------------------
Función que se llama cada vez que se actualiza los progresos en los objetivos del juego. Esta función sólo se ejecutará en el hilo de simulación del juego, es decir desde una partida iniciada.
Además en el apartado (“2.9 IMilestones”) http://www.skylineswiki.com/Modding_API#IMilestones nos encontramos:
void UnlockMilestone(string name);
Thread: Simulation
Unlocks the Milestone designated by the name parameter. Use EnumerateMilestones() to list all the available milestone strings
---------------------------------------------------------------
Función que al llamarla, nos permite decirle al gestor de objetivos del juego, que un objetivo concreto se ha cumplido. Esta función sólo se ejecutará en el hilo de simulación del juego.
Muy bien, como lo que queremos es que se complete el objetivo una vez durante la partida (lo lógico sería una vez termine de cargar el nivel) vamos a echarle un nuevo vistazo a la documentación y nos encontramos en ("2.7.3 LoadingExtensionBase")http://www.skylineswiki.com/Modding_API#LoadingExtensionBase lo siguiente:
void OnLevelLoaded(LoadMode mode);
Thread: Main
Invoked when a level has completed the loading process
The mode defines what kind of level was just loaded
---------------------------------------------------------------
Función que se llama una vez se ha completado el proceso de carga del nivel. Se ejecuta en el hilo principal.
Esto nos da el acceso al punto que queremos, pero no nos da acceso al hilo de simulación que es donde se ejecuta “.UnlockMilestone(string name);”. Seguimos buscando, y en la sección ("2.12.1 IThreading")http://www.skylineswiki.com/Modding_API#IThreading_2 nos encontramos con:
void QueueSimulationThread(Action action);
Thread: Any
Add an action to be executed in simulation thread
---------------------------------------------------------------
Añade una acción a ejecutar en el hilo de simulación
¿Action, y eso qué es? Bueno, eso es de las librerías del sistema de c#, más concretamente “mscorlib.dll”. Si están usando Visual Studio es posible que esté configurado para usar el framework “.Net Framework 4.5” y cuando uses Action y el juego ejecute tu modificación, este mostrará un mensaje de error diciendo que no encuentra “System.Action”. Esto se debe a que el juego usa una librería “mscorlib.dll” de una versión inferior y el sistema de excepciones produce un error. Lo vamos a solucionar dirigiéndonos a (Project> NameProject Properties…), dentro en la sección “Application” y en “Target framework:” seleccionamos “.Net Framework 3.5”. Y con esto se soluciona el dichoso error y podemos seguir explicando qué es una Action.
El uso de Action en este contexto retrasa la llamada de una función a un momento posterior en la ejecución de la modificación, dentro del hilo de simulación. Así que, crearemos una función que complete el objetivo y con la llamada a la función “.QueueSimulationThread(miFuncionCreada);” retrasaremos la ejecución de miFuncionCreada al momento que el juego “desee” dentro del hilo de simulación, pero, solo lo hará una vez.
Empezamos a implementar la clase que nos va a desbloquear las carreteras. Esta clase hereda el comportamiento del módulo “LoadingExtensionBase”, lo que le da acceso a la función “OnLevelLoaded(...)”. Dentro del cuerpo del espacio de nombres de nuestra modificación escribimos:
- Código: Seleccionar todo
public class Desbloquea : LoadingExtensionBase {
}
Ahora, a esta clase le añadimos una función que sea la que haga cumplir el objetivo “Basic Road Created” al juego.
- Código: Seleccionar todo
public void desbloquearCB () {
managers.milestones.UnlockMilestone("Basic Road Created");
}
Posteriormente sobreescribimos la función “OnLevelLoaded(...)” para que se añada la función anterior a la cola de ejecución del hilo de simulación.
- Código: Seleccionar todo
public override void OnLevelLoaded(LoadMode mode) {
managers.threading.QueueSimulationThread(desbloquearCB);
}
Por último, prestemos atención al uso de “managers.milestones….” y “managers.threading….” que nos dan acceso a los gestores del juego de objetivos y de los hilos respectivamente. Todos los módulos que nos proporciona el juego para heredar comportamientos (por ejemplo, “LoadingExtensionBase”) tienen implementado un acceso a un gestor de gestores y así podemos llamar a funciones de otros gestores sin tener que heredar su comportamiento.
2.4. Resumen.
- Visual Studio:
Cambiar de framework: (Project> NameProject Properties…) ->“Application” -> “Target framework:” [.Net Framework 3.5]
- Código C# - Desbloquea calles base:
- Código: Seleccionar todo
using ICities;
using System;
namespace DesbloqueaCallesBase {
public class ModInfo : IUserMod {
public string Name {
get { return "Desbloquea calles base"; }
}
public string Description {
get { return "Modificación que desbloquea la calle básica de sentido único"; }
}
}
public class Desbloquea : LoadingExtensionBase {
public void desbloquearCB () {
managers.milestones.UnlockMilestone("Basic Road Created");
}
public override void OnLevelLoaded(LoadMode mode) {
managers.threading.QueueSimulationThread( desbloquearCB);
}
}
}
PD: Próximo capítulo: "Opciones de configuración."