มีโค้ดในปริมาณที่เหมาะสม มองเห็นได้ชัดเจนยิ่งขึ้นใน "บล็อกต้นฉบับ"

การแนะนำ

เป็นเวลาเกือบสองทศวรรษแล้วที่พีซีกลายเป็นแพลตฟอร์มที่เชื่อถือได้สำหรับเกมอาร์เคด และความสำเร็จอย่างสูงของแพลตฟอร์ม Type X จาก Taito ก็ตัดสินได้ว่าจะไม่ย้อนกลับไปอีก โลจิสติกส์นั้นเรียบง่าย ส่วนประกอบนอกชั้นวางที่มีการผลิตจำนวนมากจะมีราคาถูกกว่าในการผลิตและให้การสนับสนุน ซึ่งช่วยลดต้นทุนโดยรวม ไม่เพียงแค่นี้ แต่ซอฟต์แวร์จะพัฒนาได้ง่ายขึ้นและเข้าถึงได้มากขึ้น เนื่องจากแพลตฟอร์มที่จะทำงานต่อจากนี้ไม่เพียงแต่เป็นที่รู้จักเท่านั้น แต่ยังมีมาตรฐานมากกว่าอีกด้วย เกมอาร์เคดบนพีซีมีศักยภาพในการเปิดการพัฒนาซอฟต์แวร์อาร์เคดให้กับนักพัฒนาในวงกว้างขึ้น

ข้อเสียเพียงอย่างเดียวอาจเป็นการละเมิดลิขสิทธิ์และ การลักลอบค้าของเถื่อน และถึงแม้ว่ามันจะเป็นปัญหาในฉากอาร์เคดมาโดยตลอด แต่คราวนี้อาจจะแข็งแกร่งขึ้นได้ตั้งแต่พีซี แม้จะง่ายพอๆ กับการพัฒนา แต่ก็อาจกล่าวได้เช่นเดียวกัน สำหรับการป้องกันแคร็กและถอดรหัสข้อมูล สิ่งเหล่านี้เป็นส่วนหนึ่งของเลเยอร์ที่ห้ามไม่ให้เราเรียกใช้ซอฟต์แวร์บนพีซีปกติ ร่วมกับ อุปกรณ์ I/O

ประวัติศาสตร์บ้าง

แม้จะมีแพลตฟอร์มอาร์เคดบนพีซีจำนวนมากที่ปรากฏตามกาลเวลา และยังคงแข็งแกร่งมาจนถึงทุกวันนี้ ฉันจะเน้นไปที่เครื่องฮาร์ดแวร์เฉพาะสองเครื่องและซอฟต์แวร์ของพวกเขา: Taito Type X และ Examu eX-BOARD อันหนึ่งเป็น คลาสสิก และอีกอันเป็นความล้มเหลว แต่ทั้งคู่ต่างเป็นผู้บุกเบิกในการเคลื่อนไหวอาร์เคดบนพีซี

เรื่องราวย้อนกลับไปในปี 2009–2011 (ฉันจำไม่ได้แน่ชัด) เมื่อข้อมูลของเกม Type X ส่วนใหญ่, X2 บางเกม และเกม eX-BOARD ทั้งหมดถูกปล่อยออกมาในรูปแบบอาร์เคด ฟอรั่ม ข้อมูลนี้ไม่ได้รับการป้องกัน ซึ่งหมายความว่าไม่ต้องใช้อุปกรณ์รักษาความปลอดภัยหรือการตรวจสอบเพื่อให้เกมทำงานได้ ฉันจำไม่ได้ว่าอีมูเลเตอร์สำหรับ I/O ได้รับการเผยแพร่ในเวลาเดียวกันหรือไม่ แต่แน่นอนว่าหลังจากมีอันหนึ่งปรากฏขึ้นไม่นาน ตัวโหลดตัวแรกนี้ทำเช่นนั้น โดยจำลองอุปกรณ์ I/O และลบวอลล์สุดท้ายที่ทำให้ซอฟต์แวร์ทำงานโดยไม่แสดงหน้าจอ I/O ERROR

