[Devlog] Emulador de WarRock CP1

DarkRaptor

WarRock Chapter 1 Private Server (WCPS) standard

Hola a todos.

Lo primero, me presento. Soy un habitual del foro de /mafia/ donde disfruto viendo pelearse y failear a kidandcat y heridote Ridote. Ahora mismo estoy en un momento vital en el que tengo bastante tiempo libre y he decidido (re)programar un emulador de los servidores de WarRock, un clon F2P del BF2 más viejo que su puta madre (año 2005) y con el que me inicié en los FPS.

En concreto, voy a utilizar el cliente del año 2008, versión internacional, parcheado hasta PF20, que me consta que es fácil de conseguir por ahí. A título personal, prefiero PF23 pero requiere hacer una serie de modificaciones extensivas para saltarse las rutinas del fallecido HackShield y considero que es más inaccesible. ¿Por qué el capítulo 1? Porque es el que más me gusta :)

Motivación aka por qué hacer esta mierda

La principal y más importante: porque me parece divertido y me relaja. Este juego formó parte de mi adolescencia y conocí a gente genial a través de la comunidad hispana, con la que sigo en contacto. Además, fue uno de los empujones que tuve para aprender a programar.

En segundo lugar, porque me gustaría dejar documentado cómo funcionaba este cliente en caso de que alguien en algún momento quiera programar su propio emulador en condiciones. Lo que hay/había por ahí en la época en la que yo le daba activamente a la scene, eran servidores montados por alemanes adolescentes que se robaban las sources entre ellos y el código era una especie de basurilla edgelordesca, todo esto aderezado con foros de hacking y venta chunga xd Esto echó para atrás a mucha gente que tenía curiosidad.

Pe-pero tío el juego era una basura y esto no lo va a usar nadie
Exacto, esto lo va a usar menos gente que Vircon32 y tiene menos futuro que los foros, pero para mi es un pasatiempo a la altura de cultivar lechugas cuando me sale más caro el abono y el agua que comprarla en el supermercado. El juego es un clon barato del BF2 con más agujeros que un colador en cuanto a seguridad. Los servidores oficiales ni siquiera eran autoritativos, sino más bien enrutadores de tráfico con 4 comprobaciones cutres.

FAQ

¿Cómo funciona esto a grandes rasgos?
¿Por qué python y no (inserte aquí lenguaje de programación) ?
¿Por qué asyncio y no usar hilos a pelo?
¿Por qué este y no el de pepito que es mil veces mejor?
Tu implementación tiene un rendimiento de mierda/es un coladero de seguridad/basura
¿Esto es legal?
¿Eres el DarkRaptor de WarRock.es que montó un servidor hace más de un lustro?
¿Qué puedo esperar de este devlog?
¿Por qué no hay tests unitarios y no sigues PEP xxxx?
¿Estás usando chatGPT? Esta función apesta a chatGPT

Estado del proyecto

De momento he seguido la estructura más básica de las implementaciones de C# incompletas que tenía en su momento, por comodidad. He escrito un módulo común con algunas funciones básicas y el servidor de autentificación entero: tanto para otros servidores de juego como para el propio cliente, junto con una DB pequeñita para almacenar los datos más sencillos. A su vez, tengo un STUB (por llamarlo así) de Game Server que está casi todo en estático ahora mismo, pensado para probar el propio servidor de autentificación. El objetivo es refactorizarlo y empezar a desarrollar el servidor de juego propiamente dicho.

Repositorios

Iré actualizando conforme vaya avanzando. Primero quiero hacer un refactor importante a la chapuza que he hecho en el servidor del juego, porque como ya he dicho estaba usándolo para probar el de autentificación sobre todo y está todo caótico y en estático xd

Imagénes y gifs

Palabras finales

Si alguien quiere montar este server o cualquiera por su cuenta, preguntad sin miedo que no muerdo. Aprovecho para citar a @imPJ1712 catedrático del juego, por si en algún momento digo alguna gilipollez que por favor me corrija. Las sugerencias o comentarios son bienvenidos, el proselitismo no gracias jaja. Pensad que este es un proyecto distendido con 0 intención de llegar a nada, así que no espereis nada profesional.

Un saludo

5
Sesshoumaru1
#1DarkRaptor:

python

2 2 respuestas
DarkRaptor

#2
NI
UN
PUTO
MINUTO

:laughing:

1 respuesta
Sesshoumaru1

#3 Estoy siempre al acecho cuando me citan jajaja ánimo con el proyecto! Si necesitas una mano de lo que sea ya sabes donde encontrarme

Jastro

dioooooooos el War rock que buenos vicios me pegueeee, el juego te permitia hacer locuras hacer caballito con la moto mientras te perseguian los helis jajajaj good old times

1 respuesta
DarkRaptor

#5
Era un clon cutre del BF2, TODO se lo tragaba el server como los juegos que hacía Midgard y la única razón por la que el sistema de armas y tal aguantaba era porque la gente que hacia cheats prefería inyectar en memoria a modificar los ficheros del juego y romper el checksum CRC (cosa que ahora se hacer) o modificar el Global.FCL

