Star Wars Trilogy lever feedback activated

This commit is contained in:
SpinDizzy 2020-10-24 09:12:54 +00:00
parent 402baf87b8
commit 5143669cb9
4 changed files with 318 additions and 53 deletions

View file

@ -2086,8 +2086,8 @@
<patches> <patches>
<!-- skip force feedback lever check --> <!-- skip force feedback lever check -->
<!-- <patch region="crom" bits="32" offset="0xf6e44" value="0x60000000" /> --> <!-- from MAME --> <!-- <patch region="crom" bits="32" offset="0xf6e44" value="0x60000000" /> --> <!-- from MAME -->
<patch region="crom" bits="32" offset="0xf0e48" value="0x60000000" /> <!--<patch region="crom" bits="32" offset="0xf0e48" value="0x60000000" /> --> <!-- no more needed if ffb lever rom loaded -->
<patch region="crom" bits="32" offset="0x043dc" value="0x48000090" /> <!-- related to joystick feedback --> <!--<patch region="crom" bits="32" offset="0x043dc" value="0x48000090" /> --> <!-- related to joystick feedback --> <!-- no more needed if ffb lever rom loaded -->
<patch region="crom" bits="32" offset="0x029a0" value="0x60000000" /> <!-- skip force feedback setup? --> <patch region="crom" bits="32" offset="0x029a0" value="0x60000000" /> <!-- skip force feedback setup? -->
<patch region="crom" bits="32" offset="0x02a0c" value="0x60000000" /> <patch region="crom" bits="32" offset="0x02a0c" value="0x60000000" />
<!-- Additional patches from MAME for test menu --> <!-- Additional patches from MAME for test menu -->
@ -2153,11 +2153,11 @@
<file offset="0xC00000" name="mpr-21378.24" crc32="0x1FCF715E" /> <file offset="0xC00000" name="mpr-21378.24" crc32="0x1FCF715E" />
</region> </region>
<!-- Force feedback controller prg --> <!-- Force feedback controller prg -->
<!--
<region name="ffb_program" stride="1" chunk_size="1"> <region name="ffb_program" stride="1" chunk_size="1">
<file offset="0" name="epr-21119.ic8" crc32="0x65082B14" /> <file offset="0" name="epr-21119.ic8" crc32="0x65082B14" />
</region> </region>
-->
</roms> </roms>
</game> </game>
@ -2184,10 +2184,14 @@
<!--<patch region="crom" bits="32" offset="0xf6dd0" value="0x60000000" /> --> <!-- from MAME --> <!--<patch region="crom" bits="32" offset="0xf6dd0" value="0x60000000" /> --> <!-- from MAME -->
<!-- Spindizzi notes : mimic patch from Bart's patch in swtrilgy to prevent game to be slow--> <!-- Spindizzi notes : mimic patch from Bart's patch in swtrilgy to prevent game to be slow-->
<!-- instead of patch we can make hack timing in code --> <!-- instead of patch we can make hack timing in code -->
<patch region="crom" bits="32" offset="0x043dc" value="0x48000090" /> <!--<patch region="crom" bits="32" offset="0x043dc" value="0x48000090" />--> <!-- no more needed if ffb lever rom loaded -->
<patch region="crom" bits="32" offset="0x029a0" value="0x60000000" /> <patch region="crom" bits="32" offset="0x029a0" value="0x60000000" />
<patch region="crom" bits="32" offset="0x02a0c" value="0x60000000" /> <patch region="crom" bits="32" offset="0x02a0c" value="0x60000000" />
<patch region="crom" bits="32" offset="0xf0dd4" value="0x60000000" /> <!--<patch region="crom" bits="32" offset="0xf0dd4" value="0x60000000" />--> <!-- no more needed if ffb lever rom loaded -->
<!-- Additional patches from MAME for test menu -->
<patch region="crom" bits="32" offset="0xf76f8" value="0x60000000" /> <!-- unemulated JTAG stuff -->
<patch region="crom" bits="32" offset="0xf76fc" value="0x60000000" /> <!-- "" -->
<patch region="crom" bits="32" offset="0xf7700" value="0x60000000" /> <!-- "" -->
</patches> </patches>
<region name="crom" stride="8" chunk_size="2" byte_swap="true"> <region name="crom" stride="8" chunk_size="2" byte_swap="true">
<file offset="0" name="epr-21382.20" crc32="0x0B9C44A0" /> <file offset="0" name="epr-21382.20" crc32="0x0B9C44A0" />