หลังจากนั้นไม่นาน Romhack ได้ปรับปรุงโปรแกรมจำลองนี้ให้เป็น โปรแกรมจำลอง I/O แบบโอเพ่นซอร์ส X I/O, ttx_monitor และต่อมาได้สร้างขึ้นเพิ่มเติม ตัวแปร eX-BOARD, xb_monitor ต่อมาเขายังทำ Cave-PC Variant, cv_monitor ด้วย แต่ตามความรู้ของฉัน มันไม่ได้เป็นแบบโอเพ่นซอร์ส โปรแกรมจำลอง Romhack กลายเป็นค่าเริ่มต้นที่จะใช้เป็นเวลานาน จนกระทั่งเมื่อไม่นานมานี้มีตัวเลือกอื่นที่มีความสามารถปรากฏขึ้น เช่น JConfig และ TeknoParrot และแม้ว่าสิ่งเหล่านี้จะจำลองอุปกรณ์ I/O ได้มากกว่ามาก ของเครื่องอื่นๆ เช่นกัน แกน Type X และ eX-BOARD ล้วนมีพื้นฐานมาจากโปรแกรมจำลองของ Romhack ตอนนี้แม้แต่ตัวฉันเองก็เข้าสู่กระแสและสร้าง เวอร์ชันที่ได้รับการปรับปรุง ของทั้ง ttx_monitor และ xb_monitor, TTX-Monitor+ และ XB-Monitor+ ตามลำดับ

คาดหวังอะไร

ขั้นแรก เราจะมาดูการป้องกันซอฟต์แวร์ในระดับสูงโดยสรุป จากนั้นไปที่ฮาร์ดแวร์ (และ JVS โดยรวม) วิธีเลียนแบบ วิเคราะห์การใช้งานของ Romhack และต่อยอด เหนือสิ่งอื่นใด เพิ่มฟีเจอร์และยกระดับคุณภาพชีวิตบางส่วน

เนื่องจากฉันไม่ได้เป็นเจ้าของเกมอาร์เคดใดๆ เลย คำศัพท์พื้นฐานบางอย่างเช่น JAMMA จึงไม่เป็นที่รู้จักสำหรับฉัน และยังคงเป็นเช่นนั้น ดังนั้นการสนับสนุนของฉันเพียงอย่างเดียวคือเอกสาร JVS ต้นฉบับและโค้ดของ Romhack นอกเหนือจาก เล่นเองด้วยซอฟต์แวร์ที่ใช้อุปกรณ์เหล่านั้น อย่างไรก็ตาม ฉันคิดว่าการทำความเข้าใจว่าสิ่งต่างๆ ทำงานอย่างไรก็เพียงพอแล้ว ดังนั้นคำพูด ระดับสูง ดังที่กล่าวไปแล้ว การเขียนนี้อาจเพลิดเพลินได้เฉพาะกับคนทั่วไปที่ไม่อยู่ในหัวข้อเท่านั้น และไม่ต้องแปลกใจหากมีข้อผิดพลาดใดๆ เกิดขึ้นในระหว่างการบรรยาย

การปกป้องอย่างดีที่สุด

เครื่องจักรรุ่นเก่าใช้การป้องกันที่ น่ารังเกียจ และ เถื่อน บางอย่าง แม้ในระดับฮาร์ดแวร์ เช่น ชิปฆ่าตัวตายและแบตเตอรี่ โชคดีที่เราไม่มีการป้องกันกามิกาเซ่ เป็นเพียงการตรวจสอบดองเกิล USB ง่ายๆ แต่มาดูรายละเอียดกันดีกว่า ฮาร์ดดิสก์ไดรฟ์มีสองพาร์ติชัน:

อันแรกมีการติดตั้ง Windows XP Embedded ตัวโหลด/ตัวเรียกใช้งานประเภท X และดิสก์อิมเมจเสมือนพร้อมข้อมูลเกม

ตัวโหลดจะตรวจสอบฮาร์ดแวร์ (ดองเกิล ดิสก์ไดรฟ์ พาร์ติชั่น) และหากทุกอย่างเรียบร้อยดี ระบบจะดึงคีย์ขึ้นมาซึ่งจะถอดรหัสไฟล์อิมเมจ เพื่อให้สามารถรันเกมได้ เมื่อเล่นเกมแล้ว เกมจะตรวจสอบ อุปกรณ์ JVS I/O ที่ถูกต้องใน พอร์ต COM2 หากมีอยู่ เกมจะดำเนินไปจนถึงการดำเนินการเต็มรูปแบบตามปกติ และหากไม่เป็นเช่นนั้น ข้อผิดพลาดของบอร์ด I/O จะปรากฏขึ้น

นอกจากนี้ยังมีพาร์ติชันที่สองซึ่งจัดเก็บการกำหนดค่าและข้อมูลเกมทั้งหมด เหตุผลเดียวที่ฉันคิดได้ก็คือพาร์ติชันแรกอาจเป็น อ่านอย่างเดียว หรืออย่างน้อยก็ไม่มีอะไรเขียนไว้ตรงนั้น