De hecho durante una década fue posible hacer un MitM que te permitía robarle a alguien la cuenta si el timing era el correcto. Esto se destapó cuando le mangaron la cuenta a un random de nivel altísimo, aunque yo ya no jugaba cuando pasó así que no conozco los detalles. Lo que si he visto es por qué ocurría al programarlo yo. De nuevo, resultado de ser un juego que nunca debió salir de Corea.

Además hay cantidad de contenido sin usar exagerada, dado que tenían un sistema regional donde la versión directriz era la coreana (ojalá un cliente del coreano) y el resto de versiones licenciaban componentes. Pero claro, se dejaban cosillas en el tintero. Cuando estuve más a full con el cliente fuimos capaces de meter mapas, sacar TODAS las armas que tuvieran alguna referencia en el fichero de animaciones y nos lo pasamos muy bien, no nos vamos a engañar.

1 respuesta
Jastro

#6 pues lo espero con ilusion, me encantaria volver a probarlo. Este y el Duelz creo que se llamaba, le di muy duro en mi juventus

1
hda

Me parece un proyectaz9o, Raptor. Con ganas de seguirlo y, sobre todo, de que pronto me lo cuentes en persona con esa pasión tuya que tanto te caracteriza.

Mucho ánimo :D

2
imPJ1712

Dale duro! Seguro que sale algo espectacular. Ganazas de volver a Blue Storm 😁

2 respuestas
DarkRaptor

Aprovechando que refactorizo el game server, voy a comentar a grandes rasgos cómo va el tema de los paquetes. Ayer me preguntaron por otro sitio y me parece interesante, así que allá vamos.

En líneas generales, WarRock usa el puerto 5330 y 5340 para comunicarse por TCP con el servidor. Los paquetes están "cifrados" con una clave XOR que lleva años rota (el cómo lo puedo contar en otro post si hay interés) y tan solo hay que descrifar, sanear y responder. Un ejemplo simplón puede ser el login packet:

Si imprimimos a pelo el paquete tal cual llega cuando intentamos hacer login, recibimos esto:

b'\xf7\xf3\xf6\xfa\xfa\xf0\xf2\xf7\xe3\xf7\xf0\xf6\xf1\xe3\xf0\xf2\xf6\xf1\xf7\xf6\xf3\xf2\xf3\xf4\xe3\xf3\xe3\xa7\xa2\xb1\xa8\xb1\xa2\xb3\xb7\xac\xb1\xe3\xa7\xa2\xb1\xa8\xb1\xa2\xb3\xb7\xac\xb1\xe3\xf3\xe3\xf3\xe3\xc9'

La "b" es la forma que tiene Python de indicarme que estoy imprimiendo una cadena de caracteres que representa bytes, es decir, una cadena de bytes. El resto es el paquete tal cual.

Aunque yo ya conozco la clave XOR, quiero aclarar que es bastante fácil romperla si sabéis que el username y password que he puesto es darkraptor/darkraptor y de alguna manera, esta información le tiene que llegar al servidor. SURPRISE, una vez descifrado, veréis que está en texto plano.

Las claves XOR son estas:

class ClientXorKeys:
    SEND = 0x96
    RECEIVE = 0xC3

Importante: WarRock está programado en C#. Sabemos esto entre otras cosas porque cuando yo era un chavalín, pushearon los .pdb por error en un update y bueno xddd no es la primera vez que se ha filtrado algo del debug. ¿Por qué esto es importante? Porque para tomar muchas decisiones "por defecto", podemos limitarnos a pensar en cómo funcionaba el C# de aquella por defecto. ¿En qué unidades se expresa el tiempo en las librerías más comunes? ¿Con qué tipos mandarían los paquetes? Pista: casi todo es un ushort en el juego xd Puedes desbordar el cliente mandando valores suficientemente grandes en los paquetes jaja.

Para "descifrar" el paquete hacemos algo tal que así

    def xor_decrypt(self, packet_buffer: bytearray, xor_key: int) -> str:
        # Decrypt the bytes with the XORKey.
        this_buffer = bytearray(packet_buffer)
        for i in range(len(this_buffer)):
            this_buffer[i] = this_buffer[i] ^ xor_key

    # Return the decrypted packet
    decoded_buffer = this_buffer.decode("utf-8")
    return decoded_buffer

Y obtenemos algo como:

INFO:root:IN:: 40599314 4352 3152450107 0 darkraptor darkraptor 0 0 

El INFO:root:IN:: es mi logger, lo que sigue es el paquete, en bloques.

La estructura del paquete es tal que así:

  1. Un encabezado de 2 bloques: los ticks del cliente y el id del paquete, en este caso 4352 o 0x1100 como lo veréis en muchas sources por ahí. Es básicamente lo mismo en hexadecimal.

  2. Los datos per sé. En este caso el primer bloque lo ignoro para este paquete porque no está bien documentado qué es en el capítulo 1. Esto es bastante común al hacer emuladores, no sabemos de antemano qué es todo. Estoy seguro de que tengo algo en mi PC ppal pero como estoy de reformas en casa pues :man_shrugging:

