Berisi kode dalam jumlah yang cukup, lebih baik dilihat di blog asli.

Perkenalan

Sudah hampir dua dekade sejak PC menjadi platform yang andal untuk arcade, dan kesuksesan besar platform Tipe X dari Taito memastikan hal tersebut tidak akan pernah terulang kembali. Logistiknya sederhana, komponen siap pakai dengan produksi massal lebih murah untuk diproduksi dan memberikan dukungan, sehingga mengurangi biaya keseluruhan. Tidak hanya itu, perangkat lunak akan lebih mudah dan lebih mudah diakses untuk dikembangkan, karena platform yang akan berjalan di atasnya tidak hanya sangat terkenal, tetapi juga lebih terstandarisasi. Arcade berbasis PC berpotensi membuka pengembangan perangkat lunak arcade bagi lebih banyak pengembang.

Satu-satunya kelemahan mungkin adalah pembajakan dan penyelundupan, dan meskipun hal ini selalu menjadi masalah di kancah arcade, kali ini bisa menjadi lebih kuat karena PC, sesederhana pengembangannya, bisa dikatakan hal yang sama untuk memecahkan perlindungan dan mendekripsi informasi. Ini merupakan bagian dari lapisan yang melarang kita menjalankan perangkat lunak pada PC normal, bersamaan dengan perangkat I/O.

Beberapa sejarah

Meskipun banyak sekali platform arcade berbasis PC yang bermunculan seiring berjalannya waktu, dan terus berkembang hingga saat ini, saya hanya akan berfokus pada dua mesin perangkat keras tertentu dan perangkat lunaknya: Taito Type X dan Examu eX-BOARD, yang satu adalah klasik, yang lainnya gagal, namun keduanya merupakan pionir dalam gerakan arcade berbasis PC.

Ceritanya dimulai pada tahun 2009–2011 (saya tidak ingat persisnya), ketika data dump dari sebagian besar game Tipe X, beberapa X2, dan semua judul eX-BOARD dirilis di arcade forum. Data ini tidak dilindungi, yang berarti tidak diperlukan perangkat keamanan atau pemeriksaan agar game dapat berfungsi. Saya tidak ingat apakah emulator untuk I/O juga dirilis pada waktu yang sama, tapi yang pasti tidak lama setelah emulator tersebut muncul. loader pertama ini melakukan hal tersebut, mengemulasi perangkat I/O, dan dengan demikian menghapus dinding terakhir yang mencegah perangkat lunak dijalankan tanpa menampilkan layar I/O ERROR.

Beberapa waktu setelahnya, Romhack mengerjakan ulang emulator ini menjadi emulator I/O sumber terbuka Tipe X, ttx_monitor, dan kemudian dibangun di atasnya menjadi varian eX-BOARD, xb_monitor. Dia kemudian juga membuat varian Cave-PC, cv_monitor, namun sepengetahuan saya, varian tersebut belum bersumber terbuka. Emulator Romhack menjadi default untuk digunakan dalam waktu yang lama, hingga baru-baru ini muncul alternatif lain yang mumpuni, seperti JConfig dan TeknoParrot, dan meskipun keduanya meniru lebih banyak perangkat I/O dari mesin lain juga, inti Tipe X dan eX-BOARD semuanya didasarkan pada emulator Romhack. Sekarang saya sendiri pun memasuki tren tersebut, dan membuat versi yang disempurnakan dari ttx_monitor dan xb_monitor, masing-masing TTX-Monitor+ dan XB-Monitor+.

Apa yang diharapkan

Pertama, kita akan melihat sekilas perlindungan perangkat lunak pada tingkat tinggi, kemudian membahas perangkat keras (dan JVS secara keseluruhan), cara menirunya, menganalisis implementasi Romhack, dan pengembangannya. di atas yang sudah ada, menambahkan fitur dan beberapa peningkatan kualitas hidup.

Karena saya tidak memiliki arcade apa pun, beberapa terminologi dasar seperti JAMMA tidak saya ketahui, dan masih tetap demikian, jadi satu-satunya dukungan saya adalah dokumentasi JVS asli dan kode Romhack, selain beberapa bermain sendiri dengan perangkat lunak yang menggunakan perangkat tersebut. Namun, menurut saya memahami cara kerja sesuatu sudah cukup, itulah komentar tingkat tinggi. Meskipun demikian, tulisan ini mungkin hanya dapat dinikmati oleh orang-orang biasa yang tidak mengetahui topik tersebut, dan jangan heran jika ada ketidakakuratan yang muncul selama perkuliahan.