ในกรณีของเกม eX-BOARD เกมเหล่านั้นถูกส่งมาใน คาร์ทริดจ์ IDE ซึ่งป้องกันได้เพียงพอแล้ว หรือนั่นคือสาเหตุที่ Examu คิดว่าอย่างน้อยก็ถูกแคร็กในเวลาอันสั้น เนื่องจากขาดเอกสารเพิ่มเติมเกี่ยวกับการป้องกัน เมื่อดูที่โค้ด xb_monitor เราจะเห็นจุดเชื่อมต่อในไลบรารี IpgExKey.dll ฟังก์ชัน _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;

ตะขอฟังก์ชั่นใน XB-Monitor+

เมื่อโหลด DLL ใน Ghidra เราจะเห็นฟังก์ชันที่ส่งออกทั้งหมด GetKeyLicense เป็นหนึ่งในนั้น:

ฉันคิดว่ามันปลอดภัยที่จะสมมติจากชื่อว่ามันจัดการ การตรวจสอบการป้องกัน ดังนั้นการทำให้ผลลัพธ์กลับมา true ทุกครั้งก็เพียงพอแล้วที่จะข้ามมันไป ขณะนี้ข้อมูลที่ถอดรหัสและการป้องกันถูกถอดรหัสแล้ว สิ่งกีดขวางเดียวที่เหลืออยู่คืออุปกรณ์ JVS I/O

อุปกรณ์สื่อสาร

บอร์ด JVS มีฟังก์ชัน I/O ใน บอร์ดแยก ซึ่งมีตัวเชื่อมต่อ JAMMA สำหรับคอนโทรลเลอร์ และตัวเชื่อมต่อ CN2 กับ USB สำหรับการถ่ายโอนข้อมูลระหว่าง บอร์ด I/O หลัก และ บอร์ด TAITO I/O ระดับย่อย ภายใน Type X เอง แม้จะมีการเชื่อมต่อ USB จริงก็ตาม โปรโตคอล JVS ใช้สำหรับการถ่ายโอนข้อมูลผ่านการส่งข้อมูลแบบอนุกรม RS-485 บอร์ดย่อยนี้เชื่อมต่อกับพอร์ต COM2 บนเมนบอร์ด ดังนั้นเราจึงสามารถพูดได้ว่าบอร์ดนี้ทำงานเป็นอินเทอร์เฟซระหว่างบอร์ด I/O และตัวเครื่องเอง

จากนั้นซอฟต์แวร์จะอ่านข้อมูลอินพุตจากอุปกรณ์พอร์ต COM2 ข้อมูลจะถูกถ่ายโอนใน แพ็กเก็ต ผ่านโปรโตคอล JVS:

พูดง่ายๆ ก็คือ Sync Code จะกำหนดจุดเริ่มต้นของแพ็กเก็ต JVS ที่ถูกต้อง ซึ่งจะมีค่าเป็น 0xE0 เสมอ Node ระบุที่อยู่ปลายทาง หรืออุปกรณ์ทาส/โหนดของปลายทาง Byte กำหนดขนาดของแพ็กเก็ตที่เหลือ รวมถึง เช็คซัม และผลรวมนี้จะช่วยระบุว่าแพ็กเก็ตเสียหายหรือไม่ Data คือข้อมูลที่สร้างขึ้นโดย คำสั่ง และอาร์กิวเมนต์ มีรายการคำสั่งมากมายสำหรับฟังก์ชันต่างๆ ในการจำลอง คำสั่งส่วนใหญ่ได้รับการฮาร์ดโค้ดสำหรับการเริ่มต้นบอร์ด I/O ดังนั้นคำสั่งที่สำคัญสำหรับเราจริงๆ ก็คือคำสั่ง 0x20, SWINP หรือที่เรียกง่ายกว่านั้น สลับอินพุต:

แล้วเราจะเลียนแบบกระบวนการนี้ได้อย่างไร?

การจำลอง I/O

เนื่องจากบอร์ด JVS I/O เชื่อมต่อเข้ากับพอร์ต COM2 เราจึงจำเป็นต้องมี อุปกรณ์ COM ปลอม ที่จะช่วยให้เรากำหนดให้อุปกรณ์อินพุตใดๆ ทำงานเข้ากันได้กับ JVS สำหรับสิ่งนี้ เรา ฉีด hooks ของเราเองสำหรับ ฟังก์ชัน COM ในไลบรารีระบบ Kernel32 สำหรับกระบวนการที่ทำงานอยู่ ซึ่งจะดึงข้อมูลที่ถูกต้องจาก อุปกรณ์เสมือนปลอมที่เราสร้างขึ้น

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