Los dos siguientes bloques son el usuario y la contraseña y los dos últimos están vacíos en el primer login, pero sirven para reportar el Displayname (nickname) y el session id en los relogs. Esto lo se porque si respondemos al paquete y salimos y entramos del server, podemos ver cómo estos bloques cambian en función de lo que hemos añadido.

Ahora solo tenemos que montar la lógica para verificar si la información del cliente es verídica, si podemos creernos la conexión y en caso afirmativo responder con nuestro propio paquete. La lógica me la voy a saltar porque es bastante típica y la tenéis en el repo, pero un "OutPacket" es tal que así:

Tocho paquete

Como podéis ver, todavía hay algunos bloques que no se lo que hacen, pero no me preocupa. Cuando recupere mi PC ppal puedo hacer algo de investigación al respecto, aunque no son críticos para para este cliente. Es importante recordar que en WarRock hay mucho contenido sin usar por el sistema de versiones que tenía.

Y para construir el paquete, la clase abstracta de la que derivan todos los paquetes del servidor a su vez implementa esta función:

    def build(self, encrypted: bool = True) -> bytearray | bytes:
        full_packet = " ".join(self.blocks)
        # End of packet character
        full_packet += chr(0xA)
        full_packet = full_packet.encode("utf-8")

    if encrypted:
        full_packet = self.xor_encrypt(packet=full_packet)

    return full_packet

Como podéis ver, hay que añadir algnos caracteres a pelo en el paquete, pero lo demás es bastante lógico. Ensamblar la cadena, pasarlo por el XOR de vuelta y enviar.

EDIT: DAOS CUENTA de que pongo "NULL" en el campo de password a mano porque si no el cliente se lo traga y almacena COMO UN CAMPEÓN y luego más tarde lo manda como parte de otro paquete. IMAGINAD la broma de ir mandando las contraseñas por ahí.

EDIT2: la forma de iterar en la función de descifrado es manifiestamente mejorable pero cuando la hice estaba empezando con Python y tenía todas las manías de otros idiomas. Prometo arreglarla en un futuro.

Jastro

me parece increible la que tienes montada, como se te ocurrio esto?

Te apetecia volver al warrock y decidiste ponerte manos a la obra o el que? xD

1 respuesta
carra

(borro, me equivoqué de hilo, sorry!)

DarkRaptor

Bueno, ya he acabado de dejar el Game Server en un estado en el que se puede desarrollar y no solo usarse para probar el de autentificación. Faltan 3 gilipolleces, que vienen a ser la config, los ficheros para empaquetar y la CLI pero esos los voy a implementar en 10 minutos haciendo copia pega del de autentificación porque son muy parecidos.

El siguiente paso es empezar a implementar los sistemas de juego. Es un tema que se lleva muy diferente entre el desarrollo de mods o juegos completos y el de emuladores, ya que en este caso tenemos un cliente ya completo que no nos va a permitir aislar los subsistemas como queramos.

En ese sentido, a mi, en estos proyectos, me gusta pensar en unidades de desarrollo mínimas que no requieren implementar a su vez otros subsistemas para funcionar. Esta noche si puedo me explayo, que la idea es empezar implementando el reloj del servidor e incluso algo tan sencillo como el ping nos arrastrará a implementar de nuevo el premium y una parte de la base de datos :laugh:

#11
El tema es que ya hace más de un lustro que tuvimos en la comunidad hispana un emulador funcional y hay hasta algún video de YouTube por ahí. No se si @imPJ1712 guarda alguna foto.

El caso es que en aquel momento yo no sabía ni 1/4 de lo que se ahora (o eso quiero pensar) y construimos sobre una codebase pública so-so. Los objetivos ahora son otros como he comentado en #1 Si simplemente quisiera arrancar otra vez un servidor de estos para pegar 4 tiros usaría la basurilla que teníamos jaja

Lo dicho, a ver si esta noche me puedo sentar a implementar ya algunos sistemas y a comentarlo por aquí.

DarkRaptor

Pues hay avances. Me voy a ahorrar la parte en la que he subido los dos servidores a un vps (ha funcionado) porque ha sido simplemente modificar un poco las configs (me dejé campos hardcoded), vamos más con la chicha:

Cómo suelo afrontar el desarrollo

A diferencia de desarrollar un juego, en los emuladores de servidor nos encontramos con un cliente que ya está terminado. ¿Eso lo hace más fácil no? Bueno, no hay documentación así que no xd Es más, el principal problema como veréis es que es difícil aislar, picar y testar algunos sistemas porque dependen de otros 4 o 5 y el cliente directamente crashea si no mandas algo.

Un ejemplo muy ilustrativo es uno de los primeros paquetes que mandamos con el servidor de juego. Es prácticamente la carta de presentación. Realmente es el tercero, pero el paquete de conexión y el de decirle al cliente la hora son triviales.

Paquete en estático/hardcoded

