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:
- 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.
- 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:
- Los resources no tienen uso en los pservers. Que yo sepa.
- 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.
- 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:
- 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.
- 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<WEAPON>
Dagger = A
Pistol = B
Rifle = C
Rifle2 = D
Rifle3 = E
SubmachineGun(SMG) = F
Sniper = G
MachineGun = H
Shotgun = I
AntitankWeapon = J
Ground_to_AirWeapon = K
AntitankMine = L
Grenade = M
Grenade_Combatant = N
ExtraAmmo = O
Bomb = P
MedicKit = Q
Spanner = R
AllClass_Paid = S
MachineGun2 = T
Engineer_Paid = U
Medic_Paid = V
AllClass = W
Scout_Paid = X
Comabatant_Paid = Y
HeavyWeapons_Paid = Z
</WEAPON>
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<!--
[ENGINEER]
<BASIC INFO>
BRANCH = ENGINEER
STR = 100
STR RATE = 1.0
CON = 90
CON RATE = 0.9
DEX = 95
DEX RATE = 0.8
STM = 100
STM RATE = 1.1
WIZ = 120
WIZ RATE = 1.2
</BASIC INFO>
<SLOT INFO>
0SLOT ITEM = DA02
0SLOT CODE = A
1SLOT ITEM = DB01
1SLOT CODE = B
2SLOT ITEM = DF01
2SLOT CODE = D,F
3SLOT ITEM = DR01
3SLOT CODE = R
4SLOT ITEM = 0
4SLOT CODE = D,I,M,O,F
5SLOT ITEM = 0
5SLOT CODE = S,U,P
6SLOT ITEM = 0
6SLOT CODE = 0
7SLOT ITEM = 0
7SLOT CODE = 0
</SLOT INFO>
<PC_CAFE PREMIUM ITEM>
PREMIUM ITEM = DF02
</PC_CAFE PREMIUM ITEM>
[/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ónBranchSlotCodes = {
Classes.ENGINEER: {
0: [WeaponTypes.DAGGER],
1: [WeaponTypes.PISTOL],
2: [WeaponTypes.RIFLE2, WeaponTypes.SMG],
3: [WeaponTypes.SPANNER],
4: [WeaponTypes.RIFLE2, WeaponTypes.SHOTGUN, WeaponTypes.GRENADE, WeaponTypes.EXTRA_AMMO, WeaponTypes.SMG],
5: [WeaponTypes.ALLCLASS_PAID, WeaponTypes.ENGINEER_PAID, WeaponTypes.BOMB],
6: [],
7: []
},
Classes.MEDIC: {
0: [WeaponTypes.DAGGER],
1: [WeaponTypes.PISTOL],
2: [WeaponTypes.RIFLE2, WeaponTypes.SMG],
3: [WeaponTypes.MEDIC_KIT],
4: [WeaponTypes.RIFLE2, WeaponTypes.SHOTGUN, WeaponTypes.GRENADE, WeaponTypes.EXTRA_AMMO, WeaponTypes.SMG],
5: [WeaponTypes.ALLCLASS_PAID, WeaponTypes.MEDIC_PAID],
6: [],
7: []
},
Classes.SNIPER: {
0: [WeaponTypes.DAGGER],
1: [WeaponTypes.PISTOL],
2: [WeaponTypes.SNIPER],
3: [WeaponTypes.GRENADE_COMBATANT],
4: [WeaponTypes.EXTRA_AMMO],
5: [WeaponTypes.ALLCLASS_PAID, WeaponTypes.SCOUT_PAID],
6: [],
7: []
},
Classes.ASSAULT: {
0: [WeaponTypes.DAGGER],
1: [WeaponTypes.PISTOL],
2: [WeaponTypes.RIFLE, WeaponTypes.RIFLE2, WeaponTypes.RIFLE3],
3: [WeaponTypes.GRENADE_COMBATANT],
4: [WeaponTypes.RIFLE, WeaponTypes.RIFLE2, WeaponTypes.RIFLE3, WeaponTypes.MACHINE_GUN, WeaponTypes.SHOTGUN, WeaponTypes.EXTRA_AMMO],
5: [WeaponTypes.ALLCLASS_PAID, WeaponTypes.COMBATANT_PAID],
6: [],
7: []
},
Classes.HEAVY: {
0: [WeaponTypes.DAGGER],
1: [WeaponTypes.PISTOL],
2: [WeaponTypes.ANTITANK_WEAPON, WeaponTypes.GROUND_TO_AIR_WEAPON],
3: [WeaponTypes.ANTITANK_MINE],
4: [WeaponTypes.ANTITANK_WEAPON, WeaponTypes.GROUND_TO_AIR_WEAPON, WeaponTypes.GRENADE, WeaponTypes.EXTRA_AMMO, WeaponTypes.MACHINE_GUN2],
5: [WeaponTypes.ALLCLASS_PAID, WeaponTypes.HEAVY_WEAPONS_PAID],
6: [],
7: []
}
}
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í
handlerclass EquipmentHandler(PacketHandler):
async def process(self, u: "User") -> None:
# 31184385 29970 0 3 D 2 DC02 2
# Determine if the item is being equipped or unequipped
is_item_equip = not bool(int(self.get_block(0)))
target_branch = int(self.get_block(1))
alt_target_slot = int(self.get_block(3))
weapon_code = self.get_block(4)
target_slot = int(self.get_block(5))
item_database = ItemDatabase()
if not u.authorized:
return
if target_slot >= MAX_WEAPONS_SLOTS:
return
if target_branch >= MAX_CLASSES:
await u.disconnect()
return
if len(weapon_code) != 4:
return
if not item_database.item_exists(code=weapon_code):
return # Item does not exist
# Let admins use inactive items
if not item_database.is_active(weapon_code) and u.rights <= 3:
await u.disconnect() # Potential cheater. Log?
if not u.inventory.has_item(weapon_code) and weapon_code not in DefaultWeapon.DEFAULTS:
await u.disconnect() # Potential cheater/scripter. Log?
# Check if the item is already equipped in the target branch
equipped_in_slot = u.inventory.equipment.is_equipped_in_class(
target_class=target_branch, weapon=weapon_code
)
if not is_item_equip: # Unequipping the item
if equipped_in_slot >= 0:
u.inventory.equipment.remove_item_from_slot(
target_class=target_branch,
target_slot=alt_target_slot
)
unequip_packet = PacketFactory.create_packet(
packet_id=PacketList.EQUIPMENT,
error_code=1,
target_class=target_branch,
new_loadout=u.inventory.equipment.loadout[target_branch]
)
await u.send(unequip_packet.build())
return
if equipped_in_slot >= 0:
# Item is already equipped
already_equipped_packet = PacketFactory.create_packet(
packet_id=PacketList.EQUIPMENT,
error_code=EquipmentError.ALREADY_EQUIPPED
)
await u.send(already_equipped_packet.build())
return
# Final check: Can the weapon be placed in the target slot?
valid_codes_for_slot = BranchSlotCodes[target_branch][target_slot]
weapon_subtype = weapon_code[1]
if weapon_subtype in valid_codes_for_slot:
# Perform the actual equipment
u.inventory.equipment.add_item_to_slot(
target_class=target_branch,
target_slot=target_slot,
item_code=weapon_code
)
equip_packet = PacketFactory.create_packet(
packet_id=PacketList.EQUIPMENT,
error_code=1,
target_class=target_branch,
new_loadout=u.inventory.equipment.loadout[target_branch]
)
await u.send(equip_packet.build())
else:
# Weapon cannot be placed in the target slot
unsuitable_packet = PacketFactory.create_packet(
packet_id=PacketList.EQUIPMENT,
error_code=EquipmentError.INVALID_BRANCH
)
await u.send(unsuitable_packet.build())
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