Perlindungan yang terbaik

Mesin lama menerapkan perlindungan yang jahat dan liar, bahkan pada tingkat perangkat keras, seperti chip dan baterai yang bersifat bunuh diri. Untungnya, kami tidak memiliki perlindungan kamikaze apa pun, ini hanya cek dongle USB sederhana, tapi mari kita selami lebih dalam. Hard disk drive memiliki dua partisi:

Yang pertama memiliki instalasi Windows XP Embedded, loader/launcher Tipe X dan disk gambar virtual dengan data game.

Pemuat melakukan pemeriksaan perangkat keras (dongle, drive disk, partisi) dan jika semuanya baik-baik saja, kunci akan diambil, yang mendekripsi file gambar, sehingga dapat menjalankan permainan. Setelah game dijalankan, game akan memeriksa perangkat I/O JVS yang valid di port COM2. Jika ada, permainan akan berlanjut secara normal hingga mencapai eksekusi penuh, dan jika tidak, kesalahan papan I/O akan muncul.

Ada juga partisi kedua, yang menyimpan semua konfigurasi dan data game. Satu-satunya alasan saya memikirkan hal ini adalah karena partisi pertama mungkin hanya-baca, atau setidaknya tidak ada yang tertulis di sana.

Dalam kasus game eX-BOARD, game tersebut dikirimkan dalam kartrid IDE, sehingga cukup memberikan perlindungan. Atau itulah sebabnya mengapa Examu berpikir, setidaknya bisa dipecahkan dalam waktu singkat. Karena kurangnya dokumentasi lebih lanjut tentang perlindungan, dengan melihat kode xb_monitor kita melihat sebuah kait di perpustakaan IpgExKey.dll, fungsi _GetKeyLicense@0:

BOOL APIENTRY HookFunctions()
    // eX-BOARD software function hooks.
    HGetKeyLicense = HookIt("IpgExKey.dll", "_GetKeyLicense@0", Hook_GetKeyLicense);

INT APIENTRY Hook_GetKeyLicense(VOID)
    return 1;

Kait fungsi di XB-Monitor+

Memuat DLL di Ghidra, kita dapat melihat semua fungsi yang diekspor, GetKeyLicense salah satunya:

Saya pikir aman untuk berasumsi dari namanya bahwa ia menangani pemeriksaan perlindungan, jadi membuatnya selalu mengembalikan true sudah cukup untuk melewatinya. Sekarang dengan data yang didekripsi dan perlindungan telah retak, satu-satunya penghalang yang tersisa adalah perangkat I/O JVS.

Perangkat Komunikasi

Papan JVS memiliki fungsi I/O dalam papan terpisah, yang memiliki konektor JAMMA untuk pengontrol, dan konektor CN2 ke USB , untuk transfer data antara papan I/O utama dan papan I/O TAITO turunan di dalam Tipe X itu sendiri. Meskipun terdapat koneksi fisik USB, protokol JVS digunakan untuk transfer data melalui transmisi serial RS-485. Papan anak ini terhubung ke port COM2 pada motherboard, sehingga dapat dikatakan bahwa papan ini berfungsi sebagai antarmuka antara papan I/O dan mesin itu sendiri.

Perangkat lunak kemudian membaca data input dari perangkat port COM2. Data ditransfer dalam paket melalui protokol JVS:

Sederhananya, Sync Code menentukan awal paket JVS yang valid, yang selalu bernilai 0xE0. Angka Node menunjukkan alamat tujuan, atau perangkat budak/node tujuan. Angka Byte menentukan ukuran paket lainnya, termasuk checksum, dan jumlah ini membantu mengidentifikasi apakah suatu paket rusak atau tidak. Data tepatnya adalah data yang dibentuk oleh perintah dan argumen. Ada banyak daftar perintah untuk berbagai fungsi. Dalam emulasi, sebagian besar perintah dikodekan secara keras untuk inisialisasi papan I/O, jadi yang benar-benar penting bagi kami adalah perintah 0x20, SWINP, atau lebih mudahnya, Ganti Input:

Jadi, bagaimana kita meniru proses ini?

Emulasi I/O

Karena board I/O JVS terhubung ke port COM2, kita memerlukan perangkat COM palsu yang memungkinkan kita membuat perangkat input apa pun berperilaku kompatibel dengan JVS. Untuk ini, kami menyuntikkan hook kami sendiri untuk fungsi COM di perpustakaan sistem Kernel32 untuk proses yang sedang berjalan, yang akan mengambil data yang benar dari perangkat virtual palsu yang kami buat.