ในแนวคิดนั้นเรียบง่าย เราสร้าง กระแสข้อมูล ที่จะ จำลอง โครงสร้างการถ่ายโอน JVS สร้างแพ็กเก็ตที่ถูกต้อง และส่งคืนการตอบกลับที่ถูกต้อง โดยพื้นฐานแล้ว เราจะป้อนข้อมูลที่สตรีมต้องการได้ยิน ส่วนที่น่าสนใจคือคำขอของคำสั่ง 0x20 เมื่อส่วนที่สองของการจำลองเข้ามาเล่น

นอกจากอุปกรณ์ COM ปลอมแล้ว เรายังต้องมี เลเยอร์อินพุตจริง ซึ่งเราสามารถเชื่อมโยงกับชั้นแรกได้ และสร้างสัญญาณอินพุตผ่าน แพ็กเก็ต JVS เสมือน สำหรับสิ่งนี้ เราสร้างอุปกรณ์ DInput สองเครื่อง (แม้ว่า API อินพุตใดๆ จะใช้งานได้ แต่เราใช้ DInput เนื่องจากเกมเองก็ใช้มันเหมือนกัน ดังนั้นเราจึงจำเป็นต้องใช้มัน): ปลอม อันหนึ่งซึ่งเชื่อมต่อกับเกมดังนั้นจึงตรวจไม่พบอินพุตใด ๆ และอันหนึ่งของจริงที่จะอ่านอินพุตของเราจากอุปกรณ์ที่เลือก ด้วยวิธีนี้ เราจะยกเลิกอินพุตใดๆ ที่อ่านจากเกม และเราใส่อินพุตของเราลงในสตรีม JVS ซึ่งเกมจะอ่านได้ในภายหลัง

// 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;
    }
}

การเริ่มต้น DInput จะทำงานตามปกติ อุปกรณ์จะถูกแจกแจง รับอุปกรณ์ที่เราต้องการ และสุดท้ายจะสร้าง เธรดการโพล เมื่อเรากดปุ่ม/ปุ่มบนอุปกรณ์ อุปกรณ์จะตั้งค่า ธง ใน อาร์เรย์สถานะอินพุต ซึ่งอ่านได้โดย การสำรวจสตรีม 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;
}

ตรวจสอบว่ามีการกดปุ่มโพลแล้วตั้งค่าสถานะที่สอดคล้องกันในอาร์เรย์

// 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;
}

จากนั้นการโพล JVS จะได้รับสถานะของตารางอินพุตและประมวลผล

เมื่อ JVS ปลอมตรวจพบแฟล็ก มันจะตั้งค่า บิต ใน ไบต์ ที่สอดคล้องกันของ บล็อกข้อมูลสวิตช์อินพุต:

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

การตั้งค่า 2 ไบต์แรกซึ่งเป็นของอินพุตของ P1

ในที่สุด แพ็กเก็ตจะถูกส่งและฟังก์ชันกลับมา การตอบกลับจะถูกบัฟเฟอร์ จากนั้นเกมจะตีความว่าเป็นการกดปุ่ม legit JVS I/O

สุดยอด แล้วไงต่อ

สิ่งที่เราได้เห็นทั้งหมดคือสิ่งที่นำมาใช้ในรถตักของ Romhack ซึ่งรวมถึงการจำลอง JVS I/O และการจัดการอินพุต งานแรกเป็นไปตามที่คาดไว้ แต่งานหลังในขณะที่ใช้งานได้ ยังขาด คุณสมบัติพื้นฐาน ในความคิดของฉัน ของฉัน ส่วนใหญ่ได้รับความนิยมจากตัวโหลดเช่น JConfig ดังนั้นจึงทำให้รู้สึกล้าสมัยเมื่อเปรียบเทียบ

แต่ทำไมไม่ใช้ทางเลือกที่ทันสมัยล่ะ? เพราะไม่ว่าจะด้วยเหตุผลใดก็ตาม ไม่มี ใดเลยที่สนับสนุนชื่อ eX-BOARD ของ Examu และสำหรับพวกเขาฉันหมายถึง JConfig เพราะ TeknoParrot ดูเหมือนจะสนับสนุนแพลตฟอร์ม แต่ฉันไม่ชอบตัวซอฟต์แวร์มากนัก ดังนั้นฉันจึงเลือกที่จะหลีกเลี่ยง แม้ว่านั่นจะหมายถึงการพัฒนาวิธีแก้ปัญหาของตัวเองก็ตาม

