-[ 0x0C ]-------------------------------------------------------------------- -[ Unete al Software libre ]------------------------------------------------- -[ by FCA00000 ]-----------------------------------------------------SET-34-- Este artículo describe los pasos que he seguido para mejorar un programa de código libre. El nivel es bajo, puesto que el objetivo es demostrar que es fácil subirse al carro del software libre; no sólo como pasajero, sino como tripulación. Este texto quizás es muy obvio para algunos; pero espero que también sirva para aquellos que quieren un empujoncito para meterse en el mundo de la programación. Sin más preámbulo, empezaré recordando mi situación hace algunos años: deambulaba yo por la sala de ordenadores de la universidad, y vi que un tipo perdía el tiempo con un juego de mover fichas por un mapa. En esa época estaba yo aficionado a los juegos de tablero (risk, Squad Leader, ...) así que despertó mi interés. Al día siguiente me acerqué a la sala de nuevo y el chico estaba jugando de nuevo. Lo mismo sucedió al otro día, y me dí cuenta de que el juego parecía ser bastante adictivo. Eso, o el individuo era un ludópata. Me acerqué para preguntarle, y me dijo que el juego se llamaba ... Civilization: uno de los mejores juegos de estrategia de la historia. Lo copié y lo instalé en casa. Al principio parecía complicado porque había muchos tipos de unidades, pero la verdad es que requería esfuerzo dejar de jugar. Desde ese momento, yo calculo que he desperdiciado unas 2.000 horas a lo largo de 5 años. Luego publicaron la típica secuela llamada Colonization, pero no me atrajo lo suficiente. El tiempo ha pasado, ahora me gano el sueldo haciendo programas, incluidos un par de lenguajes que casi nadie conoce, y solo de vez en cuando vuelvo a echar una partida. Con el auge de Internet, era inevitable que surgieran versiones de Civilization, la más avanzada se llama FreeCiv, y es bastante fiel a la original, excepto que no me enganchó. Y también ha salido una adaptación de Colonization, llamada FreeCol (estos tipos no tienen imaginación para los nombres). Ya que no me atrapó en sus inicios, decidí darle una segunda oportunidad a esta nueva versión. La bajo, la instalo, leo la documentación inicial, y me pongo a jugar. El juego en sí es fácil de aprender, y lamentablemente los enemigos no son lo bastante inteligentes como para ponerme en aprietos. Si aumentas el nivel de dificultad, lo único que pasa es que es más gravoso conseguir los recursos y el avance es más lento. Lo que me sorprendió es que el juego parece bastante maduro. No se cuelga, la parte gráfica es aceptablemente rápida, y no tarda una eternidad en mover las fichas enemigas. Al ser un programa de software libre, el código fuente está disponible, así que lo bajé por curiosidad. Y la primera sorpresa es que está escrito en Java ! Eso explica porqué tarda 20 segundos en arrancar, y usa 100 Mg de memoria. También destroza mi creencia de que Java es lento. Una primera ojeada muestra que está dividido en módulos: -el interface gráfico -el modelo de control de unidades, colonias, y terreno -el servidor, usado en modo multijugador -la inteligencia artificial En total, unas 300 clases, aunque sólo la mitad son interesantes: el resto son interfaces, pequeñas variaciones (override) de otras clases, y clases con constantes y excepciones. Después de jugar un par de partidas en 10 horas, me hago una idea de los conceptos del juego. Entiendo que hay: -jugadores -colonias -unidades -edificios -productos -consumos (impuestos, comida, cruces) -padres fundadores -mapa -terreno -turnos Por tanto, espero encontrar clases de estos tipos. Pero no adelantemos acontecimientos. El programa ejecutable viene dentro del fichero FreeCol.jar que ocupa 3 Mb en el disco, y se puede iniciar con java -Xmx128M -jar FreeCol.jar El siguiente paso es obtener el código fuente freecol-0.6.1-src.tar.gz y descomprimirlo en algún sitio. Ahora necesito un programa para navegar por los fuentes, y compilar los cambios. Yo hace bastantes años que no programo en Java, así que no estoy muy puesto al día de los compiladores, entornos de desarrollo, y librerías necesarias. Visitando las webs habituales, decido instalar java 1.6.0 como runtime NetBeans 5.5 como IDE (Integrated Development Enviroment) Así, de paso, aprendo este entorno. No digo que sea el mejor, sino que es el primero que apareció en mi búsqueda. Genero un nuevo proyecto, tomando como base el directorio donde he descomprimido las fuentes. Empiezo a navegar por el código fuente y me familiarizo con el sistema de manejar ficheros. Para ello uso la ventana "Archivos" que los muestra según la estructura de directorios. Otra cosa que necesito es poder seleccionar una clase, y ver su código. Esto se hace con el botón derecho, y: Go To -> Source Go To -> Declaration También es útil el menú Find Usages, y el Edit->FindInProjects Obviamente tengo que ser capaz de re-compilar mis cambios. Esto se consigue con el menú Build, o también con Run. Incluso es mejor el Debug, que permite poner breakpoints. Intento la primera compilación, y se queja de no encontrar la clase en la línea import cz.autel.dmi.HIGLayout; Esta no me suena que sea una clase normal de java, ni tampoco parece ser perteneciente a este proyecto. Busco en el disco una librería cz.* pero no encuentro nada. Después busco HIGLayout.* y encuentro freecol/jars/higlayout.jar La descomprimo y veo que efectivamente contiene dicha clase. Ahora hay que decirle a NetBeans que use el directorio freecol/jars , lo cual se hace en File->Project->Properties->Libraries->Compile-time Lo intento de nuevo, y ahora compila sin problemas. Magnífico: realmente el código fuente está preparado para los que quieren modificarlo. El siguiente paso es ejecutar el programa. También se queja de que no encuentra las librerías, pero se arregla de manera similar, en el sub-menú Libraries->Run-time Eso sí: el entorno usa 100 Mg de memoria, y el juego otros 100 Mg. El Civilization original funcionaba en MS-DOS con 500 Kb. de memoria ! Vamos a probar a hacer algún cambio. Lo más visible del juego es obviamente la parte gráfica. Este es el punto de inicio para ir tirando del ovillo. Por ejemplo, el informe del consejero de asuntos externos (Report->Foreign Affairs Advisor) nos muestra la actitud diplomática que tenemos con otros jugadores, en una lista que incluye la frase: Stance: Peace Para saber dónde se define dicha variable, buscamos la palabra "Stance" y la encontramos en el fichero FreeColMessages.properties en la línea report.stance=Stance esto hace extremadamente fácil la tarea de los traductores, pues las frases que se muestran al usuario están almacenadas en un fichero, que se puede traducir sin necesidad de recompilar el programa. Ahora busco la palabra "report.stance" y la encuentro, entre otros, en el fichero ReportForeignAffairPanel.java en la línea enemyPanel.add(new JLabel(Messages.message("report.stance")), higConst.rc(row, labelColumn)); int stance = Player.getStance(); enemyPanel.add(new JLabel(Player.getStanceAsString(stance)), higConst.rc(row, valueColumn)); La primera línea lo que hace es añadir una etiqueta con el texto obtenido de Messages.message("report.stance") Está claro que esta clase se encarga de leer el texto adecuado en función del idioma del usuario. Por ejemplo, el fichero FreeColMessages_it_IT.properties contiene la traducción a italiano, que contiene report.stance=Linea Diplom. Volviendo al programa, la siguiente línea int stance = Player.getStance(); nos lleva (GoTo Source) hasta la clase Player.java y hace public int getStance(Player player) { return getStance(player.getNation()); } O sea, que es una función de salto a: public int getStance(int nation) { if (nation == NO_NATION) { return 0; } else { return stance[nation]; } } Que a su vez nos referencia al array stance[]; que está definido (GoTo Declaration) al principio de esta clase, como private int[] stance = new int[NUMBER_OF_NATIONS]; y se usa (Find Usages) en public void setStance(Player player, int newStance) Ahí encontramos que los posibles valores son: public static final int WAR = -2, CEASE_FIRE = -1, PEACE = 0, ALLIANCE = 1; A su vez, setStance se invoca desde las funciones: Player.declareIndependence -> WAR contra la madre patria Player.giveIndependence ->PEACE con la madre patria Monarch.declareWar -> WAR contra otro jugador AIPlayer.determineStances -> un jugador no-humano tiene ganas de guerra ahora supongamos que queremos que el juego sea pacífico, y que no queremos líos con los jugadores robots. Entonces antes de stance[player.getNation()] = newStance; incluimos if(newStance==WAR && player.isAI()) newStance=CEASE_FIRE; Obviamente no todos los jugadores estarán de acuerdo con esta regla, así que se podría incluir como una opción. También se podría alterar en función de: -el año: al principo del juego sería calmado, y el final sería más violento -la nacionalidad: los holandeses son más favorables a la paz; los españoles más belicosos -el poder militar: no es bueno enfadar a una nación poderosa -la riqueza: la tentación de un buen tesoro es difícil de resistir -los padres fundadores: Benjamín Franklin es sosegado; Hernán Cortés irascible Pero en vez de alterar el juego, vamos a mejorarlo. En esta rutina setStance hay un error: si declaras la guerra a otro jugador, lo opuesto no sucede. Esto es malo porque el estado de guerra proporciona algunas ventajas en la lucha, y no parece justo que el agraviado ofrezca la otra mejilla. Notar que la función (pseudo-código) es Atacante.setStance(Victima, WAR); por lo que es necesario incluir una nueva línea Victima.setStance(Atacante, WAR); lo cual se hace antes del final: if(newStance==WAR && player.getStance(this) !=WAR) player.setStance(this,WAR); puesto que this es Atacante player es Victima Es decir, intercambiamos los papeles. Una vez que hemos solucionado el bug y lo hemos probado, lo normal es publicarlo. Dependiendo del tipo de control que los autores del programa quieren tener, será necesario subcribirse a algún tipo de repositorio colaborativo, normalmente en un sitio controlado por CVS o SVN, tal como sourceforge. En ese caso, el sitio es http://www.freecol.org que apunta a freecol.sourceforge.net y la lista de correo es freecol-users@lists.sourceforge.net Al principio es una buena net-etiqueta el presentarse, y ofrecer tu cooperación. Los cambios pequeños, y los que haces en los primeros dias tras unirte al grupo, se pueden mandar en un correo diciendo algo así como: Estimado señor programador, en el fichero src/net/sf/freecol/common/model/Player.java cerca de la línea 2073 dice ... if (oldStance == PEACE && newStance == WAR) { ... y yo creo que se podría mejorar, provocando la guerra complementaria. Esto es, debería decir ... if(newStance==WAR && player.getStance(this) !=WAR) player.setStance(this,WAR); if (oldStance == PEACE && newStance == WAR) { ... Atentamente, otro programador. En este caso particular, una revisión de la lógica del juego demostró que este error no era un error: es intencionado. Esto demuestra que todos cometemos errores, sobre todos los novatos. Antes de abrir la boca, hay que saber de lo que se habla. Al cabo del tiempo, cuando los otros programadores confían en tí, puedes crear un usuario en sourceforge y trabajar directamente sobre el repositorio. Para ello tienes que instalar CVS/SVN y luego: -transferir la versión más reciente a tu ordenador -averiguar qué fichero quieres modificar -checkout -hacer los cambios -probarlos -checkin En algunos grupos de colaboración sólo el administrador puede hacer cambios. En este caso debes mandarle los cambios para que los pueda revisar e instalar. Esto se hace en forma de parches: Primero debes tener la versión más reciente del código fuente. Luego haces los cambios en tu ordenador, probando que todo funciona bien. Comparas los ficheros: diff -U 3 -H -d -p -r -N original/src/..../Player.java FCA00000/src/.. ../Player.java que genera un fichero parecido a esto: diff -U 3 -H -d -p -r -N original/src/net/sf/freecol/common/model/Player.java FCA00000/src/net/sf/freecol/common/model/Player.java --- original/src/net/sf/freecol/common/model/Player.java 2007-04-03 19:48:42.000000000 +0200 +++ FCA00000/src/net/sf/freecol/common/model/Player.java 2007-05-07 15:19:58.000000000 +0200 @@ -2073,33 +2073,34 @@ public void setStance(Player player, int newStance) { +if(newStance==WAR && player.getStance(this) !=WAR) player.setStance(this,WAR); if (oldStance == PEACE && newStance == WAR) { como puedes ver, incluye el nombre del fichero modificado, la función donde está en cambio, la línea original, el código original, y el código añadido. Esto se guarda en un fichero FCA00000.2007.05.07.diff y se le manda al administrador del proyecto, que lo incluirá si le parece bien. Si tu nuevo código es útil y está bien escrito, lo incluirán. También es posible que lo cambien para adecuarse a las normas de escritura (mayúsculas, indentado, formateado, ...) y no debes ofenderte si recibes alguna crítica. Al fin y al cabo, ellos estaban antes, así que eres tú el que debe adecuarse a sus reglas. Siguiendo con las modificaciones, voy a dar otro ejemplo de un cambio que yo he hecho a este programa. El objetivo del juego es declarar la independencia de la madre patria. Para eso fundas colonias y construyes edificios. Una vez que has completado uno puedes empezar con otro, pero solo sucede automáticamente en unos pocos casos: si acabas de construir un almacén, inmediatamente empiezas a trabajar en uno más grande. Cuando lo finalizas, no hay otra ampliación disponible. Esto provoca que algunas de tus colonias no construyen nada, lo cual es una pérdida de recursos. Es posible hacer aparecer un menú con la lista de todas tus colonias, y te muestra los edificios ya construidos, además del que se está construyendo (en color gris). Lamentablemente no es evidente que no se está construyendo nada. ¿Dónde se programa esta lista? Bueno, el título es "Colony Advisor" que está definido como menuBar.report.colony=Colony Advisor que, entre otros, aparece en ReportProductionPanel.java que hace ... add(new JLabel(Messages.message("Colony")), higConst.rc(1, colonyColumn)); ... for (int colonyIndex = 0; colonyIndex < colonies.size(); colonyIndex++) { ... Building building = colony.getBuildingForProducing(goodsType); } Así que pongo un breakpoint en getBuildingForProducing y arranco el programa. Hago aparecer el panel "Colony Advisor" y efectivamente acaba en el breakpoint. Lo que me sorprende es que mirando el stack (pila de llamadas) descubro que viene desde ReportColonyPanel.java Bueno, me he equivocado de panel, pero he llegado al mismo punto. Analizo la rutina private JPanel createBuildingPanel(Colony colony) { ... for (int buildingType = 0; buildingType < Building.NUMBER_OF_TYPES; buildingType++) { buildingPanel.add(new JLabel(building.getName())); if (buildingType == colony.getCurrentlyBuilding()) { buildingLabel.setForeground(Color.GRAY); } } Lo cual quiere decir: -para esta colonia: -para cada edificio: -muéstralo -si se está construyendo, ponlo de color gris Lo que quiero hacer es que si no se está construyendo nada, mostrar una línea en rojo que lo diga. Algo así como: if(colony.getCurrentlyBuilding()==NULL) buildingPanel.add(new JLabel("No se está construyendo nada"), Colour.RED); Hay varios pequeños detalles: -primero, getCurrentlyBuilding() devuelve -1 , no NULL -segundo, que JLabel no admite un color como segundo argumento. -por último, que queremos que aparezca en todos los idiomas Todos estos ajustes son detectados por el compilador, por lo que no es necesario probar el programa para darse cuenta de que no funciona. De hecho, la manera correcta es if (colony.getCurrentlyBuilding() == -1) { JLabel unitLabel = new JLabel(Messages.message("nothing")); unitLabel.setForeground(Color.RED); buildingPanel.add(unitLabel); } Es más, el entorno NetBeans permite modificar el código en tiempo de ejecución. Usando el menú Run->ApplyCodeChanges los cambios quedan reflejados inmedatamente. Por supuesto que no todas las clases permiten esto, pero si funciona, es un esfuerzo que te ahorras. Probarlo es fácil: hago que una colonia no construya nada, produzco el informe, y efectivamente se muestra una línea "Nothing" en color rojo chillón. En los primeros dias de jueguetear con el código fuente es habitual estar perdido y navegar desde una clase a otra sin encontrar lo que buscas, Aparte de poner breakpoints y examinar el stack, también es útil tracear el programa. En este caso, los autores han previsto que necesitan saber lo que va haciendo el programa. No sólo para ellos mismos, sino para que otro usuario que tenga un error pueda mandar un informe detallado de la situación. Eso se consigue a través de una clase logger que escribe en un fichero dependiendo del nivel de detalle que necesitas. Incluye métodos severe, warning, info, config, fine, finer, finest Cuando quiero saber cuáles han sido las rutinas y clases ejecutadas más recientemente, sólo tengo que mirar las últimas líneas de este fichero. Esto es un método realmente cómodo. Es sorprendente que haya muchos proyectos que no incluyen una traza de ejecución, sobre todo cuando disponen de capacidad de hacerlo. Me pregunto cómo hacen para debuggear errores en el entorno de producción. También he encontrado útil el uso del parámetro java -verbose cuando se ejecuta el programa. Esto muestra todas las clases que se van cargando, por lo que resulta más eficiente para saber dónde buscar. Bueno, eso es todo. Ahora, ve y busca un proyecto en sorceforge. Seguro que encuentras alguno en el que cooperar. Yo, voy a ver si acabo con los Franceses de Louis XIV en Martinique. Este artículo ha sido escrito en 5 horas, sin contar la investigación inicial sobre los fuentes de Colonization. Durante su redacción, ningún vegetal fue sometido a daños innecesarios. *EOF*