BOOL APIENTRY HookFunctions() {
    // Communications devices function hooks.
    HOOK("kernel32.dll", ClearCommError, H, LP, Hook);
    HOOK("kernel32.dll", CloseHandle, H, LP, Hook);
    HOOK("kernel32.dll", EscapeCommFunction, H, LP, Hook);
    HOOK("kernel32.dll", GetCommModemStatus, H, LP, Hook);
    HOOK("kernel32.dll", GetCommState, H, LP, Hook);
    HOOK("kernel32.dll", GetCommTimeouts, H, LP, Hook);
    HOOK("kernel32.dll", SetCommMask, H, LP, Hook);
    HOOK("kernel32.dll", SetCommState, H, LP, Hook);
    HOOK("kernel32.dll", SetCommTimeouts, H, LP, Hook);
    HOOK("kernel32.dll", SetupComm, H, LP, Hook);
}

Konsepnya sederhana, kita membuat aliran data yang akan mensimulasikan struktur transfer JVS, membangun paket yang benar dan mengembalikan balasan yang valid. Pada dasarnya, kami memasukkan data yang ingin didengar oleh aliran tersebut. Bagian yang menarik adalah permintaan perintah 0x20, ketika bagian kedua dari emulasi mulai dimainkan.

Selain perangkat COM palsu, kita juga memerlukan lapisan masukan yang benar, yang kemudian kita dapat menghubungkannya dengan lapisan sebelumnya dan dengan demikian menghasilkan sinyal masukan melalui paket JVS virtual. Untuk ini, kami membuat dua perangkat DInput (walaupun API masukan apa pun bisa berfungsi, kami menggunakan DInput karena game itu sendiri juga menggunakannya, jadi kami tetap memerlukannya): palsu satu, yang dihubungkan ke dalam game sehingga tidak mendeteksi masukan apa pun, dan yang nyata, yang akan membaca masukan kita dari perangkat yang dipilih. Dengan cara ini, kami membatalkan input apa pun yang dibaca dari game, dan kami memasukkan input kami ke dalam aliran JVS, yang kemudian akan dibaca oleh game.

// Prevents the games of having access to input devices.
HRESULT APIENTRY Fake_DirectInput8Create(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut, LPUNKNOWN punkOuter) {
    // Flag to make a true DInput device after the fake one was already created.
    if (DIMagicCall)
        // Passthrough to create a normal DInput device
        return FDirectInput8Create(hinst, dwVersion, riidltf, ppvOut, punkOuter);
    else {
        *ppvOut = (LPVOID)pFakeInterface;
        punkOuter = NULL;
        // This device returns null when GetState() is called, thus no inputs are registered.
        return DI_OK;
    }
}

Inisialisasi DInput berperilaku seperti biasa, perangkat dihitung, memperoleh perangkat yang kita inginkan, dan terakhir membuat thread polling. Ketika kita menekan sebuah tombol/tombol pada perangkat, itu akan menyetel bendera dalam array status masukan, yang dibaca oleh jajak pendapat aliran JVS.

// Check for a joystick command.
if (IS_JOY_OBJECT(InValue)) {
    // Check joystick axes and buttons.
}
// Check for keyboard commands.
else {
    int Button = GET_JOY_BUT(InValue);
    StateTable[i] = JoyState[JoyNumber].rgbButtons[Button] & 0x80 ? 1 : 0;
}

Periksa apakah tombol yang disurvei ditekan dan atur tanda yang sesuai dalam array

// Controller status. Command SWINP.
case 0x20: {
    // Push to byte 0.
    JVS.bPush(InputMgr.GetState(TEST_MODE) ? 0x80 : 0);
    // Push to bytes 1 and 2.
    JVS.bPush(InInfo.Xp1HiByte());
    JVS.bPush(InInfo.Xp1LoByte());
    JVS.bPush(InInfo.Xp2HiByte());
    JVS.bPush(InInfo.Xp2LoByte());
    break;
}

Kemudian polling JVS akan mendapatkan status tabel input dan memprosesnya

Ketika JVS palsu mendeteksi tanda tersebut, ia menyetel bit dalam byte yang sesuai dari blok data Saklar Input:

BYTE Xp1HiByte() {
    BYTE Byte = 0;
    if (InputMgr.GetState(P1_START))
        Byte |= 0x80;
    if (InputMgr.GetState(P1_SERVICE))
        Byte |= 0x40;
    if (InputMgr.GetState(P1_UP))
        Byte |= 0x20;
    if (InputMgr.GetState(P1_DOWN))
        Byte |= 0x10;
    if (InputMgr.GetState(P1_RIGHT))
        Byte |= 0x04;
    if (InputMgr.GetState(P1_LEFT))
        Byte |= 0x08;
    if (InputMgr.GetState(P1_BUTTON_1))
        Byte |= 0x02;
    if (InputMgr.GetState(P1_BUTTON_2))
        Byte |= 0x01;
    return Byte;
}

BYTE Xp1LoByte() {
    BYTE Byte = 0;
    if (InputMgr.GetState(P1_BUTTON_3))
        Byte |= 0x80;
    if (InputMgr.GetState(P1_BUTTON_4))
        Byte |= 0x40;
    if (InputMgr.GetState(P1_BUTTON_5))
        Byte |= 0x20;
    if (InputMgr.GetState(P1_BUTTON_6))
        Byte |= 0x10;
    return Byte;
}

Menyiapkan 2 byte pertama, yang merupakan milik input P1.

Akhirnya, paket dikirim dan fungsinya kembali, balasannya di-buffer dan kemudian ditafsirkan oleh permainan sebagai penekanan tombol JVS I/O yang sah.

Luar biasa, sekarang bagaimana

Yang kita lihat sejauh ini hanyalah apa yang diterapkan pada loader Romhack. Ini termasuk emulasi JVS I/O dan penanganan input. Versi pertama berfungsi seperti yang diharapkan, namun versi kedua, meskipun berfungsi, masih kurang, menurut pendapat saya, fitur dasar. Kebanyakan dari mereka dipopulerkan oleh loader seperti JConfig, sehingga membuatnya terasa ketinggalan jaman jika dibandingkan.

Tapi mengapa tidak menggunakan alternatif modern? Karena, apa pun alasannya, tidak ada yang mendukung judul eX-BOARD Examu, dan yang saya maksud adalah JConfig, karena TeknoParrot sepertinya mendukung platform tersebut, namun saya tidak terlalu menyukai perangkat lunak itu sendiri jadi saya lebih memilih menghindarinya, meskipun itu berarti mengembangkan solusi saya sendiri.

Di sinilah versi saya yang disempurnakan dari satu-satunya solusi sumber terbuka yang tersedia, xb_monitor, ikut berperan, mencoba menjadikan loader lama sebagai alternatif modern. Dan karena saya sudah ada di sana, saya juga memutuskan untuk menerapkan perlakuan yang sama pada ttx_loader, karena mengapa tidak (dan keduanya berbagi sebagian besar kode), meskipun nantinya saya akan menemukan kegunaan yang baik untuk itu. Namun sebelum membahas fitur-fitur baru tersebut, penting untuk diperhatikan bahwa kedua proyek tersebut telah mengalami perombakan besar-besaran pada basis kode, sedemikian rupa sehingga Saya merasa jauh lebih baik untuk dibaca. dan mengerti, jadi jika Anda ingin melihat karya bagian dalamnya, mungkin ingin memeriksa TTX-Monitor+ dan XB-Monitor+ di atas aslinya.

Peningkatan kualitas hidup

Pertama, kontrol. Nilai zona mati untuk sumbu terlalu rendah, sehingga pada pengontrol modern dan sensitif, seperti Xbox Controller dan DualShock 4, secara harfiah tidak mungkin untuk dikonfigurasi dan bermain dengan stik analog, apalagi jika Anda memiliki masalah drift seperti saya, meskipun hanya masalah kecil. Menambah sedikit nilai hardcode ini sudah cukup untuk memperbaiki masalah ini.

#define DEADZONE 500 /*(MAX_AXIS_VAL / DEADZONE_DIV)*/

Nilai zona mati baru (500), dengan implementasi lama dikomentari (10)

Dalam fungsi pengumpulan input, hanya sumbu kiri (AxisL) dan tombol yang dicentang, sangat terbatas. Dukungan untuk sumbu kanan (AxisR), pemicu (AxisZ) dan POVs ditambahkan, selain opsi PovAsAxis, yang memungkinkan untuk menggunakan POV bersama dengan input yang dipetakan sumbu kiri (seperti tombol Analog pada pengontrol DualShock).

// Axis definitions.
#define AXIS_X              1
#define AXIS_Y              2
#define AXIS_Z              3
#define AXIS_RX             4
#define AXIS_RY             5
#define POVN                10
// POVs definitions.
#define POV_CENTER          -1
#define POV_UP              0
#define POV_UP_RIGHT        4500
#define POV_RIGHT           9000
#define POV_RIGHT_DOWN      13500
#define POV_DOWN            18000
#define POV_DOWN_LEFT       22500
#define POV_LEFT            27000
#define POV_LEFT_UP         31500