นี่คือจุดที่โซลูชันโอเพ่นซอร์สเท่านั้นเวอร์ชันปรับปรุงของฉันที่มีให้ใช้งาน xb_monitor ได้เข้ามามีบทบาท โดยพยายามนำตัวโหลดเก่าไปใช้ทางเลือกที่ทันสมัย และเนื่องจากฉันอยู่ที่นั่นแล้ว ฉันจึงตัดสินใจใช้วิธีเดียวกันกับ ttx_loader เพราะ ทำไมไม่ (และทั้งสองก็ใช้โค้ดส่วนใหญ่ร่วมกันอยู่แล้ว) แม้ว่าในภายหลังฉันจะพบว่ามีประโยชน์ สำหรับมัน. แต่ก่อนที่จะใช้คุณลักษณะใหม่เหล่านั้น สิ่งสำคัญคือต้องสังเกตว่าทั้งสองโครงการได้เห็นการยกเครื่องครั้งใหญ่ของ codebase ในลักษณะที่ ฉัน รู้สึกว่าอ่านได้ดีขึ้นมาก และทำความเข้าใจ ดังนั้นหากคุณต้องการดูผลงานภายใน อาจต้องตรวจสอบ TTX-Monitor+ และ XB-Monitor+ เหนือต้นฉบับ

การปรับปรุง QOL

ขั้นแรก ควบคุม ค่า deadzone สำหรับแกนนั้น เกินไป ต่ำ ดังนั้นในตัวควบคุมสมัยใหม่และละเอียดอ่อน เช่น Xbox Controller และ DualShock 4 จึงเป็นไปไม่ได้อย่างแท้จริงในการกำหนดค่า และเล่นด้วยแท่งอนาล็อก ไม่ต้องพูดถึงถ้าคุณมีปัญหาการดริฟท์เหมือนฉัน แม้ว่าจะเล็กน้อยก็ตาม การเพิ่มค่าฮาร์ดโค้ดนี้อีกเล็กน้อยก็เพียงพอที่จะแก้ไขปัญหานี้ได้

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

ค่า deadzone ใหม่ (500) โดยมีการใช้งานแบบเก่าใส่ความคิดเห็นไว้ (10)

ในฟังก์ชันสำหรับการรวมอินพุต จะมีการตรวจสอบเฉพาะแกนซ้าย (AxisL) และปุ่มเท่านั้น จำกัดมาก เพิ่มการรองรับแกนขวา (AxisR), ทริกเกอร์ (AxisZ) และ POVs นอกเหนือจากตัวเลือก PovAsAxis ซึ่งอนุญาตให้ใช้ POV ร่วมกับอินพุตที่แมปแกนซ้าย (เช่นปุ่ม Analog บนคอนโทรลเลอร์ 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;
    }
}

แยกจากฟังก์ชันการโพลอินพุต

ตอนนี้เราควบคุมเสร็จแล้ว ฉันจะพูดถึงคุณลักษณะบางอย่างที่ฉันคิดว่าไม่จำเป็นต้องรวมไว้: ฟังก์ชันการบันทึก และ wrapper DirectDraw แบบแรกยังคงมี การใช้โค้ด และสามารถใช้เป็นเครื่องมือแก้ไขข้อบกพร่องเพื่อการพัฒนาโดยเฉพาะ แทนที่จะเปิดและสร้างบันทึกอยู่เสมอซึ่งผู้ใช้ส่วนใหญ่ไม่สนใจ ทั้งหมด. แม้ว่า Wrapper ของ DirectDraw จะถูกลบออกไปแล้ว แต่จำเป็นต้องใช้ Direct3D 9 สำหรับ XB-Monitor+ แต่ถูกลดขนาดลงเพื่อจุดประสงค์เดียว นั่นคือ แก้ไขการวาดหน้าต่างใน Arcana Heart 3 การใช้งานแบบเดิมนั้นซับซ้อนกว่า แต่สำหรับสิ่งนี้ ฉันเพียงแค่บังคับโหมด เต็มหน้าจอ @640x480 เช่นเดียวกับเกม eX-BOARD ที่เหลือ

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

ส่วนสำคัญของกระดาษห่อ

การตัดสินใจครั้งนี้สนับสนุนการใช้ external wrappers เช่น ยอดเยี่ยม dgVoodoo ซึ่งไม่เพียงแต่สามารถแก้ปัญหาความไม่เข้ากันในระบบสมัยใหม่เท่านั้น แต่ยัง ปรับปรุง ภาพ ตัวโหลดอื่นๆ เช่น JConfig มีการรวม Wrapper ที่จำกัดเพื่อให้ได้รับประสบการณ์ นอกกรอบ มากขึ้น แต่ภายใต้ตัวฉันนั้นกลับทำงานได้ไม่ดีนักหรือขาดคุณสมบัติ ไม่ว่าสิ่งดี ๆ ที่จะมี