Como podéis ver, ya en el primer paquete podemos ver varios sistemas que habría que implementar para que la información fuera real. Resumidamente:

  1. Los datos básicos del usuario: id de sesión, el username, el nickname y el clan.
  2. Las estadísticas: premium, nivel, experiencia, dinero, kills, deaths.
  3. El loadout o equipamiento que lleva en los 8 slots disponibles para las 5 clases, aunque por defecto solo 4 se usan.
  4. La cadena de 31 FUCKING bloques donde cada bloque es el ID de una pieza de inventario. Los códigos los define el cliente y son por ej. DC04 para la FAMAS.

Podríamos dejar esto y casi todo en estático y avanzar hasta que implementemos todos los paquetes, pero esta estrategia no me gusta por 2 motivos:

a) No solo te comprometes a implementar en el servidor todos los sistemas que ocultas bajo valores predefinidos, te comprometes a que se integrarán BIEN con lo que tengas en ese momento.

b) Te privas del ciclo normal de probar los componentes del juego que ya has reimplementado mientras pruebas y desarrollas otros. Por ej. cada vez que quiera entrar al game server a ver algo del inventario, tengo que pasar por el login si o si.

Mi forma de hacerlo

Surge entonces otra pregunta: por dónde empezar. Y yo aquí suelo ir al sistema más pequeño que puede funcionar de manera independiente del resto y construyo desde allí. Por tanto, para "terminar" este paquete, tenemos que sentarnos a programar:

1) El "nickname", que es un dato que ya saneo internamente contra el servidor de autentificación. Solo tengo que parsear el bloque en el paquete de confirmación de jugador autorizado e incorporarlo a la clase user. Fácil.

2) La primera tabla de la base de datos del servidor de juego, que es distinta al de autentificación. En este caso, tengo que implementar funciones para leer los datos generales del jugador: username, nivel, experiencia, dinero y las estadísticas: kills/deaths y por otro lado banderas capturadas, partidas ganadas, bombas desactivadas etc. Las pongo aparte porque no cuesta nada ampliarlas con información adicional que el cliente no usa. Igualmente, fácil.

3) El "loadout" por defecto del jugador y su inventario. Este último punto es tricky y empiezan las dependencias. En el WarRock, las armas se alquilan por una duración determinada y luego hay que volverlas a comprar por dinero dentro del juego. Implementar el inventario está íntimamente ligado a llevar la cuenta de si los items han expirado o no y mandar los paquetes correspondientes (que el cliente SI TIENE) en lugar de simplemente hacerlos desaparecer de la DB en cuanto expire el periodo de alquiler.

Además el "premium" también podía expirar, ya que se compraba (por dinero real) tipo netflix, como una subscripción. ¿Y qué controla el tiempo que te queda de premium? El reloj del servidor.

Por lo tanto, antes de implementar el inventario y el premium toca implementar el reloj del servidor, tanto el ping a jugadores por TCP como por UDP (yeeep) y ya de paso la comunicación periódica server-server a base de paquetes internos.

Algunas fotos

ping_udp

El 999 indica que la rutina del cliente de mandar paquetes UDP de ping cada segundo está funcionando. ¿Por qué 999? Es una consecuencia de estar jugando en "training" en mi propia máquina con el servidor en local. Cuando implementemos el tunneling hablaré más de esto.

user_list
Como el día de hoy ha sido bastante árido en términos de cosas que brillan en el juego, he dejado ya preparado el paquete de la lista de usuarios. En su momento era un paquete particular que muchos servidores privados no tenían porque los adolescentes no son muy dados a compartir.

Siguientes pasos

Mi objetivo principal es dejar el paquete de login que he enseñado al principio completamente funcional. Esto quiere decir que la base de datos y el servidor deben poder cargar, gestionar y actualizar todos los datos que aparecen en él (inventario, dinero, nivel, tiempo de premium etc.) excepto el sistema de clanes, que en CP1 es una chorrada de 4 campos de los cuáles sirven 2 y que está ligado a un sistema web aparte que podemos dejar por ahora.

1 1 respuesta
Jastro

#14 vaya pedazo de curro abismal, entiendo las complicaciones de trabajar con un cliente que ya funciona. Menudo caos

El usuario que tienes ahí es algo que has puesto tu? O son datos del juego como tal. Me fliparia saber que le metí tan duro para tener ese nivel jajaja

Increíble trabajo técnico, la verdad que estoy flipando con los avances y lo que estás haciendo

1 respuesta
c0b4c

juegardo de la infancia
todavía me acuerdo de la musiquita

1 1 respuesta
DarkRaptor

#15

#15Jastro:

El usuario que tienes ahí es algo que has puesto tu? O son datos del juego como tal. Me fliparia saber que le metí tan duro para tener ese nivel jajaja

Las bases de datos tanto del servidor de autentificación como el de juego las estoy haciendo desde 0 así que puedo poner lo que quiera jajaja. El paquete de user list (el de la derecha con los niveles) es una tontería de paquete y bastante independiente del juego en si. Por eso no me importaba hacer un poco el garrulo y dejarlo en estático.

#16
Me alegro de que lo recuerdes con cariño. Fíjate si le tengo cariño yo que alguna vez me he planteado escribir un server de la open beta (2005) que tengo hasta el cliente.

En otro orden de cosas, hoy he avanzado bastante y he dejado funcionando una serie de sistemas bastante necesarios, aunque están incompletos:

a) Cargar la loadout desde la base de datos y procesarla para incluirla en el paquete que comenté en #10
b) Cargar el inventario con las fechas correctas para que las armas puedan "expirar" cuando el periodo de alquiler de las mismas termina.

El inventario y el sistema de armas en general de WarRock merece una mención especial por ser una de las mayores soplapolleces que he visto en un videojuego en cuanto a su implementación. Como va a ser un post algo largo y hoy voy escaso de tiempo, os dejo con 2 imágenes:

La puta basura de paquete de inventario. A ver si adivináis la estructura lahmentabla. DC04 es la Famas y DC05 es la L85A1. Si sabéis algo de cómo se codifica el tiempo en programación (seguro que si) lo sacáis fácil.

DC04-1-0-24090315-0,DC05-1-0-24090315-0,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^,^

Más novedades próximamente.

1 respuesta
Ridote

Python, pero serás hijo de fruta. Lo pongo en favoritos, mucho ánimo!

Jastro

#17 Igual me mando un gol, soy muy novato en todo esa parte, pero entiendo que las comas son los huecos de inventario, el 1 la cantidad de armas (Famas o la L85A1) el 0 no tengo ni idea y el numero largo, me da la sensacion de fecha, rollo el dia que la consiguio o algo jajaja

1 respuesta
DarkRaptor

#19
Algo así :)

El juego en capítulo 1 soporta 31 items en inventario como máximo por persona. Los huecos vacíos se marcan con ^ Cuando SI hay item, el primer bloque separado por "-" es el código del arma según items.bin (próximamente cómo desempaquetarlo xd), el 1 es un tipado interno que en capítulo 1 es:

Si el item empieza con D (todas las armas son D*), se marca 1. En caso contrario (items de los PXs por ej, que son C), es un 2. El 0 es un ¿?, en cap1 no se qué hace. El siguiente numeraco es fácil, es la fecha de expirar del item en formato:

yyMMddHH

Y el último bloque, "0", es el "amount" que en CP1 no tiene mucho sentido porque casi no hay consumibles que recuerde.

Más adelante el juego pasó a mandar cosas así

CC02-3-0-13070522-0-0-0-0-0-9999-9999

Y si yo extiendo los 0s a la misma longitud que el bloque que ves arriba, el cliente no cruje por lo que la estructura internamente ya la tiene. Es simplemente contenido no implementado de CP2, CP3 en adelante.

EDIT: Aprovecho para enseñaros el todo poderoso loadout del juego.

Hay que mandar 5 cadenas tal que así:

DA02,DB01,DF01,DR01,^,^,^,^

Una por cada clase porque WarRock tiene 5 clases. Cada código de 4 dígitos es un arma equipada al momento de entrar en el servidor. También hay que mandarle 4 [F,T] en función de si los slots 5-8 están activos o no. Los 1-4 siempre lo están por defecto.

1 respuesta
Jastro

#20 coño es verdad, lo habia olvidado tenias unas opcion de alquilar el arma o algo asi, jajajajaj

vaya locuron, no te voy a negar que estoy deseando que montes algo jugable y darle caña

DarkRaptor

Como ya adelanté, después de un

He decidido que el próximo sistema que vamos a dejar implementado entero es la tienda de armas, que nos meterá en una madriguera de conejo larga pero divertida.

Motivos

  1. Implementar los chats implica en cierto modo implementar las salas, ya que había varios tipos del mismo: lobby / room / clan / channel / whisper así de memoria.
  1. Implementar las salas nos llevará a implementar los sistemas de la partida propiamente dichos y para eso necesitamos al menos el inventario y las armas funcionando.
  1. Podríamos implementar los canales, realmente el paquete no devuelve mucho PERO internamente aprovechamos la señal del InPacket para mandar la lista de salas de ese canal y la lista de usuarios en ese canal (ahora mismo está "a mano"). Además el canal es un buen objeto para encargarse de enrutar las peticiones de chat a través de una referencia de usuarios en dicho canal.

¿Qué nos queda? La tienda. La tienda puede parece accesoria, pero pensad que:

  1. Ya tenemos completo el paquete de bienvenida y eso implicaba tener un sistema de equipamiento e inventario básico capaz de convertir la info de la DB en las cadenas de texto que espera el paquete.
  1. Nos llevará a desempaquetar del cliente la base de datos (un fichero) de armas, objetos y recursos y llevarlos ya al lado del servidor. Esto nos permitirá sanear el input que nos manda el usuario acerca de armas e items durante el resto del juego.
  1. Interacciona con otros sistemas ya listos y podemos ver si lo hemos hecho bien: el premium y su fecha de expiración, el reloj del servidor, el propio sistema de inventario.

Plan de acción

Fijaos en este esquema lamentable hecho en Impress

Estos ficheros gobiernan prácticamente todas las definiciones de objetos del juego. ¿Cómo lo se? Porque como ya comenté, cuando hace un lustro monté el primer server, estaba muy metido en el mundillo del modding (COD4 sobre todo) y aunque no sabía mucho de programar, si sabía de lo otro. Gracias al servidor privado aproveché para entender cómo funcionaba el cliente de pé a pá e incluso metimos mapas y más armas (todo sin usar en el cliente) mi equipo y yo. Por lo tanto, se bastante bien con quién me juego los cuartos.

