new naomi network protocol. vblank event. initd rumble support

new vblank event, used by cheats, lua and naomi net
new udp net protocol for naomi. rx/tx on vblank on emu thread.
input: rumble power configurable (Issue #158)
ui: rumble intensity slider, enable/disable upnp
aica: hook to consume midi out
decode midi out to simulate rumble for initd
upnp can now be disabled
This commit is contained in:
Flyinghead 2022-03-27 15:23:21 +02:00
parent 55e613fae5
commit 6c38295d62
32 changed files with 664 additions and 813 deletions

View File

@ -17,6 +17,7 @@
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "option.h"
#include "network/naomi_network.h"
namespace config {
@ -117,7 +118,9 @@ Option<bool> NetworkEnable("Enable", false, "network");
Option<bool> ActAsServer("ActAsServer", false, "network");
OptionString DNS("DNS", "46.101.91.123", "network");
OptionString NetworkServer("server", "", "network");
Option<int> LocalPort("LocalPort", NaomiNetwork::SERVER_PORT, "network");
Option<bool> EmulateBBA("EmulateBBA", false, "network");
Option<bool> EnableUPnP("EnableUPnP", true, "network");
Option<bool> GGPOEnable("GGPO", false, "network");
Option<int> GGPODelay("GGPODelay", 0, "network");
Option<bool> NetworkStats("Stats", true, "network");

View File

@ -462,7 +462,9 @@ extern Option<bool> NetworkEnable;
extern Option<bool> ActAsServer;
extern OptionString DNS;
extern OptionString NetworkServer;
extern Option<int> LocalPort;
extern Option<bool> EmulateBBA;
extern Option<bool> EnableUPnP;
extern Option<bool> GGPOEnable;
extern Option<int> GGPODelay;
extern Option<bool> NetworkStats;

View File

@ -25,6 +25,7 @@
#include "reios/reios.h"
#include "cfg/cfg.h"
#include "cfg/ini.h"
#include "emulator.h"
const WidescreenCheat CheatManager::widescreen_cheats[] =
{
@ -315,6 +316,20 @@ const WidescreenCheat CheatManager::naomi_widescreen_cheats[] =
};
CheatManager cheatManager;
static void vblankCallback(Event event, void *param)
{
((CheatManager *)param)->apply();
}
void CheatManager::setActive(bool active)
{
this->active = active;
if (active || widescreen_cheat != nullptr)
EventManager::listen(Event::VBlank, vblankCallback, this);
else
EventManager::unlisten(Event::VBlank, vblankCallback, this);
}
void CheatManager::loadCheatFile(const std::string& filename)
{
#ifndef LIBRETRO
@ -360,7 +375,7 @@ void CheatManager::loadCheatFile(const std::string& filename)
if (cheat.type != Cheat::Type::disabled)
cheats.push_back(cheat);
}
active = !cheats.empty();
setActive(!cheats.empty());
INFO_LOG(COMMON, "%d cheats loaded", (int)cheats.size());
cfgSaveStr("cheats", gameId, filename);
#endif
@ -368,10 +383,11 @@ void CheatManager::loadCheatFile(const std::string& filename)
void CheatManager::reset(const std::string& gameId)
{
widescreen_cheat = nullptr;
if (this->gameId != gameId)
{
cheats.clear();
active = false;
setActive(false);
this->gameId = gameId;
#ifndef LIBRETRO
std::string cheatFile = cfgLoadStr("cheats", gameId, "");
@ -380,51 +396,51 @@ void CheatManager::reset(const std::string& gameId)
#endif
if (gameId == "VF4 FINAL TUNED JAPAN")
{
active = true;
setActive(true);
cheats.emplace_back(Cheat::Type::setValue, "Skip DIMM version check", true, 16, 0x000205c6, 9);
}
}
widescreen_cheat = nullptr;
if (!config::WidescreenGameHacks)
return;
if (settings.platform.isConsole())
if (config::WidescreenGameHacks)
{
for (int i = 0; widescreen_cheats[i].game_id != nullptr; i++)
if (settings.platform.isConsole())
{
if (!strcmp(gameId.c_str(), widescreen_cheats[i].game_id)
&& (widescreen_cheats[i].area_or_version == nullptr
|| !strncmp(ip_meta.area_symbols, widescreen_cheats[i].area_or_version, sizeof(ip_meta.area_symbols))
|| !strncmp(ip_meta.product_version, widescreen_cheats[i].area_or_version, sizeof(ip_meta.product_version))))
for (int i = 0; widescreen_cheats[i].game_id != nullptr; i++)
{
widescreen_cheat = &widescreen_cheats[i];
NOTICE_LOG(COMMON, "Applying widescreen hack to game %s", gameId.c_str());
break;
if (!strcmp(gameId.c_str(), widescreen_cheats[i].game_id)
&& (widescreen_cheats[i].area_or_version == nullptr
|| !strncmp(ip_meta.area_symbols, widescreen_cheats[i].area_or_version, sizeof(ip_meta.area_symbols))
|| !strncmp(ip_meta.product_version, widescreen_cheats[i].area_or_version, sizeof(ip_meta.product_version))))
{
widescreen_cheat = &widescreen_cheats[i];
NOTICE_LOG(COMMON, "Applying widescreen hack to game %s", gameId.c_str());
break;
}
}
}
}
else
{
std::string romName = get_file_basename(settings.content.path);
size_t folder_pos = get_last_slash_pos(romName);
if (folder_pos != std::string::npos)
romName = romName.substr(folder_pos + 1);
else
{
std::string romName = get_file_basename(settings.content.path);
size_t folder_pos = get_last_slash_pos(romName);
if (folder_pos != std::string::npos)
romName = romName.substr(folder_pos + 1);
for (int i = 0; naomi_widescreen_cheats[i].game_id != nullptr; i++)
{
if (!strcmp(gameId.c_str(), naomi_widescreen_cheats[i].game_id)
&& (naomi_widescreen_cheats[i].area_or_version == nullptr
|| !strcmp(romName.c_str(), naomi_widescreen_cheats[i].area_or_version)))
for (int i = 0; naomi_widescreen_cheats[i].game_id != nullptr; i++)
{
widescreen_cheat = &naomi_widescreen_cheats[i];
NOTICE_LOG(COMMON, "Applying widescreen hack to game %s", gameId.c_str());
break;
if (!strcmp(gameId.c_str(), naomi_widescreen_cheats[i].game_id)
&& (naomi_widescreen_cheats[i].area_or_version == nullptr
|| !strcmp(romName.c_str(), naomi_widescreen_cheats[i].area_or_version)))
{
widescreen_cheat = &naomi_widescreen_cheats[i];
NOTICE_LOG(COMMON, "Applying widescreen hack to game %s", gameId.c_str());
break;
}
}
}
if (widescreen_cheat != nullptr)
for (size_t i = 0; i < ARRAY_SIZE(widescreen_cheat->addresses) && widescreen_cheat->addresses[i] != 0; i++)
verify(widescreen_cheat->addresses[i] < RAM_SIZE);
}
if (widescreen_cheat == nullptr)
return;
for (size_t i = 0; i < ARRAY_SIZE(widescreen_cheat->addresses) && widescreen_cheat->addresses[i] != 0; i++)
verify(widescreen_cheat->addresses[i] < RAM_SIZE);
setActive(active);
}
u32 CheatManager::readRam(u32 addr, u32 bits)
@ -756,7 +772,7 @@ void CheatManager::addGameSharkCheat(const std::string& name, const std::string&
throw FlycastException("Unsupported cheat type");
}
}
active = !cheats.empty();
setActive(!cheats.empty());
#ifndef LIBRETRO
std::string path = cfgLoadStr("cheats", gameId, "");
if (path == "")

View File

@ -81,6 +81,7 @@ public:
private:
u32 readRam(u32 addr, u32 bits);
void writeRam(u32 addr, u32 value, u32 bits);
void setActive(bool active);
static const WidescreenCheat widescreen_cheats[];
static const WidescreenCheat naomi_widescreen_cheats[];

View File

@ -38,7 +38,6 @@
#include "hw/mem/mem_watch.h"
#include "network/net_handshake.h"
#include "rend/gui.h"
#include "lua/lua.h"
#include "network/naomi_network.h"
#include "serialize.h"
#include "hw/pvr/pvr.h"
@ -831,7 +830,7 @@ bool Emulator::render()
void Emulator::vblank()
{
lua::vblank();
EventManager::event(Event::VBlank);
// Time out if a frame hasn't been rendered for 50 ms
if (sh4_sched_now64() - startTime <= 10000000)
return;

View File

@ -45,6 +45,7 @@ enum class Event {
Resume,
Terminate,
LoadState,
VBlank,
};
class EventManager

View File

@ -13,6 +13,7 @@ void WriteMem_aica_reg(u32 addr,u32 data,u32 sz);
void aica_Init();
void aica_Reset(bool hard);
void aica_Term();
void aica_setMidiReceiver(void (*handler)(u8 data));
void aica_sb_Init();
void aica_sb_Reset(bool hard);

View File

@ -127,6 +127,8 @@ static const s32 qtable[32] = {
0x1C00,0x1D00,0x1E00,0x1F00
};
static void (*midiReceiver)(u8 data);
//Remove the fractional part by chopping..
static SampleType FPs(SampleType a, int bits) {
return a >> bits;
@ -779,7 +781,6 @@ struct ChannelEx
FEG.ReleaseRate = FEG_SPS[EG_EffRate(base_rate, ccd->FRR)];
}
//WHEE :D!
void RegWrite(u32 offset, int size)
{
switch(offset)
@ -1024,7 +1025,7 @@ void StreamStep(ChannelEx* ch)
else
{
CA = ch->loop.LSA;
key_printf("[%d]LPCTL : Looping LSA %x LEA %x", ch->ChannelNumber, ch->loop.LSA, ch->loop.LEA);
key_printf("[%d]LPCTL : Looping LSA %x LEA %x AEG %x", ch->ChannelNumber, ch->loop.LSA, ch->loop.LEA, ch->AEG.GetValue());
}
}
@ -1040,52 +1041,60 @@ void StreamStep(ChannelEx* ch)
}
template<s32 ALFOWS>
enum class LFOType
{
Sawtooth,
Square,
Triangle,
Random
};
template<LFOType Type>
void CalcAlfo(ChannelEx* ch)
{
u32 rv;
switch(ALFOWS)
switch(Type)
{
case 0: // Sawtooth
case LFOType::Sawtooth:
rv=ch->lfo.state;
break;
case 1: // Square
case LFOType::Square:
rv=ch->lfo.state&0x80?255:0;
break;
case 2: // Triangle
case LFOType::Triangle:
rv=(ch->lfo.state&0x7f)^(ch->lfo.state&0x80 ? 0x7F:0);
rv<<=1;
break;
case 3:// Random ! .. not :p
case LFOType::Random: // ... not so much
rv=(ch->lfo.state>>3)^(ch->lfo.state<<3)^(ch->lfo.state&0xE3);
break;
}
ch->lfo.alfo=rv>>ch->lfo.alfo_shft;
}
template<s32 PLFOWS>
template<LFOType Type>
void CalcPlfo(ChannelEx* ch)
{
u32 rv;
switch(PLFOWS)
switch(Type)
{
case 0: // sawtooth
case LFOType::Sawtooth:
rv = ch->lfo.state;
break;
case 1: // square
case LFOType::Square:
rv = ch->lfo.state & 0x80 ? 0xff : 0;
break;
case 2: // triangle
case LFOType::Triangle:
rv = (ch->lfo.state & 0x7f) ^ (ch->lfo.state & 0x80 ? 0x7F : 0);
rv <<= 1;
break;
case 3:// random ! .. not :p
case LFOType::Random:
rv = (ch->lfo.state >> 3) ^ (ch->lfo.state << 3) ^ (ch->lfo.state & 0xE3);
break;
}
@ -1220,25 +1229,25 @@ static void staticinitialise()
STREAM_INITAL_STEP_LUT[3]=&StepDecodeSampleInitial<3>;
STREAM_INITAL_STEP_LUT[4]=&StepDecodeSampleInitial<-1>;
AEG_STEP_LUT[0]=&AegStep<0>;
AEG_STEP_LUT[1]=&AegStep<1>;
AEG_STEP_LUT[2]=&AegStep<2>;
AEG_STEP_LUT[3]=&AegStep<3>;
AEG_STEP_LUT[EG_Attack] = &AegStep<EG_Attack>;
AEG_STEP_LUT[EG_Decay1] = &AegStep<EG_Decay1>;
AEG_STEP_LUT[EG_Decay2] = &AegStep<EG_Decay2>;
AEG_STEP_LUT[EG_Release] = &AegStep<EG_Release>;
FEG_STEP_LUT[0]=&FegStep<0>;
FEG_STEP_LUT[1]=&FegStep<1>;
FEG_STEP_LUT[2]=&FegStep<2>;
FEG_STEP_LUT[3]=&FegStep<3>;
FEG_STEP_LUT[EG_Attack] = &FegStep<EG_Attack>;
FEG_STEP_LUT[EG_Decay1] = &FegStep<EG_Decay1>;
FEG_STEP_LUT[EG_Decay2] = &FegStep<EG_Decay2>;
FEG_STEP_LUT[EG_Release] = &FegStep<EG_Release>;
ALFOWS_CALC[0]=&CalcAlfo<0>;
ALFOWS_CALC[1]=&CalcAlfo<1>;
ALFOWS_CALC[2]=&CalcAlfo<2>;
ALFOWS_CALC[3]=&CalcAlfo<3>;
ALFOWS_CALC[(int)LFOType::Sawtooth] = &CalcAlfo<LFOType::Sawtooth>;
ALFOWS_CALC[(int)LFOType::Square] = &CalcAlfo<LFOType::Square>;
ALFOWS_CALC[(int)LFOType::Triangle] = &CalcAlfo<LFOType::Triangle>;
ALFOWS_CALC[(int)LFOType::Random] = &CalcAlfo<LFOType::Random>;
PLFOWS_CALC[0]=&CalcPlfo<0>;
PLFOWS_CALC[1]=&CalcPlfo<1>;
PLFOWS_CALC[2]=&CalcPlfo<2>;
PLFOWS_CALC[3]=&CalcPlfo<3>;
PLFOWS_CALC[(int)LFOType::Sawtooth] = &CalcPlfo<LFOType::Sawtooth>;
PLFOWS_CALC[(int)LFOType::Square] = &CalcPlfo<LFOType::Square>;
PLFOWS_CALC[(int)LFOType::Triangle] = &CalcPlfo<LFOType::Triangle>;
PLFOWS_CALC[(int)LFOType::Random] = &CalcPlfo<LFOType::Random>;
}
ChannelEx ChannelEx::Chans[64];
@ -1312,6 +1321,7 @@ void sgc_Init()
beepCounter = 0;
dsp::init();
midiReceiver = nullptr;
}
void sgc_Term()
@ -1375,6 +1385,10 @@ void WriteCommonReg8(u32 reg,u32 data)
state.RBP = (CommonData->RBP * 2048) & ARAM_MASK;
state.dirty = true;
}
else if (reg == 0x280c) { // MOBUF
if (midiReceiver != nullptr)
midiReceiver(data);
}
}
void vmuBeep(int on, int period)
@ -1766,3 +1780,7 @@ void channel_deserialize(Deserializer& deser)
deser.skip(4); // samples_gen
}
}
void aica_setMidiReceiver(void (*handler)(u8 data)) {
midiReceiver = handler;
}

