BMD Converter is a MuOnline client file converting tool.
MU Online is an Isometric medieval fantasy MMORPG, produced by Webzen, a Korean gaming company.
This is an Open Source (MIT) project, you may find source code in my GitHub repository.
This is my pet project, in my free time I fall for reverse engineering, code injection, and game modification using C++ and Assembler (education purposes only 👀). And for these purposes, I needed a client file converting tool.
I coded a universal convertor from the binary file into .csv file format (using reflection).
...
func (b BaseBmdInfo) Decrypt(data []byte, out *os.File) error {
dataType := reflect.TypeOf(b.adapter.GetStruct())
dataArray := reflect.MakeSlice(reflect.SliceOf(dataType), 0, 0)
if version, err := b.adapter.GetVersion(&data); version != -1 {
if err != nil {
return err
}
fmt.Println("bmd version:", uint32(version))
}
if count, err := b.adapter.GetRowsCount(&data); count != -1 {
if err != nil {
return err
}
fmt.Println("rows count:", uint32(count))
}
if crc, err := b.adapter.GetCrcValue(&data); crc != -1 {
if err != nil {
return err
}
fmt.Println("file crc:", uint32(crc))
}
reader := bytes.NewReader(data)
for reader.Len() > 0 {
data := b.adapter.GetStruct()
val := reflect.ValueOf(data).Elem()
totalLen := 0
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
valueType := val.Type().Field(i)
len, err := getFieldSize(val, i)
if err != nil {
return err
}
tmp := make([]byte, len)
err = binary.Read(reader, binary.LittleEndian, &tmp)
if err != nil {
return err
}
// Do slice XOR only when it was enabled
if tagValue := valueType.Tag.Get("xor"); tagValue != "false" {
doSliceXor(tmp, totalLen)
totalLen += len
}
buff := bytes.NewReader(tmp)
switch valueField.Interface().(type) {
// Int
case int:
var val int
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetInt(int64(val))
case int8:
var val int8
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetInt(int64(val))
case int16:
var val int16
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetInt(int64(val))
case int32:
var val int32
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetInt(int64(val))
case int64:
var val int64
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetInt(val)
// Uint
case uint:
var val uint
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetUint(uint64(val))
case uint8:
var val uint8
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetUint(uint64(val))
case uint16:
var val uint16
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetUint(uint64(val))
case uint32:
var val uint32
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetUint(uint64(val))
case uint64:
var val uint64
err = binary.Read(buff, binary.LittleEndian, &val)
valueField.SetUint(val)
// String
case string:
if strLen := clen(tmp); strLen > 0 {
aaa := string(tmp[:strLen])
valueField.SetString(aaa)
} else {
valueField.SetString(string('\x00'))
}
default:
return errors.New("unknown structure type")
}
if err != nil {
return err
}
}
ptr := reflect.New(dataType)
ptr.Elem().Set(reflect.ValueOf(data))
value := reflect.ValueOf(ptr.Interface()).Elem()
dataArray = reflect.Append(dataArray, value)
}
err := gocsv.MarshalFile(dataArray.Interface(), out)
if err != nil {
return err
}
fmt.Println("decrypt completed!")
return nil
}
....
Each game client file has its own structure, so to get this tool to work, I needed to define each file structure.
...
func (i *Item) GetStruct() interface{} {
type bmdStruct struct {
Name string `csv:"name" size:"30"`
TwoHands uint8 `csv:"two_hands"`
ItemLevel uint8 `csv:"item_level"`
ItemSlot uint8 `csv:"item_slot"`
ItemSkill uint8 `csv:"item_skill"`
X uint8 `csv:"x"`
Y uint8 `csv:"y"`
DamageMin uint8 `csv:"damage_min"`
DamageMax uint8 `csv:"damage_max"`
DefenceRate uint8 `csv:"defence_rate"`
Defence uint8 `csv:"defence"`
Unk1 uint8 `csv:"unk_1"`
AttackSpeed uint8 `csv:"attack_speed"`
WalkSpeed uint8 `csv:"walk_speed"`
Durability uint8 `csv:"durability"`
MagicDurability uint8 `csv:"magic_durability"`
MagicPower uint8 `csv:"magic_power"`
ReqStr uint16 `csv:"req_str"`
ReqDex uint16 `csv:"req_dex"`
ReqEne uint16 `csv:"req_ene"`
ReqVit uint16 `csv:"req_vit"`
ReqLea uint16 `csv:"req_lea"`
ReqLevel uint16 `csv:"req_level"`
Unk2 uint16 `csv:"unk_2"`
Value uint16 `csv:"value"`
Money uint16 `csv:"money"`
SetAttrib uint8 `csv:"set_attrib"`
ClassDW uint8 `csv:"class_dw"`
ClassDK uint8 `csv:"class_dk"`
ClassELF uint8 `csv:"class_elf"`
ClassMG uint8 `csv:"class_mg"`
ClassDL uint8 `csv:"class_dl"`
IceRes uint8 `csv:"ice_res"`
PoisonRes uint8 `csv:"poison_res"`
LightRes uint8 `csv:"light_res"`
FireRes uint8 `csv:"fire_res"`
EarthRes uint8 `csv:"earth_res"`
WindRes uint8 `csv:"wind_res"`
WaterRes uint8 `csv:"water_res"`
Unk3 uint8 `csv:"unk_3"`
Unk4 uint8 `csv:"unk_4"`
Unk5 uint8 `csv:"unk_5"`
}
return &bmdStruct{}
}
...
Also, this tool bypasses game client protections.
func SetCrcValue(file *os.File, buff bytes.Buffer, keyVal uint32) error {
reader := bytes.NewReader(buff.Bytes())
crcVal := keyVal * 512
varTemp := []uint32{
0xFFFFFFFF,
0x7FFFFFFF,
0x3FFFFFFF,
0x1FFFFFFF,
0x0FFFFFFF,
0x07FFFFFF,
0x03FFFFFF,
0x01FFFFFF,
0x00FFFFFF,
}
var pos uint32
for reader.Len() > 0 {
var val uint32
err := binary.Read(reader, binary.LittleEndian, &val)
if err != nil {
return err
}
if ((pos/4)+keyVal)%2 == 1 {
crcVal += val
} else {
crcVal ^= val
}
if pos%16 == 0 {
crcVal ^= (keyVal + crcVal) >> (((pos / 4) % 8) + 1) & varTemp[((pos/4)%8)+1]
}
pos += 4
}
fmt.Println("calculated crc =", crcVal)
tmp := make([]byte, 4)
binary.LittleEndian.PutUint32(tmp, crcVal)
return binary.Write(file, binary.LittleEndian, tmp)
}