🇪🇸 Español | 🇺🇸 English | 🇧🇷 Português
Port a C++ del cliente Mu Online 0.97k (main.exe, MD5
eb95ac0785e40a7ad60c9ddb5d8bef34), reconstruido por ingeniería inversa a
partir del binario original.
El cliente compilado se comporta igual que el binario original: mismo flujo de login, mismo render, mismos paquetes. No es una reescritura ni un "cliente inspirado en": cada función es un port de su contraparte en el binario, y las desviaciones deliberadas están documentadas en un comentario junto al código.
Sobre esa base se integraron las funciones del Main.dll de inyección (Fase 2)
y se agregó soporte para Linux (Fase 3).
La reconstrucción está completa. No quedan funciones del binario sin portar.
Puede quedar algún bug latente, que aparece al ejercitar un camino poco usado y
se corrige comparando contra IDA, pero la base de la ingeniería inversa está
terminada (tag 0.97.00).
El cliente arranca, conecta, hace login, elige personaje, entra al mundo y se juega: terreno, personajes, NPCs y monstruos, inventario, equipo, tienda, baúl, trade, chat, party, guild, combate, skills, efectos, eventos, sonido y música.
| Plataforma | Backend | Estado |
|---|---|---|
| Windows x86 | Win32 (el del binario original) | Completo |
| Windows x86 | SDL3 | Completo |
| Linux x86 | SDL3 | Completo; con NVIDIA conviene X11 (ver Wayland y GPUs NVIDIA) |
- Los assets ya están incluidos en
bin/Client/Data/(~209 MB: modelos.bmd, texturas.ozj/.ozt, mapas, sonidos y música). Clonás, compilás y arranca. - Un servidor. El port está validado contra MuEmu - Linux (season 0.97k), que es la fuente autoritativa para el formato de los paquetes. También se puede usar la versión de Windows, MuEmu - Kayito. Cada Release del cliente indica con qué versión del server funciona.
Hay dos backends de plataforma, que se eligen con MU_PLATFORM_BACKEND:
Win32(el default): ventana, input, sonido y red con la API de Windows, como el binario original. Sólo Windows.SDL3: lo mismo sobre SDL3, FreeType y miniaudio. Windows y Linux.
Siempre es x86 (32 bits). El port asume punteros de 32 bits: las direcciones del binario original, los layouts de struct y los pools de memoria. CMake corta la configuración si se intenta compilar en 64 bits.
Requiere Visual Studio 2022 con el toolset de C++ para escritorio (trae CMake). Hay dos formas equivalentes, que generan el mismo exe:
- Visual Studio: abrir
mu97k.sln, elegir Debug o Release con plataforma Win32, y compilar. - CMake:
El primer comando también genera
cmake -S . -B build -A Win32 -T v143 cmake --build build --config Releasebuild/mu97k.sln, por si querés seguir en Visual Studio con las carpetas desrc/como filtros.
Salida: bin/Client/main.exe (más main.map para resolver stacks de crash).
El proyecto enlaza directo sobre bin/Client/, que es donde viven los assets y
Config.ini, así que no hay copia intermedia ni riesgo de terminar ejecutando
un binario viejo.
Mismos requisitos que el anterior, más conexión a internet la primera vez que se
configura: SDL3, FreeType y miniaudio se descargan con FetchContent en
versiones fijas.
cmake -S . -B build-sdl3 -A Win32 -T v143 -DMU_PLATFORM_BACKEND=SDL3
cmake --build build-sdl3 --config Release
La salida también es bin/Client/main.exe, así que pisa la del backend Win32.
Hace falta Git, CMake 3.25+, Ninja, Python 3, pkg-config y GCC/G++ con soporte
-m32, más las bibliotecas de desarrollo de 32 bits (también en un Linux
x64):
- Arch (con
[multilib]habilitado):base-devel gcc cmake ninja git python pkgconf glu wayland-protocols, más las bibliotecaslib32-*de Correr en Linux, que ya incluyen los archivos de desarrollo. - Debian/Ubuntu:
g++-multilib libc6-dev-i386 cmake ninja-build git python3 pkg-config wayland-protocols libx11-dev:i386 libxext-dev:i386 libxrandr-dev:i386 libxinerama-dev:i386 libxcursor-dev:i386 libxi-dev:i386 libgl1-mesa-dev:i386 libglu1-mesa-dev:i386 libwayland-dev:i386 libxkbcommon-dev:i386 libdecor-0-dev:i386 libasound2-dev:i386 libpulse-dev:i386. Ubuntu 22.04 trae CMake 3.22: instalá CMake 3.25+ antes de configurar (por ejemplo, en un entorno virtual de Python). - Fedora:
gcc gcc-c++ cmake ninja-build git python3 pkgconf-pkg-config wayland-devel wayland-protocols-devel glibc-devel.i686 libstdc++-devel.i686 mesa-libGLU-devel.i686 libglvnd-devel.i686 libX11-devel.i686 libXext-devel.i686 libXrandr-devel.i686 libXinerama-devel.i686 libXcursor-devel.i686 libXi-devel.i686 wayland-devel.i686 libxkbcommon-devel.i686 libdecor-devel.i686 alsa-lib-devel.i686 pulseaudio-libs-devel.i686.
# PKG_CONFIG_LIBDIR tiene que apuntar a las bibliotecas de 32 bits.
export PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig:/usr/share/pkgconfig # Arch
# export PKG_CONFIG_LIBDIR=/usr/lib/i386-linux-gnu/pkgconfig:/usr/share/pkgconfig # Debian / Ubuntu
# export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/share/pkgconfig # Fedora
cmake -S . -B build/linux-release -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/linux-x86.cmake \
-DMU_PLATFORM_BACKEND=SDL3 -DMU_PLATFORM_ONLY=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build/linux-release --parallel 4El ejecutable queda en build/linux-release/bin/main (no en bin/Client/, así
no se mezcla con el de Windows). Para correrlo contra los assets del repo,
apuntá MU_ASSET_ROOT a bin/Client (ver Correr en Linux).
- Mientras convivan el
.slny CMake, un.cppnuevo se agrega en el.vcxprojy en elCMakeLists.txt. El CI falla si las dos listas no coinciden; se puede chequear local conpython .github/scripts/check_cmake_sources.py. - Dependencias incluidas en el repo:
lib/stb(decodifica las texturas.ozj/.ozty guarda las capturas) ylib/miniaudio(música). Con SDL3 se suman SDL3 y FreeType, que se descargan al configurar. - El CI compila las tres combinaciones (Windows Win32, Windows SDL3 y Linux SDL3) en Debug y Release, y arma el paquete Linux del Release.
La dirección y la identidad del server van compiladas en el cliente, como en
MU 5.2: no hay archivo de configuración que distribuir. Están en
src/Config/ServerConfig.h y apuntan al server de referencia del proyecto; para
usar el tuyo, editá ese archivo y recompilá.
La dirección puede ser una IP o un nombre de host (el cliente lo resuelve por DNS, igual que el original). Conviene un nombre: así no queda una IP pública fija en el código y el server puede mudarse sin recompilar. Si el dominio está en Cloudflare, el registro tiene que ir sin proxy ("DNS only"): el proxy sólo deja pasar tráfico web y corta las conexiones del juego.
constexpr char ConnectServerIP[] = "mu.server-pups.space";
constexpr unsigned short ConnectServerPort = 44405; // 0 = sin ConnectServer
constexpr char GameServerIP[] = "mu.server-pups.space";
constexpr unsigned short GameServerPort = 55901;
constexpr char CustomerName[] = "MuLinux";
constexpr char ServerSerial[] = "TbYehR2hFUPBKgZj";
constexpr char ClientVersion[] = "0.97.11";Direcciones. Con ConnectServerPort distinto de 0 se usa el flujo
ConnectServer: el cliente pide la lista real (F4/02), el server contesta con
nombres y ocupación, y al elegir uno el F4/03 redirige al GameServer; si el
ConnectServer no responde, se conecta al GameServer. Con 0 se conecta directo
al GameServer y el select-server muestra una entrada fija.
El select-server aparece siempre, incluso apuntando directo al GameServer: es una pantalla del flujo original, no un indicio de que estés llegando al ConnectServer.
Identidad del server. Los tres valores tienen que coincidir con los del
GameServer (MuServer/GameServer/DATA/GameServerInfo - StartUp.dat). Si alguno
no coincide, el cliente conecta pero no entra, y sin ningún mensaje útil:
| Valor | De dónde sale | Qué pasa si no coincide |
|---|---|---|
CustomerName |
CustomerName= del .dat |
El cliente conecta, desencripta basura y se queda en "conectando al GameServer" para siempre |
ServerSerial |
ServerSerial= del .dat |
Igual que arriba, y además el login devuelve "versión incorrecta" |
ClientVersion |
ServerVersion= del .dat |
Login rechazado con "versión incorrecta" |
CustomerName y ServerSerial alimentan la clave de encriptación, que el
GameServer deriva de los dos combinados (GameServer/HackCheck.cpp::InitHackCheck).
ServerSerial cumple doble función: entra en esa derivación y además el server
lo compara byte a byte en el login. ClientVersion acepta 0.97.11 o 09711.
Para diagnosticar, debug.log registra al arrancar la dirección usada y la
clave derivada:
ServerConfig: ConnectServer=mu.server-pups.space:44405 GameServer=mu.server-pups.space:55901 version='09711'
MuEmu: InitKeys CustomerName='MuLinux' Serial='TbYehR2hFUPBKgZj' -> EncDecKey1=0xC2 EncDecKey2=0x01 (xor=0xC2 add=0xC2)
Si el cliente se queda colgado conectando, esa línea es lo primero que hay que
mirar: comparala con el CustomerName del server.
Las preferencias se leen de Config.ini, con las mismas secciones y claves
que usaba el Main.dll de inyección, así sirve el Config.ini que ya tengas.
En Windows está en bin/Client/Config.ini; en Linux, en
~/.local/share/mu97k/Config.ini. La mayoría también se cambia desde el menú de
opciones del juego, que las guarda en el mismo archivo.
En Windows, si una clave no está en Config.ini manda el registro (donde el
0.97k original guardaba lo que configuraba el launcher oficial) y, si tampoco
está ahí, el default del binario. Lo que se aplicó queda en debug.log (línea
Config.ini: ...).
| Clave | Default | Notas |
|---|---|---|
[Window] WindowMode |
— (desviación) | 1 = en ventana, 0 = pantalla completa. El 0.97k sólo corre fullscreen y busca un modo de video de 16 bits que en Windows 10/11 no existe. |
[Window] Borderless |
— (desviación) | 1 = sin barra de título ni borde. Sólo aplica en modo ventana. |
[Window] Resolution |
0 (640x480) |
Índices del DLL: 0 640x480, 1 800x600, 2 1024x768, 3 1280x1024, 4 1280x720, 5 1366x768, 6 1600x900, 7 1920x1080. Ojo: el 4 no es el mismo del registro (ahí es 1600x1200). |
[Window] RenderDecoupled |
1 |
Desacopla el render; 0 vuelve al límite de 40 ms. La simulación sigue a 25 Hz. |
[Window] FpsLimit |
60 |
Límite de FPS con desacople activo; 0 = sin límite. Opciones → Pantalla permite 25/30/60/120/144/sin límite. |
[Window] VSync |
0 |
Sincronización vertical. Desacople, VSync y FPS se aplican en caliente y se guardan desde Opciones → Pantalla. VSync también limita «sin límite» al refresco de la pantalla. F12 muestra u oculta los FPS a la izquierda dentro del juego, incluso en pantalla completa; comienza oculto. |
[Sound] EnableSound |
1 |
Efectos de sonido. |
[Sound] EnableMusic |
0 (apagada) |
El Config.ini del repo la trae en 1. El tema del login es Data\Music\MuTheme.mp3, que el pack no trae (como en el original). |
[Sound] SoundLevel, MusicLevel |
— (desviación) | Volumen de 0 (mudo) a 9 (volumen original); como en el DLL, cada nivel son 6,25 dB. |
[User] Username |
— | Precarga el campo de usuario del login. |
[Language] LangSelection |
— (Text.bmd, que acá es la variante en español) |
Idioma de la interfaz: Eng, Spn o Por. Elige entre los Text_*.bmd y Dialog_*.bmd de Data/Local/. Los nombres de items, skills y quests están en inglés y no tienen variante. |
[MiniMap] Alpha, ZoomLevel |
10, 0 |
Transparencia (3 a 10) y zoom (0 a 6) del minimapa. |
[Antilag] Delete… |
0 |
Apagan partes del render para equipos lentos: DeleteShadows, DeleteObjects, DeleteFloor, DeleteSkills, DeleteStaticEffects, DeleteDynamicEffects, DeleteWings, DeleteHealthBar, DeleteInterface, DeleteWeather, DeleteGlow. |
[Font] FontName, FontHeight, FontBold, FontItalic, FontCharset, FontWidth, FontUnderline, FontQuality, FontStrikeOut |
Arial, alto según la resolución | Como el DLL: alto fijo (tope 25) y la fuente grande al doble. El Config.ini del repo trae Verdana 13. Si se borra la sección [Font] completa, el cliente vuelve a la fuente original. |
Ejecutar bin/Client/main.exe.
El CI arma un paquete del cliente Linux (artefacto mu97k-linux-release: el
.tar.gz, su SHA256 y el inventario de bibliotecas). Trae main, Data,
licencias, README y launch.sh; no lleva el Config.ini del desarrollador.
El ejecutable es ELF32 con SSE2, compilado sobre Ubuntu 22.04 (no usa símbolos de glibc posteriores a 2.35). SDL3, FreeType y miniaudio van enlazados estáticamente; OpenGL, glibc y las bibliotecas del escritorio son del sistema y tienen que estar en 32 bits, incluso en un Linux x64:
# Arch ([multilib] habilitado)
sudo pacman -S --needed lib32-glibc lib32-gcc-libs lib32-glu lib32-libglvnd \
lib32-libx11 lib32-libxext lib32-libxrandr lib32-libxcursor lib32-libxi \
lib32-wayland lib32-libxkbcommon lib32-libdecor lib32-alsa-lib lib32-libpulse
# Intel/AMD: lib32-mesa. NVIDIA: lib32-nvidia-utils de la misma versión que el driver.
# Debian 12 / Ubuntu 22.04+
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install libc6:i386 libstdc++6:i386 libgcc-s1:i386 \
libglu1-mesa:i386 libgl1:i386 libegl1:i386 libgl1-mesa-dri:i386 \
libx11-6:i386 libxext6:i386 libxrandr2:i386 libxcursor1:i386 libxi6:i386 \
libwayland-client0:i386 libwayland-cursor0:i386 libwayland-egl1:i386 \
libxkbcommon0:i386 libdecor-0-0:i386 libdecor-0-plugin-1-cairo:i386 libpulse0:i386
# ALSA: libasound2:i386 (Debian 12 / Ubuntu 22.04), libasound2t64:i386 (Ubuntu 24.04+).
# Fedora
sudo dnf install glibc.i686 libstdc++.i686 libgcc.i686 mesa-libGLU.i686 \
libglvnd-glx.i686 libglvnd-egl.i686 mesa-dri-drivers.i686 \
libX11.i686 libXext.i686 libXrandr.i686 libXcursor.i686 libXi.i686 \
wayland.i686 libxkbcommon.i686 libdecor.i686 alsa-lib.i686 pulseaudio-libs.i686Descomprimí el .tar.gz en una partición Linux (en NTFS se pierden los permisos
de ejecución) y ejecutá ./launch.sh desde una terminal, sin sudo. La
configuración, las capturas y el debug.log se guardan en
${XDG_DATA_HOME:-$HOME/.local/share}/mu97k; MU_CLIENT_ROOT permite elegir
otro directorio. El primer arranque crea una configuración en ventana a
1024×768 y nunca pisa preferencias existentes.
Para leer los assets desde otra carpeta sin copiarlos (por ejemplo, un disco
NTFS o el bin/Client del repo), apuntá MU_ASSET_ROOT a la carpeta que
contiene Data:
MU_ASSET_ROOT='/run/media/usuario/HDD/ruta/Client' ./launch.shSin SDL_VIDEODRIVER, una sesión Wayland prueba Wayland y después
X11/XWayland; el backend elegido y el renderer OpenGL quedan en stderr al
arrancar (Backend de video: … y OpenGL: vendor | renderer | versión). Para
forzar uno: SDL_VIDEODRIVER=x11 ./launch.sh o SDL_VIDEODRIVER=wayland ./launch.sh.
Otros detalles de Linux:
lib32-libdecorda las decoraciones de ventana del lado del cliente. Sin él, un compositor que no decora del lado del server puede mostrar la ventana sin marco.- En HiDPI se conserva la resolución lógica del juego y el compositor escala la imagen, que puede verse menos nítida.
- El IME usa la entrada de texto de SDL/Wayland, limitada al repertorio Windows-1252 del juego; no muestra el indicador IMM de Windows.
- La captura de pantalla es con Impr Pant. En KDE la toma Spectacle antes de que llegue al juego.
El cliente es de 32 bits, y con NVIDIA eso tiene dos consecuencias en Wayland nativo (encontradas en Arch, KDE Plasma, RTX 3080 Ti, driver 615):
- Sin la plataforma EGL Wayland de 32 bits, Wayland usa render por software.
NVIDIA sólo distribuye
egl-waylandpara x64. Sinlibnvidia-egl-wayland.so.1en/usr/lib32, el contexto GL de Wayland cae en Mesallvmpipe: 24 fps, efectos de skill en cuadrados blancos y negros transparentes. El cliente lo detecta al arrancar (Renderer por software en wayland) y pasa solo a X11/XWayland, donde GLX sí usa la GPU. - Con esa plataforma instalada, Wayland nativo funciona pero rinde mucho
menos. El driver EGL de NVIDIA en 32 bits resuelve variables thread-local con
__tls_get_addren cada llamada GL, y el cliente dibuja en modo inmediato (glVertex/glTexCoordpor vértice, como el 0.97k). Medido en el mismo punto del juego: 4,7 ms por frame con X11/GLX contra 70-85 ms con Wayland/EGL, o sea 25 fps contra 11-17.__GL_SYNC_TO_VBLANKy__GL_MaxFramesAllowedno lo cambian. No se corrige desde el cliente: se resuelve con el renderer moderno o el paso a x64 de la hoja de ruta.
En el segundo caso el cliente no cae solo a X11, porque el renderer es acelerado. Con NVIDIA, corré el cliente en X11:
SDL_VIDEODRIVER=x11 ./launch.shWayland nativo queda para GPUs con Mesa (Intel/AMD), cuyo EGL de 32 bits no tiene este problema.
egl-wayland de 32 bits no está en los repositorios oficiales de Arch. El
paquete lib32-egl-wayland de AUR suele estar atrasado y exige la misma versión
que egl-wayland; instalarlo con un helper puede bajar el de 64 bits. Si lo
querés para probar, compilá el PKGBUILD cambiando pkgver a la versión de tu
egl-wayland, y recompilalo cada vez que actualices el driver.
Con el Main.dll de inyección, un item custom se agregaba de los dos lados: el
server lo definía en su Item.txt, y el cliente necesitaba un item.bmd
regenerado más los .txt del Encoder (CustomItem.txt, CustomGlow.txt,
etc.) empaquetados en ClientInfo.bmd. Si cliente y server no coincidían, el
item se veía mal o el server lo rechazaba.
Ahora el server es la única fuente. Al loguearse, le manda al cliente un
catálogo con todas las definiciones (items, monstruos, efectos, pets). El
cliente sólo necesita los archivos del modelo (.bmd y texturas) dentro de
bin/Client/Data. No hay que regenerar item.bmd ni tocar código.
Los .txt del Encoder del DLL se siguen leyendo desde
Data/Custom/Encoder del server, así que una carpeta de customs armada para el
DLL funciona sin convertir nada.
Cada sección de items (espadas, hachas, …, joyas) tiene en el 0.97k 32 índices (0 a 31). Además, cada sección acepta índices 32 a 511: son los items agregados. En el protocolo viajan con 13 bits (el item usa 7 bytes en vez de 5), así que entran sin pisar ningún item vanilla.
Un agregado necesita decir qué item vanilla imita (columna Comportamiento): eso decide la lógica que el 0.97k tiene escrita por tipo de item (si es un arco y gasta flechas, si es un ala, si es una joya, qué opciones excellent puede tener). Todo lo demás —nombre, stats, tamaño, modelo, brillo, efectos— sale de su propia fila.
El item está dentro del rango clásico (0,20); el modelo y el brillo se
definen en el Encoder:
// Data/Custom/Encoder/CustomItem.txt
00,020 22 "Sword21" // Knight Blade
// Data/Custom/Encoder/CustomGlow.txt
00,020 191 165 127 // Knight Blade
Assets en el cliente: bin/Client/Data/Item/Custom/20/ (sword21.bmd y sus
texturas).
El mismo modelo como item agregado (0,32), todo en una fila de
Data/Item/Item.txt. Al final de las columnas de siempre se agregan:
comportamiento, carpeta y nombre del modelo, y el color del brillo (RGB):
32 0 22 1 4 1 1 0 "Crimson Knight Blade" ... 00,020 "Item\Custom\20\" "Sword21" 255 40 40
Se comporta como la Knight Blade (00,020), usa el modelo Sword21 y brilla en
rojo. Comando de prueba: /make 0 32.
Cinco piezas en el índice 21 de las secciones 7 a 11, con el modelo definido
en el CustomItem.txt del Encoder (como en el DLL):
07,021 0 "HelmMale22" // Great Dragon Helm
08,021 0 "ArmorMale22" // Great Dragon Armor
09,021 0 "PantMale22" // Great Dragon Pant
10,021 0 "GloveMale22" // Great Dragon Glove
11,021 0 "BootMale22" // Great Dragon Boot
Assets en el cliente: bin/Client/Data/Player/Custom/21/. Las mismas piezas
también se pueden definir sin el Encoder, con las columnas de modelo en
Item.txt ("Player\Custom\21\" "HelmMale22").
Data/Custom/Items/<sección>_<índice>.json agrega a un item (custom o vanilla)
su pose en el inventario y efectos que el cliente dibuja sobre los huesos del
modelo. Por ejemplo, unas alas con los destellos de las Wings of Illusion del
5.2:
{
"item": "12,032",
"effects": [
{ "on": "equipped", "type": "sprite", "texture": "Effect/Flare.jpg",
"bones": [5, 6, 7, 8, 18, 19], "color": [0.5, 0.0, 0.0], "scale": 0.6,
"pulse": { "speed": 0.002, "scale": 0.2, "color": 0.4 } },
{ "on": "equipped", "type": "particle", "particle": 1230,
"bones": [13, 31], "chance": 2, "color": [0.8, 0.8, 0.3], "scale": 0.5 }
]
}En el repo del server hay un caso real: Data/Custom/Items/03_000.json corrige
la posición del Light Spear (vanilla) en su casilla, con la corrección del 5.2.
El Rudolph del 5.2 como pet que da vueltas alrededor del jugador y levanta el
zen cercano. Va en un índice alto (13,400) a propósito, para que sirva de
ejemplo del rango extendido. Son cuatro archivos, todos incluidos en los repos:
| Dónde | Archivo | Qué define |
|---|---|---|
| cliente | bin/Client/Data/Item/Custom/Rudolph/ |
el modelo xmas_deer.bmd y sus texturas |
| server | Data/Item/Item.txt |
la fila del item: Slot 8 (helper), comportamiento * (no imita a ningún pet vanilla) y el modelo |
| server | Data/Custom/Items/13_400.json |
la pose en el inventario |
| server | Data/Custom/Pets/13_400.json |
cómo se mueve y qué hace |
{
"item": "13,400",
"blendMesh": 0,
"movement": { "type": "orbit", "radius": 50, "period": 4000, "height": 20 },
"abilities": [ { "type": "pickup", "what": "zen", "range": 3, "interval": 1000, "delay": 1500 } ]
}El cliente dibuja el movimiento; las habilidades (levantar el zen) las ejecuta
el server, que es el que decide. Comando de prueba: /make 13 400.
Como en el DLL, con Data/Custom/Encoder/CustomMonster.txt (índice, tipo
0=NPC 1=monstruo, dorado, escala, carpeta y modelo):
152 1 1 2.0 "Monster\\Karane\\" "Karane" // Karane
O directamente en Data/Monster/Monster.txt, con las mismas columnas al final
de la fila del monstruo:
152 0 "Karane" ... 0 0 1 1 2.0 "Monster\Karane\" "Karane"
Assets en el cliente: bin/Client/Data/Monster/Karane/.
Columnas opcionales al final de cada fila de Item.txt (* = sin valor):
| Columna | Ejemplo | Qué hace |
|---|---|---|
| Comportamiento | 00,020 |
vanilla que imita en la lógica |
| Carpeta y modelo | "Item\Custom\20\" "Sword21" |
modelo en el inventario, el suelo y el personaje |
| Brillo | 255 40 40 |
color del brillo por nivel |
| Carpeta y modelo puesto | "Item\Custom\FenrirMount\" "fenril_black" |
sólo si el item se ve distinto puesto (una montura) |
| Gate | 22 |
para pergaminos: lleva siempre a ese gate |
Los JSON de Data/Custom/Items también aceptan "tooltip": hasta 6 líneas
propias debajo del nombre del item, como texto ("Zen picker") o con color
({ "text": "Zen picker", "color": "gold" }; colores: white, blue, red,
gold, green, darkred, purple, darkblue, darkgold).
Para alas custom, Data/Item/CustomWing.txt agrega las constantes de defensa y
daño. Los JSON de Data/Custom/Items y Data/Custom/Pets se validan al
arrancar el server: un archivo con errores se descarta entero y el motivo queda
en GameServer/LOG.
mu97k-src/
├── mu97k.sln solución de VS2022 (backend Win32)
├── mu97k.vcxproj proyecto (Win32)
├── CMakeLists.txt build de las tres combinaciones (Win32, SDL3 Windows, SDL3 Linux)
├── cmake/ módulos de CMake y el toolchain de Linux x86
├── lib/stb/ decodifica las texturas .ozj/.ozt y guarda las capturas
├── lib/miniaudio/ reproduce la música
└── src/
├── WinMain.cpp punto de entrada + loop de mensajes
├── Platform/ interfaces de plataforma y sus backends (Win32/, SDL3/, Posix/)
├── globals.{h,cpp} estado global; los DAT identificados conservan trazabilidad IDA
├── functions.h declaraciones compartidas y procedencia IDA de símbolos renombrados
├── structs.h layouts de struct verificados contra IDA
├── ghidra_compat.h macros que el decompile da por existentes (qmemcpy, LODWORD, ...)
│
├── Combat/ Config/ Core/ Entity/ Game/ GameGuard/ Input/ Item/
├── Local/ Math/ Model/ Monster/ Net/ Party/ Path/ Physics/
└── Render/ Scene/ Sound/ Terrain/ Trade/ UI/ Util/
Los módulos agrupan por responsabilidad. La dirección en el binario sigue siendo una pista importante para verificar una función o resolver un símbolo, pero no determina la ubicación del código portado.
Cada versión XX.XX.00 equivale a una versión de Webzen: 0.97.00 es el 0.97k,
0.99.00 será el 0.99 y así con cada Season. Los tags intermedios son el camino
hacia la versión siguiente.
El tag 0.97.00 cierra la reconstrucción fiel: el cliente se comporta como el
binario original. Sobre esa base, la línea 0.97 se trabaja por fases de
ingeniería; cada fase cerrada es una versión.
flowchart TD
F0["0.97.00 · Reconstrucción fiel (base)"] --> F1
F1["0.97.10 · Fase 1: base de ingeniería<br/>CMake, CI, limpieza de código y comentarios, espejo de protocolo"] --> F2
F2["0.97.20 · Fase 2: integración nativa del Main.dll + modularización<br/>customs integrados a la lógica base; switches y defines de Webzen → tablas y enums"] --> F3
F3["0.97.30 · Fase 3: abstracción de plataforma + Linux"] --> F4
F4["0.97.40 · Fase 4: timing real + renderer moderno<br/>simulación separada del render, fps altos sin acelerar el juego, OpenGL 3.3 y x64"] --> F5
F5["0.97.50 · Fase 5: contenido compartido cliente + server, autoridad del server y seguridad<br/>0.97.5x: launcher, updater y otras mejoras"] --> F6
F6["0.97.60 → 0.97.8x · contenido y funciones nuevas"] --> F9
F9["0.97.9x · 97+99: items y mapas del 0.99 sobre el 0.97"] --> V99
V99["0.99.00 · versión 0.99 completa, con el Dark Lord"]
Los números de cada fase son la versión prevista al cerrarla; el alcance de cada una puede ajustarse en el camino.
Las fases de ingeniería son propias de la línea 0.97. Desde 0.99.00, los tags
intermedios suman lo que trae cada versión de Webzen, siguiendo sus parches,
hasta completar la siguiente: la Season 1, después la Season 2 (que llega con la
Summoner), y así.
Las versiones son XX.XX.YZ, siempre de dos dígitos al final.
| Tag | Qué es |
|---|---|
0.97.00 |
base de la reconstrucción fiel (equivale al 0.97k) |
0.97.01, 0.97.02… |
correcciones sobre la base |
0.97.10 |
cierre de la Fase 1 |
0.97.11, 0.97.12… |
correcciones y mejoras sobre la Fase 1 |
0.97.20 … 0.97.50 |
cierre de las fases siguientes, y así |
0.97.9x |
97+99: contenido del 0.99 sobre el 0.97 |
0.99.00 |
versión 0.99 completa |
En la línea 0.97, Y es la fase y Z el arreglo o la mejora. En las líneas siguientes, los tags intermedios marcan el avance hacia la próxima versión de Webzen.
El cliente y el server usan la
misma numeración, pero el server sólo recibe un tag nuevo cuando cambia. Cada
Release del cliente indica con qué tag del server funciona (por ejemplo, el
cliente 0.97.32 funciona con el server 0.97.30).
gitGraph
commit id: "base" tag: "0.97.00"
branch fase/1
checkout fase/1
commit id: "PR tema A"
commit id: "PR tema B"
checkout main
branch hotfix
checkout hotfix
commit id: "fix"
checkout main
merge hotfix tag: "0.97.01"
checkout fase/1
merge main id: "trae el fix"
checkout main
merge fase/1 tag: "0.97.10"
branch fase/2
checkout fase/2
commit id: "PR tema C"
mainsólo recibe cierres de fase y correcciones; cada merge lleva su tag y su Release.- Cada fase se trabaja en
fase/N. Las ramas de tema (fix/…,feat/…,chore/…) salen defase/Ny vuelven por PR. - Una corrección sobre una versión publicada sale del tag afectado, se mergea a
maincon su nuevo tag y también a la fase en curso.
MIT — ver LICENSE.
La licencia cubre el código: todo lo que está bajo src/.
No cubre los assets de bin/Client/Data/, que son copyright de WebZen Inc.
Las bibliotecas de terceros mantienen sus propias licencias: stb y miniaudio
(dominio público / MIT-0), y SDL3 (zlib) y FreeType (FreeType License) en el
backend SDL3. El paquete Linux las incluye en licenses/.
Esta parte es para quien quiera verificar un port contra el binario o seguir la historia de la ingeniería inversa. No hace falta para compilar ni para jugar.
El main.exe original no está en el repo: sólo hace falta para decompilarlo y
comparar un port contra él. Sale de cualquier distribución del cliente 0.97k.
Verificá el MD5 (eb95ac0785e40a7ad60c9ddb5d8bef34) antes de comparar
direcciones: hay muchas variantes parcheadas dando vueltas y no coinciden.
El orden de autoridad para resolver cualquier duda:
- IDA / el binario original. Es la verdad. Si el decompile dice algo raro, probablemente el decompile tenga razón y nuestra intuición no.
- El servidor MuEmu, para todo lo que sea formato de paquetes.
- El DLL de inyección, como segunda referencia de comportamiento.
- El source de Mu Online 5.2, sólo como apoyo semántico y de nomenclatura cuando el contexto actual lo permite. No se copia implementación ni se incorpora comportamiento de 5.2: la UI, las definiciones y las features pueden diferir de 0.97k.
Lo que no está en ninguna de esas fuentes no se inventa. Si hace falta una desviación (porque un camino del original es inalcanzable, o depende de algo que todavía no está portado), se implementa y se documenta en un comentario ahí mismo, explicando qué hace el original y por qué nos apartamos.
Lo único que se saltea deliberadamente es el ruido anti-tamper: las operaciones de hash-table intercaladas, los bloques inalcanzables y el scrambling XOR de la versión protegida. No son lógica de juego.
El código está en español. Los símbolos que ya pudieron identificarse se nombran
por responsabilidad; su definición canónica conserva un comentario
// IDA: <nombre> (<dirección>) para mantener la trazabilidad con el binario.
Los FUN_* y DAT_* que todavía aparecen en el código no son, por sí solos,
deuda de renombrado. Algunos describen infraestructura, CRT, GameGuard, layouts
binarios, pools o compatibilidad; otros requieren investigación antes de poder
recibir un nombre semántico seguro. Tampoco son deuda los aliases o bridges ABI
que quedan en functions.h/globals.h: mantienen el contrato original de los
ports que los usan.
Los comentarios describen el código actual: la referencia a IDA, el layout
de un struct o paquete, las desviaciones vigentes respecto del binario y las
advertencias que evitan romper algo. La historia de cómo se llegó a un fix
(fechas, sondas, intentos descartados, lo que hacía el port viejo) va en el
mensaje del commit. Los comentarios de desarrollo que estaban en el código y los
decompilados de IDA que nunca se activaron (antes en src/stubs_IDA_ports.cpp)
quedaron archivados en docs/ del tag 0.97.21; el texto original completo
sigue en el tag 0.97.00.
Estas costaron sesiones enteras de depuración. Todas volvieron a aparecer más de una vez.
1. Símbolos duplicados. El mismo nombre definido dos veces: un stub viejo y
el port real. C++ puede aceptarlo como sobrecarga si las firmas difieren, y
entonces cada llamador resuelve a una copia distinta. Síntoma típico: un valor
se corrompe y no aparece ningún escritor que lo explique. Antes de auditar
cualquier función, confirmá que estás leyendo la copia que se compila —
no una dentro de #if 0. Corolario: una sonda de diagnóstico puesta en código
muerto da cero resultados, y ese silencio parece evidencia de que no hay bug.
2. Locales que Ghidra separó. El decompile emite como variables sueltas lo
que en el frame original era un bloque contiguo, y el código las recorre como
si lo fueran (&local_XX de un escalar pasado como vec3). El compilador no
garantiza ese layout. Síntoma: la primera componente sale bien y el resto es
basura (valores de ~1e9). Se arregla reconstruyendo el frame como un array
contiguo y mapeando los nombres por offset.
3. Campos enteros leídos como float. Ghidra tipa el slot como float* y
entonces todo acceso sale como float, incluidos los campos que son enteros o
punteros. (float)(uintptr_t)ptr convierte numéricamente lo que había que
reinterpretar por bits. Síntoma: no es un crash, es funcionalidad que
simplemente no ocurre — comparaciones que nunca dan verdadero, punteros en
cero, contadores clavados. Pista: valores de ~1e9 que leídos como bits dan
floats chicos y razonables. Dentro de un mismo archivo suelen convivir accesos
correctos y rotos; esa mezcla es la señal.
4. Padding de structs en los paquetes. El servidor manda structs de C con su padding de alineación. Leer los campos por offset "lógico" en vez del real devuelve basura convincente. Ya mordió en los stats, en la lista de guild y en los números de daño.
5. Las etiquetas de los offsets mienten. Varios campos de la struct de
entidad estuvieron mal etiquetados durante meses (+0x1BC no son flags de
movimiento: es la clase del personaje; +0x34E no es "muerto": es SafeZone).
Antes de confiar en el nombre de un offset, buscá quién lo escribe en el
binario.
6. Nombres de funciones parecidos con efectos opuestos. El caso recurrente
es la familia de estado de OpenGL: EnableAlphaTest (0x511680),
EnableAlphaBlend (0x511710, aditivo) y DisableTexture (0x511590, que apaga
el texturizado). Confundirlas pinta cuadrados blancos sobre medio frame,
porque el estado de GL queda pegado y contamina todo lo que se dibuje después.
7. Cotas y limpiezas inventadas. Guards que el binario no tiene y que en vez
de recortar descartan la entrada entera. Apareció de los dos lados: en los
handlers de red (count > 30 tiraba los 41 monstruos de un viewport; el síntoma
se leía como bug de spawn, con una cascada de key not found en el log) y en el
input tick (un else que al bloquear el debounce hacía
MouseLButtonPush = 0; MouseLButton = 0;, o sea perdía el click: había que
clickear varias veces para caminar). Regla: cualquier = 0 o clear que el port
agregue en el camino de «todavía no se puede» es sospechoso — el binario casi
siempre deja el estado pendiente para el próximo tick.
Están documentadas en el código, pero conviene saberlas si se apunta a otro servidor:
C3:1E(duration skill) se manda con 11 bytes, no 9. El 0.97k vanilla no incluyeindex[], peroCGDurationSkillAttackRecvde MuEmu lo lee siempre (SkillManager.cpp:2047): con 9 bytes el server toma esos dos bytes de fuera del paquete y el skill le pega a otra entidad. El DLL de inyección hace lo mismo (CPatchs::SendRequestMagicContinue).- Triple Shot manda el byte
angle. El server arma el cono conangle, no condir(SkillManager.cpp:1185). - F3/12 al entrar al mundo. Sin ese ACK el server deja
RegenOken 1 y rechaza todo/moveposterior además de no mandar las entidades del mapa.