View File

@ -9,12 +9,12 @@
#include "hw/maple/maple_cfg.h"
#include "hw/sh4/sh4_sched.h"
#include "hw/sh4/modules/dmac.h"
#include "hw/aica/aica_if.h"
#include "naomi.h"
#include "naomi_cart.h"
#include "naomi_regs.h"
#include "naomi_m3comm.h"
#include "network/naomi_network.h"
#include "serialize.h"
//#define NAOMI_COMM
@ -46,6 +46,9 @@ P-Z (0x50-0x5A)
static u8 BSerial[]="\xB7"/*CRC1*/"\x19"/*CRC2*/"0123234437897584372973927387463782196719782697849162342198671923649";
static u8 GSerial[]="\xB7"/*CRC1*/"\x19"/*CRC2*/"0123234437897584372973927387463782196719782697849162342198671923649";
static u8 midiTxBuf[4];
static u32 midiTxBufIndex;
static unsigned int ShiftCRC(unsigned int CRC,unsigned int rounds)
{
const unsigned int Magic=0x10210000;
@ -537,7 +540,6 @@ void naomi_reg_Term()
}
#endif
m3comm.closeNetwork();
naomiNetwork.terminate();
}
void naomi_reg_Reset(bool hard)
@ -570,7 +572,6 @@ void naomi_reg_Reset(bool hard)
reg_dimm_parameterh = 0;
reg_dimm_status = 0x11;
m3comm.closeNetwork();
naomiNetwork.terminate();
if (hard)
naomi_cart_Close();
}
@ -686,6 +687,8 @@ void naomi_Serialize(Serializer& ser)
ser << aw_maple_devs;
ser << coin_chute_time;
ser << aw_ram_test_skipped;
ser << midiTxBuf;
ser << midiTxBufIndex;
// TODO serialize m3comm?
}
void naomi_Deserialize(Deserializer& deser)
@ -727,4 +730,33 @@ void naomi_Deserialize(Deserializer& deser)
deser >> coin_chute_time;
deser >> aw_ram_test_skipped;
}
if (deser.version() >= Deserializer::V27)
{
deser >> midiTxBuf;
deser >> midiTxBufIndex;
}
else
{
midiTxBufIndex = 0;
}
}
static void initFFBMidiReceiver(u8 data)
{
if (data & 0x80)
midiTxBufIndex = 0;
midiTxBuf[midiTxBufIndex] = data;
if (midiTxBufIndex == 3 && ((midiTxBuf[0] ^ midiTxBuf[1] ^ midiTxBuf[2]) & 0x7f) == midiTxBuf[3])
{
// decoding from FFB Arcade Plugin (by Boomslangnz)
// https://github.com/Boomslangnz/FFBArcadePlugin/blob/master/Game%20Files/Demul.cpp
if (midiTxBuf[0] == 0x85 && midiTxBuf[1] == 0x3f)
MapleConfigMap::UpdateVibration(0, std::max(0.f, (float)(midiTxBuf[2] - 1) / 24.f), 0.f, 5);
}
midiTxBufIndex = (midiTxBufIndex + 1) % ARRAY_SIZE(midiTxBuf);
}
void initdFFBInit()
{
aica_setMidiReceiver(initFFBMidiReceiver);
}