สุดท้ายแต่ไม่ท้ายสุด คุณลักษณะ SavePatch ดังที่แสดงไว้ตอนต้นของบทความ เกมส่วนใหญ่จะจัดเก็บตัวเลือกและข้อมูลคะแนนไว้ในพาร์ติชันอื่น พฤติกรรมของพวกเขาไม่เปลี่ยนแปลงและพวกเขาจะพยายามช่วยที่นั่น ปัญหาคือไม่ใช่ทุกคนจะมีพาร์ติชั่นที่สองที่มีอักษรระบุไดรฟ์ที่ระบุ และเราไม่ต้องการให้ข้อมูลของเรากระจัดกระจาย เพื่อแก้ไขปัญหานี้ เราจะเปลี่ยนเส้นทางการทำงานของไฟล์และไดเรกทอรีทั้งหมดไปยัง โฟลเดอร์บันทึก ที่ระบุในไดเรกทอรีรากของแอปพลิเคชัน

สำหรับเกม eX-BOARD นั้นเรียบง่าย เนื่องจากข้อมูลไม่ได้จัดเก็บไว้ในฮาร์ดไดรฟ์ แต่อยู่ใน หน่วยความจำชั่วคราว (ฉันไม่รู้ว่าอยู่ที่ไหนหรือในรูปแบบใด) ดังนั้นเราจึงสร้าง virtual SRAM ซึ่งจากนั้นจะถูกโหลดลงในหน่วยความจำ xb_monitor ได้วางข้อมูลไบนารี่ของ SRAM ไว้ในโฟลเดอร์ sv ซึ่งเป็นโครงสร้างที่ใช้ใน JConfig และ แพตช์ไบนารี และจะถูกนำไปใช้บน XB-Monitor+ และ TTX-Monitor+ ด้วยเช่นกัน

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

แต่ Type X นั้นเป็น สัตว์ร้าย ที่แตกต่างออกไป ซึ่งดูเรียบง่ายในตอนแรก แต่การใช้งานกลับซับซ้อน ใน ทฤษฎี เราเชื่อมโยงฟังก์ชันระบบ CreateDirectory และ CreateFile ของเราเอง (ทั้งตัวแปร ANSI และ อักขระกว้าง) และเรียกมันว่าสักวัน แต่เมื่อเราจัดการ ด้วยไดเร็กทอรีย่อย เวรน่ารำคาญ ด้วยการใช้งานปัจจุบัน ผมมีเกมทั้งหมดที่บันทึกไว้ในไดเร็กทอรีบันทึก แต่บางเกมเช่น The King Of Fighters '98, Gouketsuji Ichizoku Senzo Kuyou และ Trouble แม่มดจะไม่อ่านมันกลับ แม้ว่าจะสามารถแก้ไขได้ แต่อาจมีอัลกอริธึมที่แตกต่างกัน แต่ก็ไม่คุ้มกับความยุ่งยาก เมื่อพิจารณาว่าตัวโหลดอื่นๆ อาจมีแพตช์ เฉพาะเกม (แน่นอนว่าคือ TeknoParrot) ในขณะที่ฉันตั้งเป้าไปที่ แนวทางแบบไดนามิก

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

ฟังก์ชั่นระบบ hooks สำหรับไดเร็กทอรีและการเปลี่ยนเส้นทางไฟล์

สิ่งสำคัญที่ต้องชี้ให้เห็นคือเราเปลี่ยนเส้นทางการทำงานของไฟล์ที่ไม่สัมพันธ์กับเส้นทางปัจจุบันเท่านั้น ด้วยวิธีนี้เราจะไม่ยุ่งกับเกมที่พยายามอ่านไฟล์ข้อมูลของมัน

แนะนำการรองรับการป้อนข้อมูลไพ่นกกระจอก

คุณลักษณะ ใหม่ และ น่าตื่นเต้น ไม่ซ้ำใคร สำหรับ TTX-Monitor+ รองรับ เกมไพ่นกกระจอก จริงๆ แล้วตอนนี้มีแค่ Taisen Hot Gimmick 5 เท่านั้น หรืออาจจะเป็น Taisen Hot Gimmick Mix Party ในอนาคต มีประวัติค่อนข้างมากเกี่ยวกับเกมไพ่นกกระจอกและการจำลอง JVS I/O