View file

@ -254,8 +254,10 @@ void CDriveBoard::Reset(void)
m_adcPortRead = 0; m_adcPortRead = 0;
m_adcPortBit = 0; m_adcPortBit = 0;
m_port42Out = 0; m_port42Out = 0;
m_port45Out = 0;
m_port46Out = 0; m_port46Out = 0;
m_prev42Out = 0; m_prev42Out = 0;
m_prev45Out = 0;
m_prev46Out = 0; m_prev46Out = 0;
m_initState = 0; m_initState = 0;
@ -266,6 +268,7 @@ void CDriveBoard::Reset(void)
m_uncenterVal2 = 0; m_uncenterVal2 = 0;
m_lastConstForce = 0; m_lastConstForce = 0;
m_lastConstForceY = 0;
m_lastSelfCenter = 0; m_lastSelfCenter = 0;
m_lastFriction = 0; m_lastFriction = 0;
m_lastVibrate = 0; m_lastVibrate = 0;
@ -496,32 +499,29 @@ UINT8 CDriveBoard::IORead8(UINT32 portNum)
UINT8 adcVal; UINT8 adcVal;
switch (portNum) switch (portNum)
{ {
case 32: // DIP 1 value case 0x20: // DIP 1 value
return m_dip1; return m_dip1;
case 33: // DIP 2 value case 0x21: // DIP 2 value
return m_dip2; return m_dip2;
case 36: // ADC channel 1 - not connected case 0x24: // ADC channel 1 - Y analog axis for joystick
case 37: // ADC channel 2 - steering wheel position (0x00 = full left, 0x80 = center, 0xFF = full right) case 0x25: // ADC channel 2 - steering wheel position (0x00 = full left, 0x80 = center, 0xFF = full right) and X analog axis for joystick
case 38: // ADC channel 3 - cockpit bank position (deluxe cabinets) (0x00 = full left, 0x80 = center, 0xFF = full right) case 0x26: // ADC channel 3 - cockpit bank position (deluxe cabinets) (0x00 = full left, 0x80 = center, 0xFF = full right)
case 39: // ADC channel 4 - not connected case 0x27: // ADC channel 4 - not connected
if (portNum == m_adcPortRead && m_adcPortBit-- > 0) if (portNum == m_adcPortRead && m_adcPortBit-- > 0)
{ {
switch (portNum) switch (portNum)
{ {
case 36: // Not connected case 0x24: // Y analog axis for joystick
adcVal = 0x00; adcVal = ReadADCChannel1();
break; break;
case 37: // Steering wheel for twin racing cabinets - TODO - check actual range of steering, suspect it is not really 0x00-0xFF case 0x25: // Steering wheel for twin racing cabinets - TODO - check actual range of steering, suspect it is not really 0x00-0xFF
if (m_initialized) adcVal = ReadADCChannel2();
adcVal = (UINT8)m_inputs->steering->value;
else
adcVal = 0x80; // If not initialized, return 0x80 so that wheel centering test does not fail
break; break;
case 38: // Cockpit bank position for deluxe racing cabinets case 0x26: // Cockpit bank position for deluxe racing cabinets
adcVal = 0x80; adcVal = ReadADCChannel3();
break; break;
case 39: // Not connected case 0x27: // Not connected
adcVal = 0x00; adcVal = ReadADCChannel4();
break; break;
default: default:
#ifdef DEBUG #ifdef DEBUG
@ -538,9 +538,9 @@ UINT8 CDriveBoard::IORead8(UINT32 portNum)
#endif #endif
return 0xFF; return 0xFF;
} }
case 40: // PPC command case 0x28: // PPC command
return m_dataSent; return m_dataSent;
case 44: // Encoder error reporting (kept at 0x00 for no error) case 0x2c: // Encoder error reporting (kept at 0x00 for no error)
// Bit 1 0 // Bit 1 0
// 0 0 = encoder okay, no error // 0 0 = encoder okay, no error
// 0 1 = encoder error 1 - overcurrent error // 0 1 = encoder error 1 - overcurrent error
@ -559,55 +559,66 @@ void CDriveBoard::IOWrite8(UINT32 portNum, UINT8 data)
{ {
switch (portNum) switch (portNum)
{ {
case 16: // Unsure? - single byte 0x03 sent at initialization, then occasionally writes 0x07 & 0xFA to port case 0x10: // Unsure? - single byte 0x03 sent at initialization, then occasionally writes 0x07 & 0xFA to port
return; return;
case 17: // Interrupt control case 0x11: // Interrupt control
if (data == 0x57) if (data == 0x57)
m_allowInterrupts = true; m_allowInterrupts = true;
else if (data == 0x53) // Strictly speaking 0x53 then 0x04 else if (data == 0x53) // Strictly speaking 0x53 then 0x04
m_allowInterrupts = false; m_allowInterrupts = false;
return; return;
case 28: // Unsure? - two bytes 0xFF, 0xFF sent at initialization only case 0x1c: // Unsure? - two bytes 0xFF, 0xFF sent at initialization only
case 29: // Unsure? - two bytes 0x0F, 0x17 sent at initialization only case 0x1d: // Unsure? - two bytes 0x0F, 0x17 sent at initialization only
case 30: // Unsure? - same as port 28 case 0x1e: // Unsure? - same as port 28
case 31: // Unsure? - same as port 31 case 0x1f: // Unsure? - same as port 31
return; return;
case 32: // Left digit of 7-segment display 1 case 0x20: // Left digit of 7-segment display 1
m_seg1Digit1 = data; m_seg1Digit1 = data;
return; return;
case 33: // Right digit of 7-segment display 1 case 0x21: // Right digit of 7-segment display 1
m_seg1Digit2 = data; m_seg1Digit2 = data;
return; return;
case 34: // Left digit of 7-segment display 2 case 0x22: // Left digit of 7-segment display 2
m_seg2Digit1 = data; m_seg2Digit1 = data;
return; return;
case 35: // Right digit of 7-segment display 2 case 0x23: // Right digit of 7-segment display 2
m_seg2Digit2 = data; m_seg2Digit2 = data;
return; return;
case 36: // ADC channel 1 control case 0x24: // ADC channel 1 control
case 37: // ADC channel 2 control case 0x25: // ADC channel 2 control
case 38: // ADC channel 3 control case 0x26: // ADC channel 3 control
case 39: // ADC channel 4 control case 0x27: // ADC channel 4 control
m_adcPortRead = portNum; m_adcPortRead = portNum;
m_adcPortBit = 8; m_adcPortBit = 8;
return; return;
case 41: // Reply for PPC case 0x29: // Reply for PPC
m_dataReceived = data; m_dataReceived = data;
if (data == 0xCC) if ((data == 0xCC && m_boardType == Wheel) || (data==0xCB && m_boardType == Joystick))
m_initialized = true; m_initialized = true;
return; return;
case 42: // Encoder motor data case 0x2a: // Encoder motor data (x axis)
m_port42Out = data; m_port42Out = data;
switch (m_boardType)
{
case Wheel:
ProcessEncoderCmd(); ProcessEncoderCmd();
break;
case Joystick:
ProcessEncoderCmdJoystick();
break;
}
return; return;
case 45: // Clutch/lamp control (deluxe cabinets) case 0x2d: // Clutch/lamp control (deluxe cabinets) ( or y axis)
m_port45Out = data;
if (m_boardType==Joystick)
ProcessEncoderCmdJoystick();
return; return;
case 46: // Encoder motor control case 0x2e: // Encoder motor control
m_port46Out = data; m_port46Out = data;
return; return;
case 240: // Unsure? - single byte 0xBB sent at initialization only case 0xf0: // Unsure? - single byte 0xBB sent at initialization only
return; return;
case 241: // Unsure? - single byte 0x4E sent regularly - some sort of watchdog? case 0xf1: // Unsure? - single byte 0x4E sent regularly - some sort of watchdog?
return; return;
default: default:
#ifdef DEBUG #ifdef DEBUG
@ -719,18 +730,148 @@ void CDriveBoard::ProcessEncoderCmd(void)
} }
} }
void CDriveBoard::ProcessEncoderCmdJoystick(void)
{
if (m_prev42Out != m_port42Out || m_prev46Out != m_port46Out || m_prev45Out != m_port45Out)
{
switch (m_port46Out)
{
case 0xEE:
// Apply constant force
if (m_port42Out > 0x7f) // X
{
SendConstantForce(2 * (m_port42Out - 0x7f));
}
else if (m_port42Out < 0x7f)
{
SendConstantForce(2 * (m_port42Out - 0x7f));
}
else
{
SendConstantForce(0);
}
if (m_port45Out > 0x7f) // Y
{
SendConstantForceY(-2 * (m_port45Out - 0x7f));
}
else if (m_port45Out < 0x7f)
{
SendConstantForceY(-2 * (m_port45Out - 0x7f));
}
else
{
SendConstantForceY(0);
}
if (m_port42Out == 0x7f && m_port45Out == 0x81)
{
SendSelfCenter(255);
}
else SendSelfCenter(0);
break;
case 0xFF:
// Stop all effects
if (m_port42Out == 0 || m_port45Out == 0)
SendStopAll();
break;
case 0xcc: // init
//42[0B] / 45[0A]
//42[0B] / 45[0B]
//42[FF] / 45[0B]
//42[FF] / 45[FF]
break;
case 0xdd: // init
// 42[FF] / 45[00]
// 42[FF] / 45[FF]
// 42[0A] / 45[00]
// 42[0A] / 45[0A]
break;
case 0xce:
// 42[7F] / 45[08]
// 42[7F] / 45[09]
// 42[7F] / 45[0A]
// 42[7F] / 45[0B]
// 42[7F] / 45[81]
if (m_port42Out == 0x7f && m_port45Out != 0x81) // X
{
SendConstantForce(2 * m_port45Out);
}
if (m_port42Out == 0x7f && m_port45Out == 0x81)
{
SendSelfCenter(255);
}
break;
case 0xec:
// 42[09] / 45[81]
// 42[2A] / 45[81]
// 42[1B] / 45[81]
// 42[7F] / 45[81]
if (m_port45Out == 0x81 && m_port42Out != 0x7f) // Y
{
SendConstantForceY(2 * m_port42Out);
}
if (m_port42Out == 0x7f && m_port45Out == 0x81)
{
SendSelfCenter(255);
}
break;
case 0x00: // init
// 42[FF] / 45[00]
// 42[FF] / 45[FF]
break;
case 0x99: // init
// 42[B0] / 45[B0]
// 42[80] / 45[B0]
// 42[80] / 45[80]
break;
default:
//printf("Unknown = 46 [%02X] / 42 [%02X] / 45 [%02X]\n", m_port46Out, m_port42Out, m_port45Out);
break;
}
m_prev42Out = m_port42Out;
m_prev46Out = m_port46Out;
m_prev45Out = m_port45Out;
}
}
void CDriveBoard::SendStopAll(void) void CDriveBoard::SendStopAll(void)
{ {
//printf(">> Stop All Effects\n"); //printf(">> Stop All Effects\n");
ForceFeedbackCmd ffCmd; ForceFeedbackCmd ffCmd;
ffCmd.id = FFStop; ffCmd.id = FFStop;
switch (m_boardType)
{
case Wheel:
m_inputs->steering->SendForceFeedbackCmd(ffCmd); m_inputs->steering->SendForceFeedbackCmd(ffCmd);
break;
case Joystick:
m_inputs->analogJoyX->SendForceFeedbackCmd(ffCmd);
m_inputs->analogJoyY->SendForceFeedbackCmd(ffCmd);
break;
}
m_lastConstForce = 0; m_lastConstForce = 0;
m_lastSelfCenter = 0; m_lastSelfCenter = 0;
m_lastFriction = 0; m_lastFriction = 0;
m_lastVibrate = 0; m_lastVibrate = 0;
m_lastConstForceY = 0;
} }
void CDriveBoard::SendConstantForce(INT8 val) void CDriveBoard::SendConstantForce(INT8 val)
@ -759,11 +900,32 @@ void CDriveBoard::SendConstantForce(INT8 val)
ForceFeedbackCmd ffCmd; ForceFeedbackCmd ffCmd;
ffCmd.id = FFConstantForce; ffCmd.id = FFConstantForce;
ffCmd.force = (float)val / (val >= 0 ? 127.0f : 128.0f); ffCmd.force = (float)val / (val >= 0 ? 127.0f : 128.0f);
switch (m_boardType)
{
case Wheel:
m_inputs->steering->SendForceFeedbackCmd(ffCmd); m_inputs->steering->SendForceFeedbackCmd(ffCmd);
break;
case Joystick:
m_inputs->analogJoyX->SendForceFeedbackCmd(ffCmd);
break;
}
m_lastConstForce = val; m_lastConstForce = val;
} }
void CDriveBoard::SendConstantForceY(INT8 val)
{
if (val == m_lastConstForceY)
return;
ForceFeedbackCmd ffCmd;
ffCmd.id = FFConstantForce;
ffCmd.force = (float)val / (val >= 0 ? 127.0f : 128.0f);
m_inputs->analogJoyY->SendForceFeedbackCmd(ffCmd);
m_lastConstForceY = val;
}
void CDriveBoard::SendSelfCenter(UINT8 val) void CDriveBoard::SendSelfCenter(UINT8 val)
{ {
if (val == m_lastSelfCenter) if (val == m_lastSelfCenter)
@ -778,11 +940,22 @@ void CDriveBoard::SendSelfCenter(UINT8 val)
ForceFeedbackCmd ffCmd; ForceFeedbackCmd ffCmd;
ffCmd.id = FFSelfCenter; ffCmd.id = FFSelfCenter;
ffCmd.force = (float)val / 255.0f; ffCmd.force = (float)val / 255.0f;
switch (m_boardType)
{
case Wheel:
m_inputs->steering->SendForceFeedbackCmd(ffCmd); m_inputs->steering->SendForceFeedbackCmd(ffCmd);
break;
case Joystick:
m_inputs->analogJoyX->SendForceFeedbackCmd(ffCmd);
m_inputs->analogJoyY->SendForceFeedbackCmd(ffCmd);
break;
}
m_lastSelfCenter = val; m_lastSelfCenter = val;
} }
void CDriveBoard::SendFriction(UINT8 val) void CDriveBoard::SendFriction(UINT8 val)
{ {
if (val == m_lastFriction) if (val == m_lastFriction)
@ -821,6 +994,59 @@ void CDriveBoard::SendVibrate(UINT8 val)
m_lastVibrate = val; m_lastVibrate = val;
} }
uint8_t CDriveBoard::ReadADCChannel1()
{
switch (m_boardType)
{
case Wheel:
return 0x00;
break;
case Joystick:
if (m_initialized)
return (UINT8)m_inputs->analogJoyY->value;
else
return 0x80; // If not initialized, return 0x80 so that ffb centering test does not fail
break;
}
}
uint8_t CDriveBoard::ReadADCChannel2()
{
switch (m_boardType)
{
case Wheel:
if (m_initialized)
return (UINT8)m_inputs->steering->value;
else
return 0x80; // If not initialized, return 0x80 so that wheel centering test does not fail
break;
case Joystick:
if (m_initialized)
return (UINT8)m_inputs->analogJoyX->value;
else
return 0x80; // If not initialized, return 0x80 so that ffb centering test does not fail
break;
}
}
uint8_t CDriveBoard::ReadADCChannel3()
{
return 0x80;
}
uint8_t CDriveBoard::ReadADCChannel4()
{
switch (m_boardType)
{
case Wheel:
return 0x00;
break;
case Joystick:
return 0x80;
break;
}
}
CDriveBoard::CDriveBoard(const Util::Config::Node &config) CDriveBoard::CDriveBoard(const Util::Config::Node &config)
: m_config(config), : m_config(config),
m_attached(false), m_attached(false),

View file

@ -36,6 +36,14 @@
class CDriveBoard : public IBus class CDriveBoard : public IBus
{ {
public: public:
enum BoardType
{
Wheel=0,
Joystick
};
BoardType m_boardType;
/* /*
* IsAttached(void): * IsAttached(void):
* *
@ -281,9 +289,11 @@ private:
UINT8 m_port42Out; // Last value sent to Z80 I/O port 42 (encoder motor data) UINT8 m_port42Out; // Last value sent to Z80 I/O port 42 (encoder motor data)
UINT8 m_port46Out; // Last value sent to Z80 I/O port 46 (encoder motor control) UINT8 m_port46Out; // Last value sent to Z80 I/O port 46 (encoder motor control)
UINT8 m_port45Out; // Last value sent to Z80 I/O port 42 (up-down motor data)
UINT8 m_prev42Out; // Previous value sent to Z80 I/O port 42 UINT8 m_prev42Out; // Previous value sent to Z80 I/O port 42
UINT8 m_prev46Out; // Previous value sent to Z80 I/O port 46 UINT8 m_prev46Out; // Previous value sent to Z80 I/O port 46
UINT8 m_prev45Out; // Previous value sent to Z80 I/O port 45 (up-down)
UINT8 m_uncenterVal1; // First part of pending uncenter command UINT8 m_uncenterVal1; // First part of pending uncenter command
UINT8 m_uncenterVal2; // Second part of pending uncenter command UINT8 m_uncenterVal2; // Second part of pending uncenter command
@ -299,6 +309,7 @@ private:
// Feedback state // Feedback state
INT8 m_lastConstForce; // Last constant force command sent INT8 m_lastConstForce; // Last constant force command sent
INT8 m_lastConstForceY; // Last constant force command sent y axis
UINT8 m_lastSelfCenter; // Last self center command sent UINT8 m_lastSelfCenter; // Last self center command sent
UINT8 m_lastFriction; // Last friction command sent UINT8 m_lastFriction; // Last friction command sent
UINT8 m_lastVibrate; // Last vibrate command sent UINT8 m_lastVibrate; // Last vibrate command sent
@ -313,15 +324,27 @@ private:
void ProcessEncoderCmd(void); void ProcessEncoderCmd(void);
void ProcessEncoderCmdJoystick(void);
void SendStopAll(void); void SendStopAll(void);
void SendConstantForce(INT8 val); void SendConstantForce(INT8 val);
void SendConstantForceY(INT8 val);
void SendSelfCenter(UINT8 val); void SendSelfCenter(UINT8 val);
void SendFriction(UINT8 val); void SendFriction(UINT8 val);
void SendVibrate(UINT8 val); void SendVibrate(UINT8 val);
uint8_t ReadADCChannel1();
uint8_t ReadADCChannel2();
uint8_t ReadADCChannel3();
uint8_t ReadADCChannel4();
}; };
#endif // INCLUDED_DRIVEBOARD_H #endif // INCLUDED_DRIVEBOARD_H

View file

@ -2932,6 +2932,7 @@ bool CModel3::LoadGame(const Game &game, const ROMSet &rom_set)
rom_set.get_rom("mpeg_program").CopyTo(dsbROM, 128*1024); rom_set.get_rom("mpeg_program").CopyTo(dsbROM, 128*1024);
rom_set.get_rom("mpeg_music").CopyTo(mpegROM, 16*0x100000); rom_set.get_rom("mpeg_music").CopyTo(mpegROM, 16*0x100000);
rom_set.get_rom("driveboard_program").CopyTo(driveROM, 64*1024); rom_set.get_rom("driveboard_program").CopyTo(driveROM, 64*1024);
rom_set.get_rom("ffb_program").CopyTo(driveROM, 64 * 1024);
// Convert PowerPC and 68K ROMs to little endian words // Convert PowerPC and 68K ROMs to little endian words
Util::FlipEndian32(crom, 8*0x100000 + 128*0x100000); Util::FlipEndian32(crom, 8*0x100000 + 128*0x100000);
@ -3030,6 +3031,15 @@ bool CModel3::LoadGame(const Game &game, const ROMSet &rom_set)
{ {
if (DriveBoard.Init(driveROM)) if (DriveBoard.Init(driveROM))
return FAIL; return FAIL;
else
DriveBoard.m_boardType = DriveBoard.Wheel;
}
else if (rom_set.get_rom("ffb_program").size)
{
if (DriveBoard.Init(driveROM))
return FAIL;
else
DriveBoard.m_boardType = DriveBoard.Joystick;
} }
else else
DriveBoard.Init(NULL); DriveBoard.Init(NULL);
@ -3046,6 +3056,8 @@ bool CModel3::LoadGame(const Game &game, const ROMSet &rom_set)
extra_hw.insert(Util::Format() << "Digital Sound Board (Type " << game.mpeg_board << ")"); extra_hw.insert(Util::Format() << "Digital Sound Board (Type " << game.mpeg_board << ")");
if (rom_set.get_rom("driveboard_program").size) if (rom_set.get_rom("driveboard_program").size)
extra_hw.insert("Drive Board"); extra_hw.insert("Drive Board");
if (rom_set.get_rom("ffb_program").size)
extra_hw.insert("Joystick FFB Board");
if (game.encryption_key) if (game.encryption_key)
extra_hw.insert("Security Board"); extra_hw.insert("Security Board");
if (netboard_present.compare("true")==0) if (netboard_present.compare("true")==0)