View File

@ -25,3 +25,5 @@ extern u32 reg_dimm_offsetl;
extern u32 reg_dimm_parameterl;
extern u32 reg_dimm_parameterh;
extern u32 reg_dimm_status;
void initdFFBInit();

View File

@ -581,6 +581,7 @@ void naomi_cart_LoadRom(const char* file, LoadProgress *progress)
|| gameId == "INITIAL D CYCRAFT")
{
card_reader::initialDCardReader.init();
initdFFBInit();
}
}
else

View File

@ -18,69 +18,86 @@
You should have received a copy of the GNU General Public License
along with flycast. If not, see <https://www.gnu.org/licenses/>.
*/
//
// Optical communication board (837-13691)
// Ring topology
// 10 Mbps
// Max packet size 0x4000
//
#include "naomi_m3comm.h"
#include "naomi_regs.h"
#include "hw/holly/sb.h"
#include "hw/sh4/sh4_mem.h"
#include "network/naomi_network.h"
#include "emulator.h"
#include <chrono>
#include <memory>
constexpr u16 COMM_CTRL_CPU_RAM = 1 << 0;
constexpr u16 COMM_CTRL_RESET = 1 << 5; // rising edge
constexpr u16 COMM_CTRL_G1DMA = 1 << 14; // active low
struct CommBoardStat
{
u16 transmode; // communication mode (0: master, positive value: slave)
u16 totalnode; // Total number of nodes (same value is entered in upper and lower 8 bits)
u16 nodeID; // Local node ID (the same value is entered in the upper and lower 8 bits)
u16 transcnt; // counter (value increases by 1 per frame)
u16 cts; // CTS timer value (for debugging)
u16 dma_rx_addr; // DMA receive address (for debugging)
u16 dma_rx_size; // DMA receive size (for debugging)
u16 dma_tx_addr; // DMA transmit address (for debugging)
u16 dma_tx_size; // DMA transmission size (for debugging)
u16 dummy[7];
};
static inline u16 swap16(u16 w)
{
return (w >> 8) | (w << 8);
}
static void vblankCallback(Event event, void *param) {
((NaomiM3Comm *)param)->vblank();
}
void NaomiM3Comm::closeNetwork()
{
network_stopping = true;
EventManager::unlisten(Event::VBlank, vblankCallback, this);
naomiNetwork.shutdown();
if (thread && thread->joinable())
thread->join();
}
void NaomiM3Comm::connectNetwork()
{
gui_display_notification("Network started", 5000);
packet_number = 0;
if (naomiNetwork.syncNetwork())
{
slot_count = naomiNetwork.slotCount();
slot_id = naomiNetwork.slotId();
connectedState(true);
}
else
{
connectedState(false);
network_stopping = true;
naomiNetwork.shutdown();
}
slot_count = naomiNetwork.getSlotCount();
slot_id = naomiNetwork.getSlotId();
connectedState(true);
EventManager::listen(Event::VBlank, vblankCallback, this);
}
void NaomiM3Comm::receiveNetwork()
bool NaomiM3Comm::receiveNetwork()
{
const u32 slot_size = swap16(*(u16*)&m68k_ram[0x204]);
const u32 packet_size = slot_size * slot_count;
std::unique_ptr<u8[]> buf(new u8[packet_size]);
if (naomiNetwork.receive(buf.get(), packet_size))
{
packet_number += slot_count - 1;
*(u16*)&comm_ram[6] = swap16(packet_number);
std::unique_lock<std::mutex> lock(mem_mutex);
memcpy(&comm_ram[0x100 + slot_size], buf.get(), packet_size);
}
u16 packetNumber;
if (!naomiNetwork.receive(buf.get(), packet_size, &packetNumber))
return false;
*(u16*)&comm_ram[6] = swap16(packetNumber);
memcpy(&comm_ram[0x100 + slot_size], buf.get(), packet_size);
return true;
}
void NaomiM3Comm::sendNetwork()
{
if (naomiNetwork.hasToken())
{
const u32 packet_size = swap16(*(u16*)&m68k_ram[0x204]) * slot_count;
std::unique_lock<std::mutex> lock(mem_mutex);
naomiNetwork.send(&comm_ram[0x100], packet_size);
packet_number++;
*(u16*)&comm_ram[6] = swap16(packet_number);
}
const u32 packet_size = swap16(*(u16*)&m68k_ram[0x204]) * slot_count;
naomiNetwork.send(&comm_ram[0x100], packet_size, packet_number);
packet_number++;
}
u32 NaomiM3Comm::ReadMem(u32 address, u32 size)
@ -98,13 +115,13 @@ u32 NaomiM3Comm::ReadMem(u32 address, u32 size)
case NAOMI_COMM2_DATA_addr & 255:
{
u16 value;
if (comm_ctrl & 1)
if (comm_ctrl & COMM_CTRL_CPU_RAM)
value = *(u16*)&m68k_ram[comm_offset];
else
// TODO u16 *commram = (u16*)membank("comm_ram")->base();
value = *(u16*)&comm_ram[comm_offset];
value = swap16(value);
DEBUG_LOG(NAOMI, "NAOMI_COMM2_DATA %s read @ %04x: %x", (comm_ctrl & 1) ? "m68k ram" : "comm ram", comm_offset, value);
DEBUG_LOG(NAOMI, "NAOMI_COMM2_DATA %s read @ %04x: %x", (comm_ctrl & COMM_CTRL_CPU_RAM) ? "m68k ram" : "comm ram", comm_offset, value);
comm_offset += 2;
return value;
}
@ -127,7 +144,7 @@ void NaomiM3Comm::connectedState(bool success)
{
if (!success)
return;
verify(slot_count >= 2);
memset(&comm_ram[0xf000], 0, 16);
comm_ram[0xf000] = 1;
comm_ram[0xf001] = 1;
@ -136,39 +153,16 @@ void NaomiM3Comm::connectedState(bool success)
u32 slot_size = swap16(*(u16*)&m68k_ram[0x204]);
memset(&comm_ram[0], 0, 32);
// 80000
comm_ram[0] = 0;
comm_ram[1] = slot_id == 0 ? 0 : 1;
// 80002
comm_ram[2] = 0x01;
comm_ram[3] = 0x01;
// 80004
if (slot_id == 0)
{
comm_ram[4] = 0;
comm_ram[5] = 0;
}
else
{
comm_ram[4] = 1;
comm_ram[5] = 1;
}
// 80006: packet number
comm_ram[6] = 0;
comm_ram[7] = 0;
// 80008
comm_ram[8] = slot_id == 0 ? 0x78 : 0x73;
comm_ram[9] = slot_id == 0 ? 0x30 : 0xa2;
// 8000A
*(u16 *)(comm_ram + 10) = 0x100 + slot_size; // offset of recvd data
// 8000C
*(u16 *)(comm_ram + 12) = slot_size * slot_count; // recvd data size
// 8000E
*(u16 *)(comm_ram + 14) = 0x100; // offset of sent data
// 80010
*(u16 *)(comm_ram + 16) = 0x80 + slot_size * slot_count; // sent data size
// FIXME wrungp uses 100, others 80
CommBoardStat& stat = *(CommBoardStat *)&comm_ram[0];
memset(&stat, 0, sizeof(stat));
stat.transmode = swap16(slot_id == 0 ? 0 : 1);
stat.totalnode = slot_count | (slot_count << 8);
stat.nodeID = slot_id | (slot_id << 8);
stat.cts = swap16(slot_id == 0 ? 0x7830 : 0x73a2);
stat.dma_rx_addr = swap16(0x100 + slot_size);
stat.dma_rx_size = swap16(slot_size * slot_count);
stat.dma_tx_addr = swap16(0x100);
stat.dma_tx_size = swap16(slot_size * slot_count);
comm_status0 = 0xff01; // But 1 at connect time before f000 is read
comm_status1 = (slot_count << 8) | slot_id;
@ -183,20 +177,19 @@ void NaomiM3Comm::WriteMem(u32 address, u32 data, u32 size)
// bit 1: comm RAM bank (seems R/O for SH4)
// bit 5: M68K Reset
// bit 6: ???
// bit 7: might be M68K IRQ 5 or 2
// bit 7: might be M68K IRQ 5 or 2 - set to 0 by nlCbIntr()
// bit 14: G1 DMA bus master 0 - active / 1 - disabled
// bit 15: 0 - enable / 1 - disable this device ???
if (data & (1 << 5))
if ((comm_ctrl & COMM_CTRL_RESET) == 0 && (data & COMM_CTRL_RESET) != 0)
{
DEBUG_LOG(NAOMI, "NAOMI_COMM2_CTRL m68k reset");
memset(&comm_ram[0], 0, 32);
comm_status0 = 0; // varies...
comm_status1 = 0;
if (!thread || !thread->joinable())
startThread();
connectNetwork();
}
comm_ctrl = (u16)(data & ~(1 << 5));
//DEBUG_LOG(NAOMI, "NAOMI_COMM2_CTRL set to %x", comm_ctrl);
comm_ctrl = (u16)data;
DEBUG_LOG(NAOMI, "NAOMI_COMM2_CTRL = %x", comm_ctrl);
return;
case NAOMI_COMM2_OFFSET_addr & 255:
@ -207,7 +200,7 @@ void NaomiM3Comm::WriteMem(u32 address, u32 data, u32 size)
case NAOMI_COMM2_DATA_addr & 255:
DEBUG_LOG(NAOMI, "NAOMI_COMM2_DATA written @ %04x %04x", comm_offset, (u16)data);
data = swap16(data);
if (comm_ctrl & 1)
if (comm_ctrl & COMM_CTRL_CPU_RAM)
*(u16*)&m68k_ram[comm_offset] = (u16)data;
else
*(u16*)&comm_ram[comm_offset] = (u16)data;
@ -232,11 +225,10 @@ void NaomiM3Comm::WriteMem(u32 address, u32 data, u32 size)
bool NaomiM3Comm::DmaStart(u32 addr, u32 data)
{
if (comm_ctrl & 0x4000)
if (comm_ctrl & COMM_CTRL_G1DMA)
return false;
DEBUG_LOG(NAOMI, "NaomiM3Comm: DMA addr %08X <-> %04x len %d %s", SB_GDSTAR, comm_offset, SB_GDLEN, SB_GDDIR == 0 ? "OUT" : "IN");
std::unique_lock<std::mutex> lock(mem_mutex);
if (SB_GDDIR == 0)
{
// Network write
@ -246,7 +238,8 @@ bool NaomiM3Comm::DmaStart(u32 addr, u32 data)
else
{
// Network read
if (SB_GDLEN == 32 && (comm_ctrl & 1) == 0)
/*
if (SB_GDLEN == 32 && (comm_ctrl & COMM_CTRL_CPU_RAM) == 0)
{
char buf[32 * 5 + 1];
buf[0] = 0;
@ -257,42 +250,30 @@ bool NaomiM3Comm::DmaStart(u32 addr, u32 data)
}
DEBUG_LOG(NAOMI, "Comm RAM read @%x: %s", comm_offset, buf);
}
*/
for (u32 i = 0; i < SB_GDLEN; i++)
WriteMem8_nommu(SB_GDSTAR + i, comm_ram[comm_offset++]);
}
return true;
}
void NaomiM3Comm::startThread()
void NaomiM3Comm::vblank()
{
network_stopping = false;
thread = std::unique_ptr<std::thread>(new std::thread([this]() {
using the_clock = std::chrono::high_resolution_clock;
if ((comm_ctrl & COMM_CTRL_RESET) == 0 || comm_status1 == 0)
return;
connectNetwork();
the_clock::time_point token_time = the_clock::now();
while (!network_stopping)
{
naomiNetwork.pipeSlaves();
receiveNetwork();
if (slot_id == 0 && naomiNetwork.hasToken())
{
const auto target_duration = std::chrono::milliseconds(10);
auto duration = the_clock::now() - token_time;
if (duration < target_duration)
{
DEBUG_LOG(NAOMI, "Sleeping for %ld ms", (long)std::chrono::duration_cast<std::chrono::milliseconds>(target_duration - duration).count());
std::this_thread::sleep_for(target_duration - duration);
}
token_time = the_clock::now();
}
sendNetwork();
}
DEBUG_LOG(NAOMI, "Network thread exiting");
}));
using the_clock = std::chrono::high_resolution_clock;
the_clock::time_point start = the_clock::now();
try {
bool received = false;
do {
received = receiveNetwork();
} while (!received && the_clock::now() - start < std::chrono::milliseconds(100));
if (!received)
INFO_LOG(NETWORK, "No data received");
sendNetwork();
} catch (const FlycastException& e) {
comm_status0 = 0;
comm_status1 = 0;
}
}

View File

@ -20,10 +20,6 @@
*/
#pragma once
#include "types.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <thread>
class NaomiM3Comm
{
@ -33,13 +29,13 @@ public:
bool DmaStart(u32 addr, u32 data);
void closeNetwork();
void vblank();
private:
void connectNetwork();
void receiveNetwork();
bool receiveNetwork();
void sendNetwork();
void connectedState(bool success);
void startThread();
u16 comm_ctrl = 0xC000;
u16 comm_offset = 0;
@ -51,7 +47,4 @@ private:
int slot_count = 0;
int slot_id = 0;
std::atomic<bool> network_stopping{ false };
std::unique_ptr<std::thread> thread;
std::mutex mem_mutex;
};

View File

@ -1,6 +1,5 @@
#include "Renderer_if.h"
#include "spg.h"
#include "cheats.h"
#include "hw/pvr/pvr_mem.h"
#include "rend/TexCache.h"
#include "cfg/option.h"
@ -276,7 +275,6 @@ void rend_vblank()
}
render_called = false;
check_framebuffer_write();
cheatManager.apply();
emu.vblank();
}