เห็นได้ชัดว่าในขณะที่ทั้งคู่ใช้ JVS สำหรับการสื่อสาร I/O จริง ๆ ดูเหมือนว่าเป็น การใช้งานที่กำหนดเอง เนื่องจาก JVS ยืดหยุ่นเพียงพอ ในการดำเนินการนี้ ทำไม ฉันไม่รู้ พวกเขาอาจติดอยู่กับการจัดการแผงไพ่นกกระจอกมาตรฐาน (ส่วน S) คนที่อยู่เบื้องหลัง JConfig พยายามคิดออกเรื่องนี้ และบอกฉันว่าจำเป็นต้องมีที่แตกต่างกัน การถ่ายโอนข้อมูล JVS เพื่อให้ข้อมูลที่ถูกต้อง เกมกำลังคาดหวัง ระหว่างนี้ เรามาหาวิธีแฮ็กกันดีกว่า

โชคดีสำหรับเราที่นักพัฒนาทิ้งการควบคุมด้วยแป้นพิมพ์ไว้สำหรับ การแก้ไขข้อบกพร่อง หรืออย่างน้อยบน THG5 ซึ่งใช้ DirectInput THGMP ไม่ได้ แม้ว่าดูเหมือนว่าจะแมปคีย์บางคีย์ ซึ่งไม่ได้เปิดใช้งานด้วยเหตุผลบางประการ ฉันยังคงตรวจสอบเรื่องนี้อยู่ หวังว่าจะมีวิธีปลดล็อกคีย์บอร์ดสำหรับ THGMP ได้ มันควรจะทำงานจนกว่าวิธีที่เหมาะสมในการเลียนแบบไพ่นกกระจอก JVS I/O จะถูกแยกออกในที่สุด

ตัวโหลด ตัวเดียว ที่สามารถรัน THG5 ด้วยการควบคุมการดีบักได้นั้นเป็นตัวโหลดดั้งเดิม ซึ่งเปิดตัวครั้งแรก TypeX Loader เหตุผลก็คือตัวโหลดอื่นๆ ทุกตัวสร้างอุปกรณ์ DirectInput ปลอมเพื่อป้องกันไม่ให้เกมรับอินพุตด้วยตัวเอง ดังนั้นจึงบังคับให้จดจำเฉพาะอินพุต JVS เท่านั้น สำหรับเกมไพ่นกกระจอก ฮุคนี้ ปิดการใช้งาน ปล่อยให้เกมรับอินพุต และแม้ว่าตัวโหลดจะเสนอการกำหนดค่าอินพุตแผงไพ่นกกระจอก แต่มันก็ไม่เคยถูกนำไปใช้และ ไม่เคยทำงาน เราจะทำอย่างแรก และแก้ไขในภายหลัง

ด้วยเหตุนี้ แนวคิดก็คือการให้ ความรู้สึกปลอมของการจำลองที่เหมาะสม โดยเปิดใช้งาน การแมปใหม่ ของแป้นคีย์บอร์ดกับอุปกรณ์ใดๆ รวมถึงตัวคีย์บอร์ดด้วย ด้วยเหตุนี้ จึงมีการนำตัวตัดคำ DirectInput มาใช้ โดยมีความซับซ้อนของอัลกอริธึมบางอย่าง โดยพื้นฐานแล้ว จะดูแลการจัดการอินพุตทั้งหมด และจัดการคีย์ที่แมป ดั้งเดิม และ ผู้ใช้ ด้วยวิธีนี้ เราต้องการหลีกเลี่ยงข้อขัดแย้งใดๆ ที่อาจเกิดขึ้นเมื่อ ทับซ้อนกัน การกำหนดค่าดั้งเดิมและการกำหนดค่าผู้ใช้

แม้จะฟังดูง่ายในตอนแรก แต่ก็ค่อนข้างท้าทายที่จะนำไปใช้อย่างถูกต้อง การกำหนดค่าอินพุตไพ่นกกระจอกทั้งหมดถูกแยกออกจากอินพุตปกติ เพื่อให้ง่ายต่อการพัฒนา ทำความเข้าใจ และในที่สุด ใช้งาน

// 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.
    }
}

อัลกอริธึมการโพลเวอร์ชันมัลติเธรด

สำหรับการโพลอินพุต จะมีการสร้าง เธรดใหม่ สำหรับปุ่ม แต่ละ ตอนแรกฉันต้องการทำให้การนับเธรดเป็นไปตามอำเภอใจ แต่นั่นไม่ได้ไปไหนเลย นอกจากนี้ ยังมีการโพลเธรดแบบเธรดเดียว ซึ่ง น่าสนุกพอสมควร แก้ไขปัญหาว่าเกมจัดการกับการกดอินพุตหลาย ๆ ครั้งอย่างไม่สอดคล้องกันได้อย่างไร

