Projects/DJade MAX Respect V

리듬게임 클론코딩 프로젝트 - 00

Jade Choe 2023. 12. 25. 22:19
SMALL

유니티로 리듬게임을 만들어보려고 한다.
시장에는 DJMAX와 EZ2ON이라는 아주 훌륭한 참고자료들이 있기 때문에,
이 게임들을 내멋대로 해석해 카피해보고자 한다. 주로 카피할 게임은 DJMAX이다.

그래서 이름도 DJadeMAX RESPECT V로 지었다.

구조 설계

BMS

이미 게임이든 음악이든 아마추어 제작자들이 BMS 데이터를 활용하고 있다.
BMSE와 BMHelper 등 아주 훌륭한 툴들이 있지만, 사용하지 않을 것이다.

MIDI

BMHelper에는 MIDI가 들어간다고 들었다.
MIDI의 구조를 공부한 김에, 노트가 연주되면 키음이 들릴 수 있도록 MIDI와 노트가 하나로 이뤄진 파일을 만들어보고자 한다.

구조 작성

내가 구상한 채보 데이터의 구조는 아래와 같다.

public enum KeyDefine
{
    Key4 = 0,
    Key5 = 1,
    Key6 = 2,
    Key8 = 3
}
public enum KeyIndex
{
    LeftShift   = 0,
    KEY_A       = 1,
    KEY_S       = 2,
    KEY_D       = 3,
    KEY_C       = 4,
    Comma       = 5,
    KEY_L       = 6,
    SemiColon   = 7,
    Quote       = 8,
    RightShift  = 9
}
[Serializable]
public struct Project
{
    public List<Head> heads;        // 헤드 정보

    public string title;            // 곡 제목
    public string artist;           // 아티스트
    public string genre;            // 장르
    public string maker;            // 제작자
    public string comment;          // 비고
    public string background;       // 배경 이미지, 로딩화면에 사용
    public string music;            // 음악 파일

    public uint deltaLength;        // 음악 길이, ms
    public byte BPM;                // Beat Per Minute, 음악 속도
}
[Serializable]
public struct Head
{
    public List<Track> tracks;      // 트랙 정보

    public KeyDefine keyDefine;     // 키 설정
    public string levelCover;       // 커버 이미지, 곡 선택창에 사용
    public byte level;              // 난이도
}
[Serializable]
public struct Track
{
    // MIDI 설정
    public uint channel;            // 채널
    public uint note;               // 음계
    public uint velocity;           // 음량
    public uint duration;           // 음 길이
    public uint instrument;         // 악기 종류

    // 노트 설정
    public uint deltaTime;          // 노트 출현 시간
    public uint noteLength;         // 노트 길이, 롱 노트일 경우에만 사용, 기본값 0
    public KeyIndex keyIndex;       // 키 Index
}

enum으로 들어간 것들은 원래 byte형으로 넣으려고 했는데, 아무리 봐도 직관적이지 않아 수정했다.

levelBPM은 255를 넘지 않을 것이기 때문에 byte형으로 넣긴 했는데........
수만개의 데이터가 들어갈 Track의 미디 데이터가 아니면 솔직히 의미 없을 것 같긴 하다.