View File

@ -345,6 +345,7 @@ bool GamepadDevice::find_mapping(int system /* = settings.platform.system */)
if (cloneMapping)
input_mapper = std::make_shared<InputMapping>(*input_mapper);
perGameMapping = perGame;
rumblePower = input_mapper->rumblePower;
return true;
}
if (!perGame)

View File

@ -56,7 +56,20 @@ public:
virtual void rumble(float power, float inclination, u32 duration_ms) {}
virtual void update_rumble() {}
bool is_rumble_enabled() const { return _rumble_enabled; }
bool is_rumble_enabled() const { return rumbleEnabled; }
int get_rumble_power() const { return rumblePower; }
void set_rumble_power(int power) {
if (power != this->rumblePower)
{
this->rumblePower = power;
if (input_mapper != nullptr)
{
input_mapper->rumblePower = power;
input_mapper->set_dirty();
save_mapping();
}
}
}
static void Register(const std::shared_ptr<GamepadDevice>& gamepad);
@ -95,7 +108,8 @@ protected:
std::string _name;
std::string _unique_id;
std::shared_ptr<InputMapping> input_mapper;
bool _rumble_enabled = true;
bool rumbleEnabled = false;
int rumblePower = 100;
private:
bool handleButtonInput(int port, DreamcastKey key, bool pressed);

View File

@ -186,6 +186,7 @@ void InputMapping::load(FILE* fp)
dz = std::min(dz, 100);
dz = std::max(dz, 0);
this->dead_zone = (float)dz / 100.f;
this->rumblePower = mf.get_int("emulator", "rumble_power", this->rumblePower);
version = mf.get_int("emulator", "version", 1);
if (version < 3)
@ -413,6 +414,7 @@ bool InputMapping::save(const std::string& name)
mf.set("emulator", "mapping_name", this->name);
mf.set_int("emulator", "dead_zone", (int)std::round(this->dead_zone * 100.f));
mf.set_int("emulator", "rumble_power", this->rumblePower);
mf.set_int("emulator", "version", 3);
int bindIndex = 0;

View File

@ -44,6 +44,7 @@ public:
std::string name;
float dead_zone = 0.1f;
int rumblePower = 100;
int version = 3;
DreamcastKey get_button_id(u32 port, u32 code)

View File

@ -68,6 +68,9 @@ static void emuEventCallback(Event event, void *)
case Event::LoadState:
key = "loadState";
break;
case Event::VBlank:
key = "vblank";
break;
}
if (v[key].isFunction())
v[key]();
@ -90,11 +93,6 @@ static void eventCallback(const char *tag)
}
}
void vblank()
{
eventCallback("vblank");
}
void overlay()
{
eventCallback("overlay");
@ -624,6 +622,7 @@ void init()
EventManager::listen(Event::Pause, emuEventCallback);
EventManager::listen(Event::Terminate, emuEventCallback);
EventManager::listen(Event::LoadState, emuEventCallback);
EventManager::listen(Event::VBlank, emuEventCallback);
doExec(initFile);
}
@ -637,6 +636,7 @@ void term()
EventManager::unlisten(Event::Pause, emuEventCallback);
EventManager::unlisten(Event::Terminate, emuEventCallback);
EventManager::unlisten(Event::LoadState, emuEventCallback);
EventManager::unlisten(Event::VBlank, emuEventCallback);
lua_close(L);
L = nullptr;
}

View File

@ -26,7 +26,6 @@ namespace lua
void init();
void term();
void exec(const std::string& path);
void vblank();
void overlay();
#else
@ -34,7 +33,6 @@ void overlay();
inline static void init() {}
inline static void term() {}
inline static void exec(const std::string& path) {}
inline static void vblank() {}
inline static void overlay() {}
#endif

View File