สัมผัสสุดท้าย

ขณะนี้คุณลักษณะหลักและการปรับปรุงทั้งหมด มีการวางแผนไว้แต่ไม่ได้ ได้ถูกนำมาใช้แล้ว ยังมีอีกสิ่งหนึ่งที่ต้องติดตาม: อินเทอร์เฟซผู้ใช้ UI ดั้งเดิม (ttx_config) นั้นเรียบง่ายมาก ซึ่งสร้างด้วย MFC Application Wizard มันทำงานได้ แต่แชร์โค้ดกับตัวโหลดหลัก ซึ่งหมายความว่าฟีเจอร์คอนโทรลเลอร์ทั้งหมดจะต้องถูกย้ายไปยังที่นั่น รวมถึงเพิ่มการรองรับไพ่นกกระจอก

แทนที่จะอัปเดต UI แบบเก่า ฉันเลือกใช้โซลูชัน .NET Windows Forms ใหม่ที่สร้างตั้งแต่เริ่มต้น แบบนั้น เนื่องจากฉันได้ทำส่วนใหญ่แล้วสำหรับโปรเจ็กต์ ที่เกี่ยวข้องกับอาร์เคด อื่น รวมถึงการทำงานของอินเทอร์เฟซและการรวมคอนโทรลเลอร์ทั้งหมด ผ่านทาง DirectInput API ตอนที่ฉันพัฒนาสิ่งนี้ครั้งแรก มันใช้เวลานานพอสมควร เมื่อพิจารณาว่าฉันไม่เคยทำงานกับ DirectX หรือ API อินพุตใดๆ มาก่อน แต่ในขณะเดียวกัน มันก็ช่วยให้ฉันเข้าใจการใช้งานอินพุตทั้งหมดใน ttx_monitor และ xb_monitor ได้มาก

อาจจะยังไม่หมด

แม้ว่าฉันจะถือว่าทุกอย่าง เป็นส่วนใหญ่ เสร็จสิ้นแล้ว แต่ก็ยังมีเกมไพ่นกกระจอก ดักฟัง ฉันอยู่ ฉันจะลองดูว่าฉันจะทำให้มันทำงานกับการควบคุมแป้นพิมพ์แก้ไขข้อบกพร่องที่ฉัน สงสัย อยู่ที่นั่นได้ไหม แต่เพิ่งปิดการใช้งานไป หากเป็นเช่นนั้น กลไกไพ่นกกระจอกปัจจุบัน ควร ใช้งานได้กับการแก้ไขบางอย่าง

อย่างไรก็ตาม เกมมีพฤติกรรมแตกต่างจาก TGH5: TGHMP คือชุดเกม 4 เกมแรกในซีรีส์ แต่ละเกมมีไฟล์ปฏิบัติการของตัวเองในโฟลเดอร์ของตัวเอง รวมถึง โหมดทดสอบ และ เมนูพิเศษ เพื่อเลือกเกม ปรากฎว่า game.exe เป็นกระบวนการที่จัดการ การสร้างกระบวนการย่อย และ การสื่อสาร JVS โดยทำหน้าที่เป็น ไปป์ไลน์ ไปยังกระบวนการเกมจริง ด้วยเหตุนี้ เกมนี้เจ็บปวดในการแก้ไขข้อบกพร่อง ดังนั้นจึงไม่ได้ตรงไปตรงมามากนักอย่างที่เคยเป็น ถึงกระนั้นฉันก็จะลองดู

มันเป็นการเดินทางอันยาวนานที่ฉันยังไม่ได้พิจารณา แต่ฉันใช้ประสบการณ์ในการปรับโครงสร้างโครงการของบุคคลอื่น การใช้คุณลักษณะหลักใหม่ๆ การเขียนโปรแกรมเป็นครั้งแรกใน C และเพียงเรียนรู้เพิ่มเติมเกี่ยวกับ เครื่องอาร์เคดสมัยใหม่ทำงานอย่างไร

อ้างอิง

JVS — อาร์เคดโอตาคุ

JVS I/O — PCB Otaku Wiki

โปรโตคอล JVS — OpenJVS

คู่มือการใช้งานไทโตะ Type X — TAITO

JAMMA Video Standard (ฉบับที่สาม) (JVS) — JAMMA

JAMMA Video Standard (JVS) รุ่นที่สาม — อเล็กซ์ มาร์แชล

ฮาร์ดแวร์พีซีใน Arcades การวิเคราะห์ — Alex Marshall