El primer "problema"

Como estoy de reforma y no tengo mis antiguas herramientas de cliente disponibles en el portátil, tendré que hacerlas de 0. Que TBQH no me viene mal. Esto implica que hay que desarrollar algo para desempaquetar los ficheros binarios.

Por ejemplo si yo abro items.bin a pelo

EBF6FAFADD8C9E83929AF7849283839E99908ADDDEEB819285849E9899E9DDDEDE919E9B9288819285849E9899F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7EADEE2DDDEEBF8819285849E9899E9DDDEEB8592849882859492E9DDDEDE98BEBBF7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7F7EADE96DDDEDE80B8B8B3

Salvo que F7 representa espacios en blanco, pues muy legible no parece. Hay que desempaquetarlo.

Paso 1: XOR

Por suerte no tengo que romper la clave XOR que usan estos ficheros para cifrar su contenido, ya que recuerdo que era 215 xd Pero sabed que es fácil sacarla por fuerza bruta ya que solo hay 255 valores posibles.

Me construyo un diccionario tal que así:

    def initialize_hex_table(self):
        for i in range(256):
            hex_key = f"{i:02X}"
            self.ht_decrypt_buffer[hex_key] = i ^ 215
  1. Itero sobre todo los posibles valores de un byte que van de 0 a 255
  2. Convierto el valor del byte en su representación hexadecimal de dos dígitos. Por ejemplo: 15 pasa a ser 0F si no recuerdo mal.
  3. Aplico la clave XOR al byte y lo guardo en el diccionario, donde cada llave es el valor hexadecimal y cada valor, el resultado del XOR

Paso 2: descifrar

Creo otra función que use el diccionario ya generado para procesar el contenido del fichero y devolver los valores correctos.

def decrypt_content(self, hex_string):
        byte_array = bytearray(self.ht_decrypt_buffer[hex_string[i:i+2]] for i in range(0, len(hex_string), 2))
        try:
            return byte_array.decode('utf-8')
        except UnicodeDecodeError:
            print("ERROR: Decryption output cannot be decoded with UTF-8.")
            return None
  1. Tomando como argumento una cadena hexadecimal completa, la procesamos en bloques de dos caracteres hexadecimales. Cada carácter va a ser un byte en hexadecimal.
  2. Por cada pareja, miramos a qué valor corresponde en nuestro diccionario.
  3. El resultado lo almacenamos en un array de bytes Es decir, al terminar tenemos un array de bytes con todos los valores en su equivalente XOR (o descifrado si lo preferís).

Finalmente, pasamos el array de bytes a su equivalente UTF8, es decir, que transforme estos valores en función de la tabla de equivalencia bytes - caracter de UTF8. La cadena que he puesto al principio pasa a ser:

<!--
[ITEM SETTING]
        <VERSION>
                FILE_VERSION                =   5
        </VERSION>
        <RESOURCE>
                Oil                             =       A
                Wood

Paso 3: consumir ficheros completos

Con todo esto, solo tengo que hacerme un pequeño script que consuma ficheros enteros y los decodifique:

Script cutre

El script no es ninguna maravilla y lo tendré que retocar un poco para dejarlo como una aplicación de línea de comandos, pero lo importante es que ya puedo abrir los ficheros binarios y pensar si pasarlos todos a la base de datos (escogiendo lo que necesito) u otras ideas que tengo jeje.

Parte 2 coming soon

2
raul_ct

Tremendo curro

Jastro

Probablemente uno de los hilos mas interesantes que he visto pro gamedev en tiempo y desconocia este mundillo, me esta flipando como vas avanzando

1
DarkRaptor

Llevo todo el día con la parte n2 de implementar el inventario, la tienda y los sistemas de items. Tengo que currarme un post largo y estoy cansado, así que os dejo con la imagen de la victoria:

ESTA PUTA MIERDA requiere una base de datos, aunque no lo parezca :laughing: Cuando recupere el aliento me explayo.

1
DarkRaptor

Bueno ahora que tengo algo más de tiempo y estoy menos hasta los coj**nes de parsear cadenas, explico la parte dos.

Lo primero, recordad que WarRock era casi todo client-side, pero sin embargo, justamente la acción de cambiar de arma va por paquete. Es decir, que no puedes cambiar de arma del inventario (aunque las haya inyectado) sin implementar también el paquete correspondiente. Y ya que hacemos las cosas, las hacemos bien ¿no?