// Polling of joystick axes and POVs.
switch (GET_JOY_AXIS(InValue)) {
    case AXIS_X: {
        if (IS_NEGATIVE_AXIS(InValue)) {
            if ((JoyState[JoyNumber].lX < -DEADZONE) || (mTable[CONFIG_POVASAXIS] && ((Dir == POV_LEFT) || (Dir == POV_DOWN_LEFT) || (Dir == POV_LEFT_UP))))
                StateTable[i] = 1;
        }
        else {
            if ((JoyState[JoyNumber].lX > DEADZONE) || (mTable[CONFIG_POVASAXIS] && ((Dir == POV_RIGHT) || (Dir == POV_UP_RIGHT) || (Dir == POV_RIGHT_DOWN))))
                StateTable[i] = 1;
        }
        break;
    }
    case POVN: {
        // To avoid problems, mapped POVs are disabled and
        // forced to work as axis if PovAsAxis option is enabled.
        if ((JoyState[JoyNumber].rgdwPOV[0] != -1) && !mTable[CONFIG_POVASAXIS]) {
            if (GET_JOY_RANGE(InValue) == POV_UP && ((Dir == POV_UP) || (Dir == POV_UP_RIGHT) || (Dir == POV_LEFT_UP)))
                StateTable[i] = 1;
            if (GET_JOY_RANGE(InValue) == POV_RIGHT && ((Dir == POV_RIGHT) || (Dir == POV_UP_RIGHT) || (Dir == POV_RIGHT_DOWN)))
                StateTable[i] = 1;
            if (GET_JOY_RANGE(InValue) == POV_DOWN && ((Dir == POV_DOWN) || (Dir == POV_RIGHT_DOWN) || (Dir == POV_DOWN_LEFT)))
                StateTable[i] = 1;
            if (GET_JOY_RANGE(InValue) == POV_LEFT && ((Dir == POV_LEFT) || (Dir == POV_DOWN_LEFT) || (Dir == POV_LEFT_UP)))
                StateTable[i] = 1;
        }
        break;
    }
}

Ekstrak dari fungsi polling input.

Sekarang setelah kita selesai dengan kontrolnya, saya akan membahas beberapa fitur yang menurut saya tidak perlu disertakan: fungsi logging dan wrapper DirectDraw. Yang pertama masih ada sesuai kode, dan tersedia sebagai alat debug hanya untuk pengembangan, daripada selalu aktif dan membuat log yang tidak dipedulikan oleh sebagian besar pengguna. semua. Meskipun pembungkus DirectDraw telah dihapus, Direct3D 9 diperlukan untuk XB-Monitor+, namun telah dikurangi menjadi satu tujuan: memperbaiki gambar jendela di Arcana Heart 3. Implementasi aslinya lebih kompleks, namun untuk ini saya hanya memaksakan mode layar penuh @640x480, seperti yang dilakukan game eX-BOARD lainnya.

HRESULT HookIDirect3D9::CreateDevice(LPVOID _this, UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface) {
    pPresentationParameters->Windowed = FALSE;
    pPresentationParameters->BackBufferWidth = 640;
    pPresentationParameters->BackBufferHeight = 480;
    return pD3D->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
}

Bagian penting dari pembungkusnya

Keputusan ini mendukung penggunaan pembungkus eksternal, seperti yang sangat baik dgVoodoo, yang tidak hanya dapat mengatasi ketidakcocokan dalam sistem modern, namun juga meningkatkan visualnya. Loader lain seperti JConfig memiliki paket wrapper terbatas untuk memberikan pengalaman yang lebih out-of-the-box, namun menurut saya, loader tersebut tidak berfungsi dengan baik, atau kurang memiliki fitur. Apapun itu, hal yang menyenangkan untuk dimiliki.

Terakhir, fitur SavePatch. Seperti yang ditunjukkan di awal artikel, sebagian besar game menyimpan opsi dan data skornya di partisi yang berbeda. Perilaku mereka tidak berubah dan mereka akan berusaha menyelamatkan diri di sana. Masalahnya adalah, tidak semua orang memiliki partisi kedua dengan huruf drive yang ditentukan, dan kita tidak ingin data kita tersebar di mana-mana. Untuk memperbaikinya, kami mengalihkan semua operasi file dan direktori ke folder penyimpanan tertentu di direktori akar aplikasi.

