Creando mi propio Photo Mode para Yakuza Kiwami 2
Resultados
Para captar tu atención, acá están los resultados de un Photo Mode implementado en Rust.
Now the Freecam for K2 automatically removes the UI for a proper "photo mode". It even removes the health of the enemies in heat actions! (Hide UI doesn't remove that).
— etra (@etra0) May 23, 2020
As always, you can get the latest release @ https://t.co/mLpG5p53ds pic.twitter.com/Fp1kJBnAt7
Habiendo terminado el Yakuza 0 (what a great game) y el Yakuza Kiwami (not so great tbh), naturalmente, el siguiente juego era Yakuza Kiwami 2.
Yakuza Kiwami 2 es conocido por tener un rendimiento no muy bueno, incluso
un modder por el nombre de Silent
hizo un parche para mejorar el frame pacing, donde se dio cuenta que para
limitar el framerate usaron un sleep
(sleep
nunca es confiable debido
a que depende del scheduler de Windows), sin embargo, a pesar de estos
problemas, el juego se ve increible. Quedé inmediatamente impresionado por los
gráficos, tiene un look que a mi por lo menos me atrae bastante.
Lamentablemente, a pesar de sus gráficas impresionante, el juego no trae un “Photo Mode” (en mi opinión, esto debería ser estándar a estas alturas), sólo se pueden tomar capturas cuando estás navegando libremente por la ciudad, pero esto limita inmediatamente la opción de obtener increíbles capturas en batallas por ejemplo, o tener panorámicas de la bella ciudad de Kamurocho.
Obviamente, como he estado investigando sobre el reverse engineering, esto no se podía quedar así
Introducing a new safe friend: Rust
Como ya mencioné, le tengo cierto cariño a los lenguajes compilados, me gusta lo eficiente que son, y lo fácil que es manipular la memoria, sin embargo, este es un poder un tanto “peligroso”, porque es facil apuntar a cosas que no existen, o hacer aritmética de punteros mal.
Habiendo amado C, y probado C++, me encontré con una nueva alternativa: Rust.
Existen otras alternativas a los lenguajes compilados, C#, Go, Rust, etc. todas con sus propios beneficios y defectos, pero Rust me llamaba la atención por ser conocido como un safe language, y por eso siempre lo tuve al ojo, pero nunca me dediqué a explorarlo, hasta que obviamente, la necesidad de programar en lenguajes compilados nació con el aprendizaje de esta área, así que ya era buen momento para meterle mano po'
Primeros pasos en Rust: memory-rs
Para crear un entorno de trabajo cómodo, lo primero es crear una librería
que permita facilitar el manejo de memoria en Windows. En este sentido,
la API es bastante straightforward, se necesita un HANDLE
para el proceso,
y teniendo la dirección de memoria base del módulo (en este caso, del EXE),
ya es suficiente para comenzar a manipular el proceso. Bajo este esquema,
lo más natural es crear una estructura que contenga esta información
como base para no tener que guardarlas en variables cada vez que se vayan
a utilizar, y además, implementar un par de métodos de cosas que se hacen
frecuentemente para reducir la redundancia.
Bajo esta necesidad, el primer paso fue crear la librería memory-rs, la cual me permite hacer algunas cosas de forma bastante más cómoda:
let process = Process::new("YakuzaKiwami2.exe")
.expect("El proceso no se pudo abrir");
// autocasteo de variable segun el tamaño
let x_pos = process.read_value::<f32>(addr);
// Se puede escribir un array directamente a la memoria
process.write_value::<[f32; 2]>(addr, [0., 0.]);
// Escribir array of bytes
let aob: Vec<u8> = vec![0xC0, 0xFF, 0xEE];
process.write_aob(addr, &aob);
Sin embargo, la función más interesante, es la que me permite inyectar shellcodes a los procesos, por ejemplo, tengo el siguiente código:
.data
PUBLIC get_controller_input
PUBLIC get_controller_input_end
;; Intercept the controller input
get_controller_input PROC
push rax
; the pointer to the controller structure is stored in the head
; of the stack, but since we pushed rax and the return address,
; we need to add 0x8 + 0x8 to the RSP
mov rax,[rsp+10h]
push rbx
lea rbx,[get_controller_input + 200h]
mov [rbx],rax
pop rbx
pop rax
; original code
test eax,eax
mov rax,[rsp+108h+8h] ; adjusted stack offset
ret
get_controller_input_end::
get_controller_input ENDP
Que intercepta el puntero de la estructura del control (o joystick) y
lo guarda en pos_injection + 0x200
, en Rust puedo facilmente inyectarlo con
extern {
static get_controller_input: u8;
static get_controller_input_end: u8;
}
fn main() {
...
// guardo la direccion de inyeccion, para despues poder acceder al
// valor interceptado
let p_shellcode = unsafe {
process.inject_shellcode(entry_point, size_origin_addr,
&get_controller_input as *const u8,
&get_controller_input_end as *cosnt u8)
};
// guardo el puntero al control
let p_controller = process.read_value::<usize>(p_shellcode + 0x200);
}
Y por lo tanto, como se puede notar, esta librería me facilita bastante la escritura de scripts en assembly, lo cual se hace con bastante frecuencia.
Mejorando lo que ya se tenía: freecam anteriores
Para el caso de Yakuza 0 y Yakuza Kiwami, las herramientas de freecam eran bastante… primitivas, ya que el movimiento no era natural, si no que se movía en base a los ejes $(X, Y, Z)$, lo cual obviamente no es intuitivo ya que depende de como está hecho el mapa y mucha gente se quejaba de eso (totalmente entendible).
Luego de largas jornadas de debugging con el amado Cheat Engine, logré encontrar la función global que maneja las cámaras para el caso del Yakuza Kiwami 2, con un gran detalle: las estructuras no contenían ni Pitch ni Yaw, si no que eran 6 valores: Punto 3D donde se enfoca, y Punto 3D a la posición de la cámara (de ahora en adelante, focus y position respectivamente).
¿La solución? back to Cálculo 3.
Mate no sirve pa ná, except some times it does
La forma más natural de navegación es con WASD
y mouse,
donde W
, S
mueven en dirección de donde se mira, y A
, D
en dirección
lateral, y obviamente con el mouse se puede cambiar donde se apunta.
¿El problema? ambos inputs por separado solo entregan coordenadas en dos
dimensiones, donde W
y S
entregan un $\Delta x$, A
y D
un $\Delta y$,
y el mouse $x \wedge y$.
Coordenadas esféricas: boyoyoi
Como se tienen dos puntos 3D en el espacio (el de Focus y el de Position) es fácil imaginar la situación como una esfera, donde la posición de la cámara corresponde al centroide, y la posición del Focus corresponde a un punto en el cascarón de la esfera. Habiendo aclarado esto, un vector en coordenadas esféricas está compuesto de la siguiente manera:
$$ (r, \theta, \phi) \in [0, R] \times [0, 2\pi] \times [0, \pi] $$
¿Por qué estamos haciendo esto? Porque en las coordenadas esféricas el $\theta$ corresponde a la visión 360º en horizontal, y $\phi$ corresponde a la visión en 90º vertical, y con estos valores, podemos mapear las coordenadas $x$ e $y$ que se reciben del mouse a la dirección en donde se mira.
¿Como se hizo al final? Se calculó un vector que llamaremos $\overrightarrow{R} = F - P$ y la velocidad de movimiento del mouse con un pequeño delta de tiempo ($\Delta x$ e $\Delta y$), definiendo así un nuevo $\overrightarrow{R}_n$ tal que:
$$ \overrightarrow{R}_n = (r, \theta + \Delta x, \phi + \Delta y) $$
Luego, el vector $\overrightarrow{R}_n$ se cambia a coordenadas cartesianas para obtener un nuevo $F$ y $P$ de tal manera que:
$$ F_n = P + \overrightarrow{R}_n + \Delta p \cdot \overrightarrow{R} \\ P_n = P + \Delta p \cdot \overrightarrow{R} $$
donde $\Delta p$ corresponde al movimiento del jugador con WS
. Listo,
con eso tenemos una manera de manipular las coordenadas de forma intuitiva!
El código se ve algo así como:
fn calc_new_focus_point(cam_x: f32, cam_z: f32,
cam_y: f32, speed_x: f32, speed_y: f32) -> (f32, f32, f32) {
// use spherical coordinates to add speed
let theta = cam_z.atan2(cam_x) + speed_x;
let phi = (cam_x.powi(2) + cam_z.powi(2)).sqrt().atan2(cam_y) +
speed_y;
let r = (cam_x.powi(2) + cam_y.powi(2) + cam_z.powi(2)).sqrt();
let r_cam_x = r*theta.cos()*phi.sin();
let r_cam_z = r*theta.sin()*phi.sin();
let r_cam_y = r*phi.cos();
(r_cam_x, r_cam_z, r_cam_y)
}
...
self.f_cam_x = self.p_cam_x + r_cam_x + dp_forward*r_cam_x +
dp_sides*r_cam_z;
self.f_cam_z = self.p_cam_z + r_cam_z + dp_forward*r_cam_z -
dp_sides*r_cam_x;
self.f_cam_y = self.p_cam_y + r_cam_y + dp_forward*r_cam_y +
dp_up*r_cam_y;
self.p_cam_x = self.p_cam_x + dp_forward*r_cam_x + dp_sides*r_cam_z;
self.p_cam_z = self.p_cam_z + dp_forward*r_cam_z - dp_sides*r_cam_x;
self.p_cam_y = self.p_cam_y + dp_forward*r_cam_y + dp_up*r_cam_y;
Cabe destacar que se sumaron unos deltas extras para el movimiento lateral y el movimiento en el eje y.
Devuelta al código
Después de haber hecho esa matemagia (quizás es básica, pero hey, I’m proud) lo único que faltaba era escribir un par de shellcodes para interceptar los valores guardados y reescribirlos a la fuerza y así fue como hice la primera versión del freecam
Aaaand the freecam tool for K2 is working as intended! feel free to take a peek at https://t.co/mLpG5p53ds in releases! #rustlang #modding pic.twitter.com/Swk6B73H9J
— etra (@etra0) May 6, 2020
De Freecam a Photo Mode
La historia no termina allí. De seguro es cool poder explorar el mundo libremente del personaje, pero las batallas y el ragdoll del juego es tan genial que simplemente no me pude detener allí, así que nació un nuevo objetivo: Hacer un “Photo Mode” real, donde se pueda pausar y explorar la escena.
Comprobar que fuera factible no fue tan complejo, ya que me di cuenta que si pausaba el juego (i.e. me iba al menú), podía seguir moviendo la cámara y los personajes seguían quietos, lo cual me motivó a seguir ese approach.
Luego de largas jornadas de búsqueda en Cheat Engine, encontré la variable que pausaba el juego, ¿El problema? si pausaba el mundo, y salía del menú, todo tipo de input quedaba bloqueado, por lo tanto no había forma de despausar el mundo, a pesar de que yo modificase esa flag nuevamente.
Por casualidad, un par de bytes más al lado del pause flag
, estaba la flag
que verificaba si el juego estaba en primer plano, y por pura suerte,
cuando esa flag cambiaba el juego se pausaba y volvía a otorgar control, por
lo tanto la solución fue forzar esa flag a cambiar por un pequeño delta
de tiempo para que el juego volviese a forzar la pausa y aceptara input:
fn trigger_pause(process: &Process, addr: usize) {
if addr == 0x0 { return; }
process.write_value::<u8>(addr, 0x1);
thread::sleep(Duration::from_millis(20));
process.write_value::<u8>(addr, 0x0);
}
Bastante hacker si me preguntan a mi :P
Como toque final, había que ocultar la UI del juego, que básicamente
se reducía a nopear
instrucciones, así que nada tan complejo.
Retoques finales
Finalmente, como A real Yakuza use a Gamepad, evidentemente el juego estaba diseñado para ser jugado con un control, por lo tanto, era un poco molestoso estar obligado a ocupar el teclado para usar el Photo Mode, así que con un poco más de experiencia, se me ocurrió la siguiente solución:
- Revisar los
DLL
que carga el ejecutable; para Windows, elDLL
que maneja los controles esXINPUT
- Obtener todas las llamadas a las funciones
GetState
deXINPUT
- Revisar las estructuras que se modifican durante esas llamadas
- Escribir un sniffer que obtendrá el puntero a la estructura del control
- Leer ese input y manejarlo
Y pam! obtuve el input del joystick, así que ahora el control es mucho más natural!
Conclusiones
Definitivamente, este es uno de los hacks más entretenidos que he hecho, ya que pude aprender un poco de Rust, mejorar los conocimientos de manejo de memoria, y hacer un freecam realmente usable.
El proyecto tuvo una excelente recepción en internet, alcanzando 70 estrellas en GitHub y estando entre los top posts del subreddit de Rust!
Además, ahora puedo hacer hermosas capturas de un juego que se ve realmente hermoso.