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)
}

Used Technologies