Untuk permainan eX-BOARD itu sederhana, karena datanya tidak disimpan di hard drive, melainkan di memori volatil (saya tidak tahu secara spesifik di mana atau dalam bentuk apa), jadi kami membuat sebuah File SRAM virtual, yang kemudian dimuat di memori. xb_monitor telah menempatkan data biner SRAM di folder sv, sebuah struktur yang digunakan di JConfig dan patch biner, dan juga akan dibawa ke XB-Monitor+ dan TTX-Monitor+.

VOID SaveSRAM() {
    FILE* Stream = NULL;
    Stream = fopen(SRAM_NAME, "wb");    
    if (!Stream)
        return;
    fwrite(SRAM, 1, SRAM_SIZE, Stream);
    fclose(Stream);
}

Namun Tipe X adalah binatang yang berbeda, yang terlihat sederhana pada awalnya, namun penerapannya menjadi rumit. Dalam teori kita mengaitkan fungsi sistem CreateDirectory dan CreateFile kita sendiri (varian ANSI dan karakter lebar) dan menghentikannya, namun saat kita berurusan dengan subdirektori sialan jadi mengganggu. Dengan implementasi saat ini, saya memiliki semua game yang disimpan di direktori penyimpanan, namun beberapa di antaranya seperti The King Of Fighters '98, Gouketsuji Ichizoku Senzo Kuyou dan Trouble Penyihir tidak akan membacanya kembali. Meskipun mungkin dapat diperbaiki, mungkin dengan algoritma yang berbeda, hal ini tidak sebanding dengan kerumitannya, mengingat loader lain mungkin memiliki patch khusus game (tentu saja TeknoParrot), sementara saya bertujuan untuk lebih patch khusus game. pendekatan >dinamis.

using namespace std::literals;

// Beautiful recursion. Necessary for games which create subfolders for savedata.
void CreateFolderA(CHAR* SaveFolder, CHAR* SaveSubFolderC) {
    if (strcmp(SaveFolder, SaveSubFolderC) != 0) {
        CHAR SaveSubFolder[MAX_PATH];
        strcpy(SaveSubFolder, SaveSubFolderC);
        strrchr(SaveSubFolderC, '\\')[0] = '\0';
        CreateFolderA(SaveFolder, SaveSubFolderC);
        HCreateDirectoryA(SaveSubFolder, nullptr);
    }
    else
        HCreateDirectoryA(SaveFolder, nullptr);
}

BOOL APIENTRY Hook_CreateDirectoryA(LPCSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes)
{
    if (mTable[CONFIG_SAVEPATCH]) {
        // Assuming that no Type X game store data in the C: drive. Excludes relative paths.
        if ((lpPathName[0] != 'C' && lpPathName[0] != 'c') && lpPathName[1] == ':') {
            CHAR RootPath[MAX_PATH];
            GetModuleFileNameA(GetModuleHandleA(nullptr), RootPath, _countof(RootPath));
            strrchr(RootPath, '\\')[0] = '\0';
            std::string SavePath = RootPath + "\\sv\\"s;
            return HCreateDirectoryA(SavePath.c_str(), nullptr);
        }
    }
    return HCreateDirectoryA(lpPathName, lpSecurityAttributes);
}