En #22 ya comenté cómo descifrar los ficheros binarios que controlan muchos aspectos del sistema de armamento, vehículos, items y extras. Lo que han hecho otros desarrolladores es proceder a dumpear todo a una base de datos. En este caso, no quería hacerlo así por 2 motivos:

  1. Mi experiencia previa con el cliente y estos servers es que el grueso de estos datos acaba cargándose 1 vez al principio del todo en el servidor y no se modifican. ¿Por qué? Porque tiene poco sentido hacerlo sin cambiar además los binarios del cliente y reconstruir el FCL. Para propagar esos cambios tienes que parar el server, crear un update y reiniciar. Más que tablas SQL con sus consultas, acaban siendo una enorme pila de datos que solo se leen y ocasionalmente alguien edita a mano.
  1. El grueso de esta información es contenido "sin usar" en capítulo 1. Parte es rescatable y funcional (again, modificando cliente tb), parte es inútil. No hace falta añadir aún más carga al SQL con datos que solo se leen 1 vez y que encima muchos son innecesarios.

Paso 1: minar los datos de los binarios descifrados

Como para construir una tabla en base de datos siempre hay tiempo, sobre todo si partimos de ficheros estandar como JSON, CSV etc. me propuse lo siguiente: parsear lo que a mi criterio necesito de esos binarios y meterlos en un JSON del siglo 21 o incluso en un CSV. Adelanto ya que esto se lo he dejado a chatGPT. Sabiendo la estructura de los ficheros, es bastante fácil que haga algo potable y yo me ahorro trabajo.

Como podéis ver, un único fichero (items.bin) de 14k líneas tiene todas las armas, los extras, los "recursos", los accesorios para el personaje (que el CP1 ni siquiera tenía) y ya de paso los vehículos (EQUIPMENT) y su armamenteo (E_WEAPONS) por separado. ¿Por qué los vehículos se llaman Equipment? Pues ni puta idea.

De antemano, cosas que no necesito:

  1. Los resources no tienen uso en los pservers. Que yo sepa.
  2. Todo lo que hay en "character" es inútil porque en WarRock CP1 no había costumes ni nada de las puta mierda gachas que metieron luego.
  3. Equipment y E_Weapons es el sistema de vehículos y hasta que no implementemos UO/BG no son necesarios.

Sabiendo esto, divido toda esa info en 3 tablas CSV:

  1. items.csv -> Conservamos la información que nos dicta si el cliente considera el item "en activo", así como su código de ITEM (4 letras) y su tipo (resource/character/weapon/loquesea)
code,english,active,source
AA01,OIL,FALSE,RESOURCE
AB01,OAK,FALSE,RESOURCE
AC01,GRANITIC,FALSE,RESOURCE
AD01,IRON_ORE,FALSE,RESOURCE
AE01,COTTON,FALSE,RESOURCE
BA01,SUIT,FALSE,CHARACTER
BB02,SHIRT,FALSE,CHARACTER
BC01,PANTS,FALSE,CHARACTER
BD02,GLASSES,FALSE,CHARACTER

TODO ITEM está compuesto por un código de 4 letras que desgrano más abajo.

  1. item_shop.csv Como su propio nombre indica, he extraido la info. que se que se usa en la tienda y tiene sentido tenerla en el servidor. Algunos aspectos son visuales (como la etiqueta de NEW!!!!) y no tiene sentido traerlos.
code,is_buyable,buy_type,buy_option,cost,required_bp,required_premium
AA01,FALSE,9,0,0,0,1
AB01,FALSE,9,0,0,0,1
AC01,FALSE,9,0,0,0,1
AD01,FALSE,9,0,0,0,1
AE01,FALSE,9,0,0,0,1
BA01,FALSE,9,0,0,0,1
BB02,FALSE,9,0,0,0,1
BC01,FALSE,9,0,0,0,1
BD02,FALSE,9,0,0,0,1

¿Qué son los BP? Battle points creo, no se usan pero no lo tengo 100% claro y me cuesta -1 sacarlo... El required_premium estoy al 75% seguro de haberlo parseado bien, pero me enteraré mejor cuando pueda acceder a mi PC ppal y a mis herramientas de cliente + notas.

Y por último, de las armas de fuego (no vehículos por ahora), sacamos los valores de daño, distancia y demás.

code,power,personal,surface,air,ship
DA01,400,"100,80,60","0,0,0","0,0,0","0,0,0"
DA02,200,"100,80,60","0,0,0","0,0,0","0,0,0"
DA03,400,"100,80,60","0,0,0","0,0,0","0,0,0"
DA04,400,"100,80,60","0,0,0","0,0,0","0,0,0"
DB01,320,"100,80,60","0,0,0","0,0,0","0,0,0"
DB02,520,"100,80,60","0,0,0","0,0,0","0,0,0"
DB03,290,"100,80,60","0,0,0","0,0,0","0,0,0"
DB04,520,"100,80,60","0,0,0","0,0,0","0,0,0"
DB05,280,"100,80,60","0,0,0","0,0,0","0,0,0"

La chasca como parábola, magazines etc. no tiene sentido sacarla porque el servidor no puede mandarle ningún paquete al cliente que sobrescriba esa info, sadly.

Paso 2: reconstruir el sistema de slots en el servidor

El grueso de los servidores privados que he visto tiene unas tablas server side enormes de 1s y 0s por cada arma, slot y clase para saber si puede o no usarse en este slot. En mi caso, voy a hacer algo más sencillo, pero tan o más funcional que lo que se suele hacer. Y sin base de datos.

El sistema de letras de WarRock