변수들은 구조체 padding을 고려해 (C#에서도 의미가 있는지는 모르겠지만) 크기가 큰 순서대로 정렬해주었다.

string의 길이가 50Bytes를 넘지 않는다는 가정 하 모든 변수의 자료형에 따라 대략적인 크기를 계산해보았을 때
약 1.2MB의 메모리를 사용하는 것으로 계산됐다.

물론 곡의 난이도와 음악의 길이에 따라 더 많이 쓰긴 하겠지만, 파일 크기가 커봐야 3MB 남짓 될 것이라고 예상하고 데이터를 이진화 후 저장하기로 했다.

복합형 암,복호화

ARMA3의 애드온파일들은 bikeybiprivatekey확장자를 가진 키 페어로 서버에서 애드온의 무결성을 검사한다.

파일을 열어보면 이진화되어있긴 하지만, 공개 키와 개인 키를 사용하는 점,
그리고 파일 안에 키 이름과 RSA1, RSA2 라는 문구가 있는 것으로 보아 RSA 비대칭 암호화를 사용하는 것으로 보인다.

RSA는 대용량 데이터 처리에 불리하므로 내용물은 암호화하지 않고 애드온 패키지 파일인 pbo파일 헤더의 서명데이터를 체크해 키 무결성 검사를 하는 것으로 보인다. (그냥 내 추측이지만)

하지만 채보 데이터는 내용물이 공개되면 게임플레이에 지대한 영향을 미칠 수 있기 때문에 내용물도 보호해야 하니
파일 내용을 AES 암호화하고 openssl로 생성한 비대칭 키 페어를 이용해 AES 암호키와 초기화벡터를 한번 더 암호화한다.

다만, 채보 데이터를 제작하는 과정에서는 내용물을 열어볼 수 있어야 하므로 RSA는 생략하고 그대로 저장한다.
이렇게 되면 프로젝트 파일이 아닌 이상 에디터에서는 다시 열 수 없고, 인게임에서만 데이터 확인이 가능할 것이다.

public class FileIO
{
    private struct AESKeyData
    {
        public byte[] key;
        public byte[] iv;
    }

    public static async Task SaveToFile(Project project, string filePath)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        using (MemoryStream memoryStream = new MemoryStream())
        {
            byte[] key, iv;

            formatter.Serialize(memoryStream, project);

            byte[] serializedData = memoryStream.ToArray();
            byte[] encryptedData = Crypto.EncryptAES(serializedData, out key, out iv);

            using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
            {
                // write to file header
                fileStream.Write(key, 0, key.Length);
                fileStream.Write(iv, 0, iv.Length);

                await fileStream.WriteAsync(encryptedData, 0, encryptedData.Length);
            }
        }
    }

    public static async Task<Project> LoadFromFile(string filePath)
    {
        // Load to file and decrypt
        byte[] encryptedData;
        byte[] key, iv;

        using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
        {
            key = new byte[16];
            iv = new byte[16];
            await fileStream.ReadAsync(key, 0, key.Length);
            await fileStream.ReadAsync(iv, 0, iv.Length);

            encryptedData = new byte[fileStream.Length - key.Length - iv.Length];
            await fileStream.ReadAsync(encryptedData, 0, encryptedData.Length);

            byte[] serializedData = Crypto.DecryptAES(encryptedData, key, iv);

            // Deserialize
            BinaryFormatter formatter = new BinaryFormatter();
            using (MemoryStream memoryStream = new MemoryStream(serializedData))
            {
                return (Project)formatter.Deserialize(memoryStream);
            }
        }
    }

    public static async Task Export(Project project, string filePath)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        using (MemoryStream memoryStream = new MemoryStream())
        {
            byte[] key, iv;

            formatter.Serialize(memoryStream, project);

            byte[] serializedData = memoryStream.ToArray();
            byte[] encryptedData = Crypto.EncryptAES(serializedData, out key, out iv);

            using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
            {
                // write to file header
                // Encrypt key and iv with RSA
                RSAParameters rsaParams = Crypto.ReadRSAKey("msePub.pem");
                byte[] encryptedKey, encryptedIV;
                Crypto.EncryptAESData(key, iv, rsaParams, out encryptedKey, out encryptedIV);

                fileStream.Write(encryptedKey, 0, encryptedKey.Length);
                fileStream.Write(encryptedIV, 0, encryptedIV.Length);

                await fileStream.WriteAsync(encryptedData, 0, encryptedData.Length);
            }
        }
    }
}

하지만 이렇게 하면 인증서 키 파일이 그대로 보여 의미가 없으므로 나중에 HTTPS를 통해 RSA 암,복호화 또는 다른 방식의 암, 복호화를 하는 것으로 수정해야겠다.

BIG