HANDLE APIENTRY Hook_CreateFileA(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile)
{
    if (mTable[CONFIG_SAVEPATCH]) {
        // Assuming that no Type X game store data in the C: drive. Excludes relative paths.
        if ((lpFileName[0] != 'C' && lpFileName[0] != 'c') && lpFileName[1] == ':') {
            // Get game working directory.
            CHAR RootPath[MAX_PATH];
            GetModuleFileNameA(GetModuleHandleA(nullptr), RootPath, _countof(RootPath));
            // Strip executable filename from path.
            strrchr(RootPath, '\\')[0] = '\0';
            std::string FilePath = lpFileName;
            // Forced to 3 to skip both slashes and backslashes.
            std::string FileName = FilePath.substr(3);
            // Get working directory lenght.
            int PathLenght = 0;
            for (int i = 0; i < MAX_PATH; i++)
                if (RootPath[i] == '\0') {
                  PathLenght = i;
                  break;
                }
            // Exclude directory files. Avoids screwing up normal file operations.
            if (strncmp(lpFileName, RootPath, PathLenght) != 0) {
                std::string SavePath = RootPath + "\\sv\\"s;
                std::string SaveFile = SavePath + FileName;
                std::string SaveSubFolderS = SaveFile.substr(0, SaveFile.length() - (FileName.length() - FileName.rfind('\\')));
                CHAR SaveFolder[MAX_PATH];
                strcpy(SaveFolder, (SavePath.substr(0, SavePath.length() - 1)).c_str());
                CHAR SaveSubFolderC[MAX_PATH];
                strcpy(SaveSubFolderC, SaveSubFolderS.c_str());
                CreateFolderA(SaveFolder, SaveSubFolderC);
                return HCreateFileA(SaveFile.c_str(), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
            }
        }
    }
    return HCreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

Kait fungsi sistem untuk pengalihan direktori dan file

Sesuatu yang penting untuk diperhatikan adalah kami hanya mengalihkan operasi file yang tidak berhubungan dengan jalur saat ini. Dengan cara ini kita tidak akan mengacaukan permainan yang mencoba membaca file datanya.

Memperkenalkan dukungan masukan mahjong

Fitur baru dan menarik yang unik pada TTX-Monitor+ adalah dukungan untuk permainan mahjong. Sebenarnya hanya Taisen Hot Gimmick 5 untuk saat ini, mungkin Taisen Hot Gimmick Mix Party di masa mendatang. Ada cukup banyak sejarah dengan permainan mahjong dan emulasi JVS I/O-nya.

Rupanya, meskipun keduanya memang menggunakan JVS untuk komunikasi I/O, sepertinya ini merupakan implementasi khusus, karena JVS cukup fleksibel untuk melakukan hal ini. Mengapa? Saya tidak tahu, mereka bisa saja terjebak dengan penanganan panel mahjong standar (Anda tahu, bagian S). Orang-orang di belakang JConfig telah mencoba mencari tahu hal ini, dan mengatakan kepada saya bahwa yang berbeda JVS dump diperlukan untuk menyediakan data yang benar permainan yang diharapkan. Sampai saat itu tiba, mari kita cari solusi hacky.

Beruntung bagi kami, pengembang meninggalkan kontrol keyboard untuk debugging, atau setidaknya di THG5, yang menggunakan DirectInput. THGMP tidak, meskipun tampaknya memetakan beberapa kunci, yang tidak diaktifkan karena alasan tertentu. Saya masih menyelidikinya, semoga ada cara untuk membuka kunci keyboard untuk THGMP. Ini akan berfungsi sampai cara yang tepat untuk meniru mahjong JVS I/O akhirnya diselesaikan.

satu-satunya loader yang dapat menjalankan THG5 dengan kontrol debug adalah yang asli, yang pertama kali dirilis TypeX Loader. Alasannya adalah setiap loader lain membuat perangkat DirectInput palsu untuk mencegah game mengambil input sendiri, sehingga hanya input JVS yang dikenali. Untuk judul mahjong, pengait ini dinonaktifkan, membiarkan game mengambil masukannya, dan meskipun pemuat menawarkan konfigurasi masukan panel mahjong, pengait ini tidak pernah diterapkan dan tidak pernah berfungsi. Kami akan melakukan yang pertama, dan memperbaiki nanti.

Jadi, dengan mengingat hal ini, idenya adalah untuk memberikan kesan palsu dari emulasi yang tepat, dengan mengaktifkan pemetaan ulang tombol keyboard ke perangkat apa pun, termasuk keyboard itu sendiri. Untuk ini, pembungkus DirectInput diimplementasikan, dengan beberapa kompleksitas algoritma. Pada dasarnya, ini menangani semua penanganan masukan, dan mengelola kunci asli dan pengguna yang dipetakan. Dengan cara ini, kami ingin menghindari konflik apa pun yang mungkin terjadi ketika tumpang tindih konfigurasi asli dan konfigurasi pengguna.

Meskipun kedengarannya sederhana pada awalnya, namun cukup menantang untuk diterapkan dengan benar. Semua konfigurasi masukan mahjong telah dipisahkan dari masukan normal, untuk memudahkan pengembangan, pemahaman, dan pada akhirnya, penggunaan.

// Only limitation is that if a key is mapped to a pointer of another key, both of those
// can't be pressed at the same time. Example: 'A' is mapped to the 'A' key, and 'B' is
// mapped to '1'. Both can't be pressed at the same time because 'A' points to '1'.
void PollInputMulti(int ThreadNumber) {
    for (;;) {
        // +3 is the mahjong inputs offset.
        if (InputMgr.GetState(ThreadNumber + 3)) {
            // Avoid the thread to process a key already being processed by another.
            if (isPressed[ThreadNumber] == 0) {
                isPressed[ThreadNumber] = 1;
                INPUT Input = { 0 };
                Input.type = INPUT_KEYBOARD;
                Input.ki.wScan = MapVirtualKey(LOBYTE(VI_CODES[ThreadNumber]), 0);
                // Value needed for the releasing of pointed keys.
                int isPointer = 0xFF;
                // Check if the key pressed has a pointer key.
                for (int k = M_START; k < M_END; k++)
                    if ((DIK_CODES[ThreadNumber] == iTable[k])) {
                        isPointer = k;
                        break;
                    }
                if (isPointer != 0xFF)
                    isPressed[isPointer] = 2;
                // SendInput loop.
                while (InputMgr.GetState(ThreadNumber + 3)) {
                    Input.ki.dwFlags = KEYEVENTF_SCANCODE;
                    SendInput(1, &Input, sizeof(Input));
                    Sleep(10); // Pause necessary for the next key to be recognized.
                    Input.ki.dwFlags = KEYEVENTF_KEYUP;
                    SendInput(1, &Input, sizeof(Input));
                }
                // Wait for another thread to process and reject the last SendInput,
                // in case the sent key pointed to an also mapped key.
                Sleep(50);
                if (isPointer != 0xFF)
                    isPressed[isPointer] = 0;
                isPressed[ThreadNumber] = 0;
            }
        }
        Sleep(20); // Reduce the thread processing.
    }
}

Versi multi-thread dari algoritma polling

Untuk jajak pendapat masukan, utas baru dibuat untuk tombol setiap. Awalnya saya ingin membuat jumlah thread sembarangan, tapi itu tidak berhasil. Selain itu, polling thread tunggal juga tersedia, yang lucunya memperbaiki masalah ketidakkonsistenan game dalam menangani beberapa penekanan input.

Satu sentuhan terakhir

Kini setelah semua fitur inti dan penyempurnaan, direncanakan dan tidak, telah diterapkan, ada satu hal lagi yang harus dilacak: antarmuka pengguna. UI asli (ttx_config) sangat sederhana, dibuat dengan MFC Application Wizard. Itu berhasil, tetapi ia berbagi kode dengan pemuat utama, yang berarti bahwa semua fitur pengontrol juga harus di-porting ke sana, ditambah menambahkan dukungan mahjong.

Daripada memperbarui UI lama, saya menggunakan solusi .NET Windows Forms baru yang dibuat dari awal. Ya, semacam, karena sebagian besar sudah saya selesaikan untuk proyek terkait arcade lainnya, termasuk semua operasi antarmuka dan integrasi pengontrol, melalui DirectInput API. Ketika saya pertama kali mengembangkan ini, butuh waktu yang cukup lama, mengingat saya belum pernah bekerja dengan DirectX atau API input apa pun sebelumnya, tetapi pada saat yang sama ini banyak membantu saya untuk memahami keseluruhan implementasi input di ttx_monitor dan xb_monitor.

Mungkin belum berakhir

Meskipun saya menganggap semuanya sebagian besar sudah selesai, masih ada permainan mahjong yang mengganggu saya. Saya akan mencobanya untuk melihat apakah saya bisa membuatnya berfungsi dengan kontrol keyboard debug yang saya duga ada di sana, tetapi baru saja dinonaktifkan. Jika demikian, mesin mahjong saat ini seharusnya berfungsi dengan beberapa modifikasi.

Namun, perilaku game ini berbeda dibandingkan TGH5: TGHMP adalah kumpulan dari 4 game pertama dalam seri ini. Tiap game memiliki executable tersendiri di foldernya masing-masing, termasuk mode uji dan menu khusus untuk memilih game. Ternyata game.exe adalah proses yang menangani pembuatan proses anak dan komunikasi JVS, yang bertindak sebagai saluran ke proses game sebenarnya. Oleh karena itu, game ini susah untuk di-debug, sehingga tidak semudah biasanya. Tetap saja, aku akan mencobanya.

Ini adalah perjalanan panjang yang belum saya pertimbangkan untuk selesai, namun saya mengambil pengalaman memfaktorkan ulang proyek orang lain, mengimplementasikan fitur-fitur inti baru, pemrograman untuk pertama kalinya di C, dan hanya mempelajari lebih lanjut tentang cara kerja mesin arcade modern.

Referensi

JVS — Otaku Arkade

JVS I/O — Wiki Otaku PCB

Protokol JVS — OpenJVS

Panduan Pengguna Taito Tipe X — TAITO

Standar Video JAMMA (Edisi Ketiga) (JVS) — JAMMA

JAMMA Video Standard (JVS) Edisi Ketiga — Alex Marshall

Perangkat Keras PC di Arcade, Analisis — Alex Marshall