Bien, pues WarRock funciona con este complejísimo sistema para asignar a los items un código de 4 caracteres:

  • La primera letra es una de [A-B-C-D-E-F] donde A es item de tipo recurso, B es de tipo character, C es de tipo EXTRA, D es un arma, E es un vehículo y F es un arma de vehículo/asiento de vehículo.

  • La segunda letra la sacamos del mismo items.bin Al principio, hay una sección llamada item settings que la gente suele ignorar, pero contiene LAS FUCKING DEFINICIONES del item type o qué tipo de item representa cada código. Por ejemplo, para las armas:

Códigos de letra largos

Por ejemplo un arma DC es un arma de tipo Rifle tal y como veis en el código de arriba. Los siguientes 2 caracteres son una numeración que va hasta el 99. DC01-DC02-DC03 etc.

Paso 3: armas permitidas por slot y clase

Toda esta chapa viene por mi rechazo a tener una tabla kilométrica de 1s y os que nos diga si un item se puede equipar en un slot o no. Si abrimos el segundo fichero .bin que señalé en el esquema de #25, branches.bin, veremos algo así:

branches.bin engineer

Los atributos rollo DnD no se usan, así que los podéis ignorar. Pero la segunda parte nos dice exactamente qué tipo de arma (DC, DG DE etc.) puede ir en cada slot de cada clase y además nos dice el arma por defecto de ese slot. Son 8 slots, del 0 al 7.

Paso 4: Llevármelo todo al servidor

Ahora que ya tengo toda la info delante, es "fácil": puedo convertir todas las relaciones clase-slot-letra en un diccionario constante tal que así:

Ojo la rueda del ratón

Y mis tablas .CSV cargarlas en el servidor como una especie de singleton al igual que una configuración cualquiera. Consigo lo mismo pero con menor complejidad (no tengo que escribir más queries) y además soy muy fan de pandas para leer tablas así que :shrug:

Paso 5: Implementar el cambio de arma

Como ya os adelanté, mi forma de hacer las cosas es quizás menos emocionante (no vamos a ver tiros pronto) pero me permite ir haciendo bola de nieve conforme las dependencias de funcionalidad se van resolviendo. Como ya completé el primer paquete incluyendo el inventario, extenderlo para cambiar de arma no era difícil.

Así que cuando el cliente me manda esto:

        # 31184385 29970 0 3 D 2 DC02 2

yo primero lo saneo así

handler

Y mando el paquete correspondiente, luego de reconstruir el inventario.

Próximos pasos

Tenemos ya un sistema de inventario y un sistema para controlar y sanear el equipamiento. Tenemos el reloj y el premium funcionando. Tenemos los datos del cliente que nos dicen cuánto costaban las armas y sus requisitos.... Pues obviamente vamos a por la tienda.

Perdón por el tocho xd

carra

Uf nunca he jugado al WarRock ni similares, pero vaya curro que te estás pegando! Lo has cogido con ganas jeje

1 respuesta
DarkRaptor

#27
Se tarda casi más en explicarlo que en hacerlo. Es una parte del emulador bastante árida salvo que te guste entender cómo funciona el cliente internamente, por lo que hay pocas imágenes y más texto. Lo bueno es que hoy debería poder rematar la tienda entera sin mucho problema y eso motiva bastante porque es muy visual y cualquiera que haya jugado a este juego ha pasado por la tienda miles de veces.

Cuando me ponga con la creación de salas y paquetes de juego si se va a poner la cosa bonita. Se juntan aspectos técnicos como rematar el UDP (o los jugadores no se ven) o mandar paquetes entre lobby/sala/jugadores con otros más visuales como reprogramar los modos de juego o rehacer el cálculo del daño de cada arma. La mayoría de los paquetes "de juego" realmente son el mismo paquete id (30000) donde el cambio de un byte (posición 4) determina qué significan los bytes del fondo y ni yo me acuerdo bien de todos jeje

1
kidandcat

Ala, me acabo de enterar de esto. Al final tendré que hacer lo que me dijo @Ridote de que el bot de telegram avise de nuevos threads.
Guapísimo todo el curro que llevas ya. Primera vez que escucho sobre ese juego, pero se un poco como va la cosa (yo era más de Tales of Pirates :D)
Seguiré con atención tu progreso :)

PD: python caca

1
DarkRaptor

Detecto mucha fobia a las serpientes por aquí :new_moon_with_face:

Recordad que esto está pensado para ser una especie de documentación del funcionamiento original. Si en algún momento python se convirtiera en un problema de rendimiento (xddd) yo mismo lo reprogramaría en C# (.NET core) por ejemplo o incluso picaría en C los cuellos de botella. Ahora mismo, es bastante cómodo poder resolver problemas al vuelo editando el source code sin tener que recompilar, sobre todo en servidores a los que estoy conectado por SSH y van con la RAM justa para no cagarse encima.

Y ya que estoy aprovecho para citar a @Zerokkk que no se si sigue por aquí, pero igual tengo dudas cuando llegue a BG y me baila el funcionamiento de algunos detalles ingame. Con +2k horas que le echó según el difunto Xfire, seguro que él se acuerda más que yo

1 respuesta