@ -760,8 +760,11 @@ std::future<bool> startNetwork()
#ifdef SYNC_TEST
startSession(0, 0);
#else
miniupnp.Init();
miniupnp.AddPortMapping(SERVER_PORT, false);
if (config::EnableUPnP)
{
miniupnp.Init();
miniupnp.AddPortMapping(SERVER_PORT, false);
}
try {
if (config::ActAsServer)

View File

@ -1,72 +1,30 @@
/*
Created on: Apr 12, 2020
Copyright 2022 flyinghead
Copyright 2020 flyinghead
This file is part of Flycast.
This file is part of flycast.
flycast is free software: you can redistribute it and/or modify
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
flycast is distributed in the hope that it will be useful,
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with flycast. If not, see <https://www.gnu.org/licenses/>.
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "naomi_network.h"
#include "types.h"
#include <array>
#include <chrono>
#include <thread>
#include "rend/gui.h"
#include "hw/naomi/naomi_cart.h"
#include "hw/naomi/naomi_flashrom.h"
#include "cfg/option.h"
#include "emulator.h"
#ifdef _MSC_VER
#if defined(_WIN64)
typedef __int64 ssize_t;
#else
typedef long ssize_t;
#endif
#endif
#include <chrono>
#include <thread>
NaomiNetwork naomiNetwork;
sock_t NaomiNetwork::createAndBind(int protocol)
{
sock_t sock = socket(AF_INET, protocol == IPPROTO_TCP ? SOCK_STREAM : SOCK_DGRAM, protocol);
if (!VALID(sock))
{
ERROR_LOG(NETWORK, "Cannot create server socket");
return sock;
}
int option = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option));
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(SERVER_PORT);
if (::bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
ERROR_LOG(NETWORK, "NaomiServer: bind() failed. errno=%d", get_last_error());
closeSocket(sock);
}
else
set_non_blocking(sock);
return sock;
}
bool NaomiNetwork::init()
{
if (!config::NetworkEnable)
@ -76,140 +34,48 @@ bool NaomiNetwork::init()
if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
{
ERROR_LOG(NETWORK, "WSAStartup failed. errno=%d", get_last_error());
return false;
throw Exception("WSAStartup failed");
}
#endif
if (config::ActAsServer)
if (config::EnableUPnP)
{
miniupnp.Init();
miniupnp.AddPortMapping(SERVER_PORT, true);
return createBeaconSocket() && createServerSocket();
miniupnp.AddPortMapping(config::LocalPort, true);
}
else
return true;
}
bool NaomiNetwork::createServerSocket()
{
if (VALID(server_sock))
return true;
createSocket();
server_sock = createAndBind(IPPROTO_TCP);
if (!VALID(server_sock))
return false;
if (listen(server_sock, 5) < 0)
{
ERROR_LOG(NETWORK, "NaomiServer: listen() failed. errno=%d", get_last_error());
closeSocket(server_sock);
return false;
}
return true;
}
bool NaomiNetwork::createBeaconSocket()
void NaomiNetwork::createSocket()
{
if (!VALID(beacon_sock))
beacon_sock = createAndBind(IPPROTO_UDP);
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET)
{
ERROR_LOG(NETWORK, "Socket creation failed: errno %d", get_last_error());
throw Exception("Socket creation failed");
}
int option = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option));
return VALID(beacon_sock);
}
sockaddr_in serveraddr{};
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(config::LocalPort);
void NaomiNetwork::processBeacon()
{
// Receive broadcast queries on beacon socket and reply
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
memset(&addr, 0, sizeof(addr));
char buf[6];
ssize_t n;
do {
memset(buf, '\0', sizeof(buf));
if ((n = recvfrom(beacon_sock, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &addrlen)) == -1)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
WARN_LOG(NETWORK, "NaomiServer: Error receiving datagram. errno=%d", get_last_error());
}
else
{
DEBUG_LOG(NETWORK, "NaomiServer: beacon received %ld bytes", (long)n);
if (n == sizeof(buf) && !strncmp(buf, "flycast", n))
sendto(beacon_sock, buf, n, 0, (const struct sockaddr *)&addr, addrlen);
}
} while (n != -1);
}
if (::bind(sock, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
ERROR_LOG(NETWORK, "NaomiServer: bind() failed. errno=%d", get_last_error());
closesocket(sock);
bool NaomiNetwork::findServer()
{
// Automatically find the adhoc server on the local network using broadcast
sock_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (!VALID(sockfd))
{
ERROR_LOG(NETWORK, "Datagram socket creation error. errno=%d", get_last_error());
return false;
}
throw Exception("Socket bind failed");
}
set_non_blocking(sock);
// Allow broadcast packets to be sent
int broadcast = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) == -1)
{
ERROR_LOG(NETWORK, "setsockopt(SO_BROADCAST) failed. errno=%d", get_last_error());
closesocket(sockfd);
return false;
}
// Set a 500ms timeout on recv call
if (!set_recv_timeout(sockfd, 500))
{
ERROR_LOG(NETWORK, "setsockopt(SO_RCVTIMEO) failed. errno=%d", get_last_error());
closesocket(sockfd);
return false;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET; // host byte order
addr.sin_port = htons(SERVER_PORT); // short, network byte order
addr.sin_addr.s_addr = INADDR_BROADCAST;
memset(addr.sin_zero, '\0', sizeof(addr.sin_zero));
struct sockaddr server_addr;
for (int i = 0; i < 3; i++)
{
if (sendto(sockfd, "flycast", 6, 0, (struct sockaddr *)&addr, sizeof addr) == -1)
{
WARN_LOG(NETWORK, "Send datagram failed. errno=%d", get_last_error());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
char buf[6];
memset(&server_addr, '\0', sizeof(server_addr));
socklen_t addrlen = sizeof(server_addr);
if (recvfrom(sockfd, buf, sizeof(buf), 0, &server_addr, &addrlen) == -1)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
WARN_LOG(NETWORK, "Recv datagram failed. errno=%d", get_last_error());
else
INFO_LOG(NETWORK, "Recv datagram timeout. i=%d", i);
continue;
}
server_ip = ((struct sockaddr_in *)&server_addr)->sin_addr;
char addressBuffer[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &server_ip, addressBuffer, INET_ADDRSTRLEN);
server_name = addressBuffer;
break;
}
closesocket(sockfd);
if (server_ip.s_addr == INADDR_NONE)
{
WARN_LOG(NETWORK, "Network Error: Can't find ad-hoc server on local network");
gui_display_notification("No server found", 8000);
return false;
}
INFO_LOG(NETWORK, "Found ad-hoc server at %s", server_name.c_str());
return true;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) == -1)
WARN_LOG(NETWORK, "setsockopt(SO_BROADCAST) failed. errno=%d", get_last_error());
}
bool NaomiNetwork::startNetwork()
@ -217,10 +83,9 @@ bool NaomiNetwork::startNetwork()
if (!init())
return false;
slot_id = 0;
slot_count = 0;
slotId = 0;
slotCount = 0;
slaves.clear();
got_token = false;
using namespace std::chrono;
const auto timeout = seconds(20);
@ -232,416 +97,191 @@ bool NaomiNetwork::startNetwork()
while (steady_clock::now() - start_time < timeout)
{
if (network_stopping)
{
for (auto& slave : slaves)
if (VALID(slave.socket))
closeSocket(slave.socket);
if (networkStopping)
return false;
}
std::string notif = slaves.empty() ? "Waiting for players..."
: std::to_string(slaves.size()) + " player(s) connected. Waiting...";
gui_display_notification(notif.c_str(), timeout.count() * 2000);
processBeacon();
poll();
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
memset(&src_addr, 0, addr_len);
sock_t clientSock = accept(server_sock, (struct sockaddr *)&src_addr, &addr_len);
if (!VALID(clientSock))
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
perror("accept");
}
else
{
NOTICE_LOG(NETWORK, "Slave connection accepted");
set_non_blocking(clientSock);
set_tcp_nodelay(clientSock);
std::lock_guard<std::mutex> lock(mutex);
slaves.emplace_back(clientSock);
}
const auto now = steady_clock::now();
u32 waiting_slaves = 0;
for (auto& slave : slaves)
{
if (slave.state == ClientState::Waiting)
waiting_slaves++;
else if (slave.state == ClientState::Connected)
{
char buffer[8];
ssize_t l = ::recv(slave.socket, buffer, sizeof(buffer), 0);
if (l < (int)sizeof(buffer) && get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
// error
INFO_LOG(NETWORK, "Slave socket recv error. errno=%d", get_last_error());
closeSocket(slave.socket);
}
else if (l == -1 && now - slave.state_time > milliseconds(100))
{
// timeout
INFO_LOG(NETWORK, "Slave socket Connected timeout");
closeSocket(slave.socket);
}
else if (l == (int)sizeof(buffer))
{
if (memcmp(buffer, naomi_game_id, sizeof(buffer)))
{
// wrong game
WARN_LOG(NETWORK, "Wrong game id received: %.8s", buffer);
closeSocket(slave.socket);
}
else
{
slave.set_state(ClientState::Waiting);
waiting_slaves++;
}
}
}
}
{
std::lock_guard<std::mutex> lock(mutex);
slaves.erase(std::remove_if(slaves.begin(),
slaves.end(),
[](const Slave& slave){ return !VALID(slave.socket); }),
slaves.end());
}
if (waiting_slaves == 3 || (start_now && !slaves.empty() && waiting_slaves == slaves.size()))
if (slaves.size() == 3 || (_startNow && !slaves.empty()))
break;
std::this_thread::sleep_for(milliseconds(100));
std::this_thread::sleep_for(milliseconds(20));
}
slot_id = 0;
slot_count = slaves.size() + 1;
u8 buf[2] = { (u8)slot_count, 0 };
int slot_num = 1;
{
for (auto& slave : slaves)
{
buf[1] = { (u8)slot_num };
slot_num++;
::send(slave.socket, (const char *)buf, 2, 0);
slave.set_state(ClientState::Starting);
}
}
NOTICE_LOG(NETWORK, "Master starting: %zd slaves", slaves.size());
if (!slaves.empty())
{
NOTICE_LOG(NETWORK, "Master starting: %zd slaves", slaves.size());
_startNow = true;
slotCount = slaves.size() + 1;
Packet packet(Start);
packet.start.nodeCount = slotCount;
for (auto& slave : slaves)
send(&slave.addr, &packet, packet.size());
nextPeer = slaves[0].addr;
gui_display_notification("Starting game", 2000);
SetNaomiNetworkConfig(0);
return true;
}
else
{
gui_display_notification("No player connected", 8000);
return false;
}
gui_display_notification("No player connected", 8000);
}
else
{
serverIp = INADDR_BROADCAST;
u16 serverPort = SERVER_PORT;
if (!config::NetworkServer.get().empty())
{
struct addrinfo *resultAddr;
if (getaddrinfo(config::NetworkServer.get().c_str(), 0, nullptr, &resultAddr))
WARN_LOG(NETWORK, "Server %s is unknown", config::NetworkServer.get().c_str());
auto pos = config::NetworkServer.get().find_last_of(':');
std::string server;
if (pos != std::string::npos)
{
serverPort = atoi(config::NetworkServer.get().substr(pos + 1).c_str());
server = config::NetworkServer.get().substr(0, pos);
}
else
for (struct addrinfo *ptr = resultAddr; ptr != nullptr; ptr = ptr->ai_next)
server = config::NetworkServer;
addrinfo *resultAddr;
if (getaddrinfo(server.c_str(), 0, nullptr, &resultAddr))
WARN_LOG(NETWORK, "Server %s is unknown", server.c_str());
else
{
for (addrinfo *ptr = resultAddr; ptr != nullptr; ptr = ptr->ai_next)
if (ptr->ai_family == AF_INET)
{
server_ip = ((sockaddr_in *)ptr->ai_addr)->sin_addr;
serverIp = ((sockaddr_in *)ptr->ai_addr)->sin_addr.s_addr;
break;
}
freeaddrinfo(resultAddr);
}
}
NOTICE_LOG(NETWORK, "Connecting to server");
gui_display_notification("Connecting to server", 10000);
steady_clock::time_point start_time = steady_clock::now();
while (!network_stopping && steady_clock::now() - start_time < timeout)
while (!networkStopping && !_startNow && steady_clock::now() - start_time < timeout)
{
if (server_ip.s_addr == INADDR_NONE && !findServer())
continue;
client_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in src_addr;
src_addr.sin_family = AF_INET;
src_addr.sin_addr = server_ip;
src_addr.sin_port = htons(SERVER_PORT);
if (::connect(client_sock, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0)
if (slotId == 0)
{
ERROR_LOG(NETWORK, "Socket connect failed");
closeSocket(client_sock);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
Packet packet(SyncReq);
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverPort);
serverAddr.sin_addr.s_addr = serverIp;
send(&serverAddr, &packet, packet.size());
}
std::this_thread::sleep_for(milliseconds(10));
poll();
}
if (!networkStopping && _startNow)
{
SetNaomiNetworkConfig(slotId);
return true;
}
}
return false;
}
bool NaomiNetwork::receive(const sockaddr_in *addr, const Packet *packet, u32 size)
{
DEBUG_LOG(NETWORK, "Received port %d pckt %d size %x", ntohs(addr->sin_port), packet->type, size - (u32)packet->size(0));
switch (packet->type)
{
case SyncReq:
if (config::ActAsServer && !_startNow)
{
Slave *slave = nullptr;
for (auto& s : slaves)
if (s.addr.sin_port == addr->sin_port && s.addr.sin_addr.s_addr == addr->sin_addr.s_addr)
{
slave = &s;
break;
}
if (slave == nullptr)
{
slaves.push_back(Slave());
slave = &slaves.back();
slave->state = 0; // unused
slave->addr = *addr;
}
Packet reply(SyncReply);
reply.sync.nodeId = (u16)(slave - &slaves[0] + 1);
if (slave - &slaves[0] + 1 < (int)slaves.size())
{
Slave *nextSlave = &slaves[slave - &slaves[0] + 1];
reply.sync.nextNodeIp = nextSlave->addr.sin_addr.s_addr;
reply.sync.nextNodePort = nextSlave->addr.sin_port;
}
else
{
gui_display_notification("Waiting for server to start", 10000);
set_tcp_nodelay(client_sock);
::send(client_sock, naomi_game_id, 8, 0);
set_recv_timeout(client_sock, (int)std::chrono::milliseconds(timeout * 2).count());
u8 buf[2];
if (::recv(client_sock, (char *)buf, 2, 0) < 2)
{
ERROR_LOG(NETWORK, "recv failed: errno=%d", get_last_error());
closeSocket(client_sock);
gui_display_notification("Server failed to start", 10000);
return false;
}
slot_count = buf[0];
slot_id = buf[1];
got_token = slot_id == 1;
set_non_blocking(client_sock);
std::string notif = "Connected as slot " + std::to_string(slot_id);
gui_display_notification(notif.c_str(), 2000);
SetNaomiNetworkConfig(slot_id);
return true;
//FIXME local ip?
reply.sync.nextNodeIp = 0;
reply.sync.nextNodePort = htons(config::LocalPort);
}
}
return false;
}
}
send(addr, &reply, reply.size());
bool NaomiNetwork::syncNetwork()
{
using namespace std::chrono;
const auto timeout = seconds(10);
if (config::ActAsServer)
{
steady_clock::time_point start_time = steady_clock::now();
bool all_slaves_ready = false;
while (steady_clock::now() - start_time < timeout && !all_slaves_ready)
{
all_slaves_ready = true;
for (auto& slave : slaves)
if (slave.state != ClientState::Ready)
{
char buf[4];
ssize_t l = ::recv(slave.socket, buf, sizeof(buf), 0);
if (l < 4 && get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
INFO_LOG(NETWORK, "Socket recv failed. errno=%d", get_last_error());
closeSocket(slave.socket);
return false;
}
if (l == 4)
{
if (memcmp(buf, "REDY", 4))
{
INFO_LOG(NETWORK, "Synchronization failed");
closeSocket(slave.socket);
return false;
}
slave.set_state(ClientState::Ready);
}
else
all_slaves_ready = false;
}
if (network_stopping)
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
for (auto& slave : slaves)
{
ssize_t l = ::send(slave.socket, "GO!!", 4, 0);
if (l < 4)
if (reply.sync.nodeId > 1)
{
INFO_LOG(NETWORK, "Socket send failed. errno=%d", get_last_error());
closeSocket(slave.socket);
return false;
// notify previous slave of nextNode change
reply.sync.nextNodeIp = addr->sin_addr.s_addr;
reply.sync.nextNodePort = addr->sin_port;
reply.sync.nodeId--;
slave = &slaves[reply.sync.nodeId - 1];
send(&slave->addr, &reply, reply.size());
}
slave.set_state(ClientState::Online);
}
gui_display_notification("Network started", 5000);
break;
case SyncReply:
if (!config::ActAsServer && !_startNow)
{
serverIp = addr->sin_addr.s_addr;
slotId = packet->sync.nodeId;
nextPeer.sin_family = AF_INET;
nextPeer.sin_port = packet->sync.nextNodePort;
nextPeer.sin_addr.s_addr = packet->sync.nextNodeIp == 0 ? addr->sin_addr.s_addr : packet->sync.nextNodeIp;
std::string notif = "Connected as slot " + std::to_string(slotId);
gui_display_notification(notif.c_str(), 2000);
}
break;
case Start:
if (!_startNow)
{
slotCount = packet->start.nodeCount;
sendAck(addr);
_startNow = true;
}
break;
case Data:
if (!receivedData.empty())
INFO_LOG(NETWORK, "Received packet overwritten");
receivedData.resize(size - packet->size(0));
memcpy(receivedData.data(), packet->data.payload, receivedData.size());
packetNumber = packet->data.packetNumber;
// TODO? sendAck(peer, port);
return true;
case Ack:
break;
case NAck:
WARN_LOG(NETWORK, "NAK received");
throw Exception("NAK received");
break;
default:
WARN_LOG(NETWORK, "Unknown packet type %d", packet->type);
throw Exception("Unknown packet type ");
break;
}
else
{
// Tell master we're ready
ssize_t l = ::send(client_sock, "REDY", 4 ,0);
if (l < 4)
{
WARN_LOG(NETWORK, "Socket send failed. errno=%d", get_last_error());
closeSocket(client_sock);
return false;
}
steady_clock::time_point start_time = steady_clock::now();
while (steady_clock::now() - start_time < timeout)
{
// Wait for the go
char buf[4];
l = ::recv(client_sock, buf, sizeof(buf), 0);
if (l < 4 && get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
INFO_LOG(NETWORK, "Socket recv failed. errno=%d", get_last_error());
closeSocket(client_sock);
return false;
}
else if (l == 4)
{
if (memcmp(buf, "GO!!", 4))
{
INFO_LOG(NETWORK, "Synchronization failed");
closeSocket(client_sock);
return false;
}
gui_display_notification("Network started", 5000);
return true;
}
if (network_stopping)
{
closeSocket(client_sock);
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
INFO_LOG(NETWORK, "Socket recv timeout");
closeSocket(client_sock);
return false;
}
}
void NaomiNetwork::pipeSlaves()
{
if (!isMaster() || slot_count < 3)
return;
char buf[16384];
for (auto it = slaves.begin(); it != slaves.end() - 1; it++)
{
if (!VALID(it->socket) || !VALID((it + 1)->socket))
// TODO keep link on
continue;
ssize_t l = ::recv(it->socket, buf, sizeof(buf), 0);
if (l <= 0)
{
if (get_last_error() == L_EAGAIN || get_last_error() == L_EWOULDBLOCK)
continue;
WARN_LOG(NETWORK, "pipeSlaves: receive failed. errno=%d", get_last_error());
closeSocket(it->socket);
continue;
}
ssize_t l2 = ::send((it + 1)->socket, buf, l, 0);
if (l2 != l)
{
WARN_LOG(NETWORK, "pipeSlaves: send failed. errno=%d", get_last_error());
closeSocket((it + 1)->socket);
}
}
}
bool NaomiNetwork::receive(u8 *data, u32 size)
{
sock_t sockfd = INVALID_SOCKET;
if (isMaster())
sockfd = slaves.empty() ? INVALID_SOCKET : slaves.back().socket;
else
sockfd = client_sock;
if (!VALID(sockfd))
return false;
ssize_t received = 0;
while (received != size)
{
ssize_t l = ::recv(sockfd, (char*)(data + received), size - received, 0);
if (l <= 0)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
WARN_LOG(NETWORK, "receiveNetwork: read failed. errno=%d", get_last_error());
if (isMaster())
{
closeSocket(slaves.back().socket);
got_token = false;
}
else
shutdown();
return false;
}
else if (received == 0)
return false;
}
else
received += l;
if (network_stopping)
return false;
}
DEBUG_LOG(NETWORK, "[%d] Received %d bytes", slot_id, size);
got_token = true;
return true;
}
void NaomiNetwork::send(u8 *data, u32 size)
{
if (!got_token)
return;
sock_t sockfd;
if (isMaster())
sockfd = slaves.empty() ? INVALID_SOCKET : slaves.front().socket;
else
sockfd = client_sock;
if (!VALID(sockfd))
return;
if (::send(sockfd, (const char *)data, size, 0) < size)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
WARN_LOG(NETWORK, "send failed. errno=%d", get_last_error());
if (isMaster())
closeSocket(slaves.front().socket);
else
shutdown();
}
return;
}
else
{
DEBUG_LOG(NETWORK, "[%d] Sent %d bytes", slot_id, size);
got_token = false;
}
}
void NaomiNetwork::shutdown()
{
network_stopping = true;
{
std::lock_guard<std::mutex> lock(mutex);
for (auto& slave : slaves)
closeSocket(slave.socket);
}
if (VALID(client_sock))
closeSocket(client_sock);
emu.setNetworkState(false);
}
void NaomiNetwork::terminate()
{
shutdown();
if (config::ActAsServer)
miniupnp.Term();
if (VALID(beacon_sock))
closeSocket(beacon_sock);
if (VALID(server_sock))
closeSocket(server_sock);
}
std::future<bool> NaomiNetwork::startNetworkAsync()
{
network_stopping = false;
start_now = false;
return std::async(std::launch::async, [this] {
bool res = startNetwork();
emu.setNetworkState(res);
return res;
});
return false;
}
// Sets the game network config using MIE eeprom or bbsram:

View File

@ -1,94 +1,211 @@
/*
Created on: Apr 12, 2020
Copyright 2022 flyinghead
Copyright 2020 flyinghead
This file is part of Flycast.
This file is part of flycast.
flycast is free software: you can redistribute it and/or modify
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
flycast is distributed in the hope that it will be useful,
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with flycast. If not, see <https://www.gnu.org/licenses/>.
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "types.h"
#include <cstdint>
#include <atomic>
#include <future>
#include <mutex>
#include <vector>
#include "net_platform.h"
#include "miniupnp.h"
#include "rend/gui.h"
#include "cfg/option.h"
#include "emulator.h"
#include <atomic>
#include <future>
class NaomiNetwork
{
public:
NaomiNetwork() {
#ifdef _WIN32
server_ip.S_un.S_addr = INADDR_NONE;
#else
server_ip.s_addr = INADDR_NONE;
#endif
class Exception : public FlycastException
{
public:
Exception(const std::string& reason) : FlycastException(reason) {}
};
~NaomiNetwork() { shutdown(); }
std::future<bool> startNetworkAsync()
{
networkStopping = false;
_startNow = false;
return std::async(std::launch::async, [this] {
bool res = startNetwork();
emu.setNetworkState(res);
return res;
});
}
void shutdown()
{
emu.setNetworkState(false);
closesocket(sock);
sock = INVALID_SOCKET;
}
bool receive(u8 *data, u32 size, u16 *packetNumber)
{
poll();
if (receivedData.empty())
return false;
size = std::min(size, (u32)receivedData.size());
memcpy(data, receivedData.data(), size);
receivedData.erase(receivedData.begin(), receivedData.begin() + size);
*packetNumber = this->packetNumber;
return true;
}
void send(u8 *data, u32 size, u16 packetNumber)
{
verify(size < sizeof(Packet::data.payload));
Packet packet(Data);
memcpy(packet.data.payload, data, size);
packet.data.packetNumber = packetNumber;
send(&nextPeer, &packet, packet.size(size));
}
int getSlotCount() const { return slotCount; }
int getSlotId() const { return slotId; }
void startNow() {
if (config::ActAsServer)
_startNow = true;
}
~NaomiNetwork() { terminate(); }
std::future<bool> startNetworkAsync();
void startNow() { start_now = true; }
bool syncNetwork();
void pipeSlaves();
bool receive(u8 *data, u32 size);
void send(u8 *data, u32 size);
void shutdown(); // thread-safe
void terminate(); // thread-safe
int slotCount() const { return slot_count; }
int slotId() const { return slot_id; }
bool hasToken() const { return got_token; }
private:
bool init();
bool createServerSocket();
bool createBeaconSocket();
bool startNetwork();
void processBeacon();
bool findServer();
sock_t createAndBind(int protocol);
bool isMaster() const { return slot_id == 0; }
void closeSocket(sock_t& socket) const { closesocket(socket); socket = INVALID_SOCKET; }
struct in_addr server_ip;
std::string server_name;
// server stuff
sock_t server_sock = INVALID_SOCKET;
sock_t beacon_sock = INVALID_SOCKET;
enum class ClientState { Connected, Waiting, Starting, Ready, Online };
struct Slave {
Slave(sock_t socket)
: state(ClientState::Connected), state_time(std::chrono::steady_clock::now()), socket(socket) {}
void set_state(ClientState state) { this->state = state; this->state_time = std::chrono::steady_clock::now(); }
ClientState state;
std::chrono::steady_clock::time_point state_time;
sock_t socket;
enum PacketType : u16 {
SyncReq,
SyncReply,
Start,
Data,
Ack,
NAck
};
std::vector<Slave> slaves;
bool start_now = false;
// client stuff
sock_t client_sock = INVALID_SOCKET;
// common stuff
int slot_count = 0;
int slot_id = 0;
bool got_token = false;
std::atomic<bool> network_stopping{ false };
std::mutex mutex;
#pragma pack(push, 1)
struct Packet
{
Packet(PacketType type = SyncReq) : type(type) {}
PacketType type;
union {
struct {
u16 nodeId;
u16 nextNodePort;
u32 nextNodeIp;
} sync;
struct {
u16 nodeCount;
} start;
struct {
u16 packetNumber;
u8 payload[0x4000];
} data;
};
size_t size(size_t dataSize = 0) const
{
size_t sz = sizeof(type);
switch (type) {
case SyncReq:
case SyncReply:
sz += sizeof(sync);
break;
case Start:
sz += sizeof(start);
break;
case Data:
sz += sizeof(data.packetNumber) + dataSize;
break;
default:
break;
}
return sz;
}
};
#pragma pack(pop)
bool init();
void createSocket();
bool startNetwork();
void poll()
{
Packet packet;
sockaddr_in addr;
while (true)
{
socklen_t len = sizeof(addr);
int rc = recvfrom(sock, (char *)&packet, sizeof(packet), 0, (sockaddr *)&addr, &len);
if (rc == -1)
{
int error = get_last_error();
if (error == EWOULDBLOCK || error == EAGAIN)
break;
throw Exception("Receive error: errno " + std::to_string(error));
}
if (rc < (int)packet.size(0))
throw Exception("Receive error: truncated packet");
receive(&addr, &packet, rc);
}
}
bool receive(const sockaddr_in *addr, const Packet *packet, u32 size);
void sendAck(const sockaddr_in *addr, bool ack = true)
{
Packet packet(ack ? Ack : NAck);
send(addr, &packet, packet.size());
}
void send(const sockaddr_in *addr, const Packet *packet, u32 size)
{
ssize_t rc = sendto(sock, (const char *)packet, size, 0,
(sockaddr *)addr, sizeof(*addr));
if (rc != size)
throw Exception("Send failed: errno " + std::to_string(get_last_error()));
DEBUG_LOG(NETWORK, "Sent port %d pckt %d size %x", ntohs(addr->sin_port), packet->type, size - (u32)packet->size(0));
}
sock_t sock;
int slotCount = 0;
int slotId = 0;
std::atomic<bool> networkStopping{ false };
MiniUPnP miniupnp;
static const uint16_t SERVER_PORT = 37391;
sockaddr_in nextPeer;
std::vector<u8> receivedData;
u16 packetNumber = 0;
bool _startNow = false;
// Server stuff
struct Slave
{
int state;
sockaddr_in addr;
};
std::vector<Slave> slaves;
// Client stuff
u32 serverIp;
public:
static constexpr u16 SERVER_PORT = 37391;
};
extern NaomiNetwork naomiNetwork;

View File

@ -46,7 +46,7 @@ public:
}
void stop() override {
naomiNetwork.terminate();
naomiNetwork.shutdown();
}
bool canStartNow() override {

View File

@ -873,7 +873,7 @@ static void *pico_thread_func(void *)
std::async(std::launch::async, [ports]() {
// Initialize miniupnpc and map network ports
MiniUPnP upnp;
if (ports != nullptr)
if (ports != nullptr && config::EnableUPnP)
{
if (!upnp.Init())
WARN_LOG(MODEM, "UPNP Init failed");

View File

@ -1390,12 +1390,13 @@ static void gui_display_settings()
header("Physical Devices");
{
ImGui::Columns(4, "physicalDevices", false);
ImGui::Text("System");
ImVec4 gray{ 0.5f, 0.5f, 0.5f, 1.f };
ImGui::TextColored(gray, "System");
ImGui::SetColumnWidth(-1, ImGui::CalcTextSize("System").x + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetStyle().ItemSpacing.x);
ImGui::NextColumn();
ImGui::Text("Name");
ImGui::TextColored(gray, "Name");
ImGui::NextColumn();
ImGui::Text("Port");
ImGui::TextColored(gray, "Port");
ImGui::SetColumnWidth(-1, ImGui::CalcTextSize("None").x * 1.6f + ImGui::GetStyle().FramePadding.x * 2.0f + ImGui::GetFrameHeight()
+ ImGui::GetStyle().ItemInnerSpacing.x + ImGui::GetStyle().ItemSpacing.x);
ImGui::NextColumn();
@ -1445,7 +1446,16 @@ static void gui_display_settings()
ImGui::SameLine();
OptionSlider("Haptic", config::VirtualGamepadVibration, 0, 60);
}
else
#endif
if (gamepad->is_rumble_enabled())
{
ImGui::SameLine(0, 16 * scaling);
int power = gamepad->get_rumble_power();
ImGui::SetNextItemWidth(150 * scaling);
if (ImGui::SliderInt("Rumble", &power, 0, 100))
gamepad->set_rumble_power(power);
}
ImGui::NextColumn();
ImGui::PopID();
}
@ -1951,6 +1961,7 @@ static void gui_display_settings()
"Enable networking for supported Naomi games");
if (config::GGPOEnable)
{
config::NetworkEnable = false;
OptionCheckbox("Play as Player 1", config::ActAsServer,
"Deselect to play as player 2");
char server_name[256];
@ -1983,10 +1994,19 @@ static void gui_display_settings()
strcpy(server_name, config::NetworkServer.get().c_str());
ImGui::InputText("Server", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr);
ImGui::SameLine();
ShowHelpMarker("The server to connect to. Leave blank to find a server automatically");
ShowHelpMarker("The server to connect to. Leave blank to find a server automatically on the default port");
config::NetworkServer.set(server_name);
}
char localPort[256];
sprintf(localPort, "%d", (int)config::LocalPort);
ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr);
ImGui::SameLine();
ShowHelpMarker("The local UDP port to use");
config::LocalPort.set(atoi(localPort));
}
OptionCheckbox("Enable UPnP", config::EnableUPnP);
ImGui::SameLine();
ShowHelpMarker("Automatically configure your network router for netplay");
}
ImGui::Spacing();
header("Other");

View File

@ -127,7 +127,7 @@ bool VkCreateDevice(retro_vulkan_context* context, VkInstance instance, VkPhysic
bool getMemReq2Supported = false;
VulkanContext::Instance()->dedicatedAllocationSupported = false;
std::vector<const char *> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
for (int i = 0; i < num_required_device_extensions; i++)
for (unsigned i = 0; i < num_required_device_extensions; i++)
deviceExtensions.push_back(required_device_extensions[i]);
for (const auto& property : physicalDevice.enumerateDeviceExtensionProperties())
{

View File

@ -177,9 +177,9 @@ public:
INFO_LOG(INPUT, "using custom mapping '%s'", input_mapper->name.c_str());
#if SDL_VERSION_ATLEAST(2, 0, 18)
sdl_has_rumble = SDL_JoystickHasRumble(sdl_joystick);
rumbleEnabled = SDL_JoystickHasRumble(sdl_joystick);
#else
sdl_has_rumble = (SDL_JoystickRumble(sdl_joystick, 1, 1, 1) != -1);
rumbleEnabled = (SDL_JoystickRumble(sdl_joystick, 1, 1, 1) != -1);
#endif
}
@ -192,17 +192,18 @@ public:
void rumble(float power, float inclination, u32 duration_ms) override
{
if (sdl_has_rumble)
if (rumbleEnabled)
{
vib_inclination = inclination * power;
vib_stop_time = os_GetSeconds() + duration_ms / 1000.0;
SDL_JoystickRumble(sdl_joystick, (Uint16)(power * 65535), (Uint16)(power * 65535), duration_ms);
Uint16 intensity = (Uint16)std::min(power * rumblePower * 65535.f / 100.f, 65535.f);
SDL_JoystickRumble(sdl_joystick, intensity, intensity, duration_ms);
}
}
void update_rumble() override
{
if (!sdl_has_rumble)
if (!rumbleEnabled)
return;
if (vib_inclination > 0)
{
@ -210,7 +211,10 @@ public:
if (rem_time <= 0)
vib_inclination = 0;
else
SDL_JoystickRumble(sdl_joystick, (Uint16)(vib_inclination * rem_time * 65535), (Uint16)(vib_inclination * rem_time * 65535), rem_time);
{
Uint16 intensity = (Uint16)std::min(vib_inclination * rem_time * 65535.f * rumblePower / 100.f, 65535.f);
SDL_JoystickRumble(sdl_joystick, intensity, intensity, rem_time);
}
}
}
@ -371,7 +375,6 @@ public:
private:
SDL_Joystick* sdl_joystick;
SDL_JoystickID sdl_joystick_instance;
bool sdl_has_rumble = false;
float vib_inclination = 0;
double vib_stop_time = 0;
SDL_GameController *sdl_controller = nullptr;

View File

@ -62,7 +62,8 @@ public:
V24 = 819,
V25 = 820,
V26 = 821,
Current = V26,
V27 = 822,
Current = V27,
Next = Current + 1,
};

View File

@ -105,6 +105,7 @@ public:
if (id == VIRTUAL_GAMEPAD_ID)
{
input_mapper = std::make_shared<IdentityInputMapping>();
rumbleEnabled = true;
}
else
{

View File

@ -107,7 +107,9 @@ Option<bool> NetworkEnable("", false);
Option<bool> ActAsServer("", false);
OptionString DNS("", "46.101.91.123");
OptionString NetworkServer("", "");
Option<int> LocalPort("", 0);
Option<bool> EmulateBBA("", false); // TODO
Option<bool> EnableUPnP("", true); // TODO
Option<bool> GGPOEnable("", false);
Option<int> GGPODelay("", 0);
Option<bool> NetworkStats("", false);

View File

@ -31,7 +31,7 @@ TEST_F(SerializeTest, SizeTest)
std::vector<char> data(30000000);
Serializer ser(data.data(), data.size());
dc_serialize(ser);
ASSERT_EQ(28191587u, ser.size());
ASSERT_EQ(28191595u, ser.size());
}