mirror of
https://github.com/flyinghead/flycast.git
synced 2026-11-09 01:59:49 -10:00
889 lines
22 KiB
C++
889 lines
22 KiB
C++
/*
|
|
Copyright 2026 flyinghead
|
|
|
|
This file is part of Flycast.
|
|
|
|
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,
|
|
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/>.
|
|
*/
|
|
#if !defined(LIBRETRO) || !defined(__ANDROID__)
|
|
#include "types.h"
|
|
#include <asio.hpp>
|
|
#include "emulator.h"
|
|
#include "hw/modem/modem.h"
|
|
#include "netservice.h"
|
|
#include "rawmodem.h"
|
|
#include "hw/mem/addrspace.h"
|
|
#include "ice.h"
|
|
#include "util/tsqueue.h"
|
|
#include "oslib/i18n.h"
|
|
#include "oslib/oslib.h"
|
|
#include "reios/reios.h"
|
|
#include <juice/juice.h>
|
|
#include <stdlib.h>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
|
|
#include <thread>
|
|
using namespace std::chrono_literals;
|
|
|
|
//
|
|
// Emulates the Capcom Direct gaming service.
|
|
// After being matched, the two opponents' consoles disconnect from the lobby server.
|
|
// Then one peer sets his modem in answer mode and the other one calls it directly.
|
|
// Once the match is over, they disconnect and reconnect to the lobby server.
|
|
//
|
|
// Emulating all this requires a complex dance involving a special network service and server
|
|
// and a tight relationship with the modem.
|
|
// 1. Once a capcom direct game disconnects from the network, two modem callbacks are installed:
|
|
// one when dialing out and one when detecting the modem is in answer mode.
|
|
// 2. The modem detects a dial out with a specific number, or detects answer mode.
|
|
// The caller keeps the modem ringing.
|
|
// 3. Both peers connect to the battle server and register: game, caller/callee, battle code.
|
|
// They start gathering local candidates for the ICE connection
|
|
// and send them to the server/peer when ready.
|
|
// 4. The battle server matches the 2 connections and send a START message to the callee.
|
|
// From this point on, the server only pipes data between peers without interfering.
|
|
// 5. On receiving START, the callee makes the phone ring.
|
|
// 6. The game detects the phone ringing and picks up the call to start handshaking/negotiation
|
|
// 7. The callee waits for the ICE connection to be established if needed,
|
|
// and sends START to the caller, prompting him to start modem handshaking/negotiation.
|
|
// 8. Both peers start exchanging game data
|
|
|
|
// TODO callee modem sometimes freezes when other party leaves -> spawn, hmgeo
|
|
|
|
// TODO some chars left in modem or buffer when starting a new match (invalid packet received from server/peer, mvsc2)
|
|
|
|
// TODO intermittent lock up at battle start after PING/PONG/START (zero3, mvsc2)
|
|
|
|
// Capcom Direct games
|
|
// 1 Marvel vs. Capcom 2 T1215M
|
|
// 2 Power Stone 2 T1218M
|
|
// 3 Street Fighter III 3rd Strike T1209M
|
|
// 4 Street Fighter Zero 3 for M.S. T1230M
|
|
// 5 Spawn T1216M
|
|
// 6 Vampire Chronicle for M.S. T1235M
|
|
// 7 Netto De Tennis T1234M
|
|
// 8 Capcom vs. SNK Millennium Fight Pro T1247M
|
|
// Capcom vs. SNK Millennium Fight T1217M
|
|
// 9 Jojo's Bizarre Adventure for M.S. T1231M
|
|
// 10 Project Justice T1221M
|
|
// 11 Super Street Fighter IIX for M.S. T1236M
|
|
// 12 Tech Romancer for M.S. T1232M
|
|
// 13 Taisen Net Gimmick T1248M
|
|
// 14 Heavy Metal: Geomatrix T1246M
|
|
// 15 Super Puzzle Fighter IIX for M.S. T1250M
|
|
|
|
namespace net::modbba
|
|
{
|
|
|
|
const std::string games[] {
|
|
"T1215M", "T1218M", "T1209M", "T1230M", "T1216M", "T1235M",
|
|
"T1234M", "T1247M", "T1231M", "T1221M", "T1236M", "T1232M",
|
|
"T1248M", "T1246M", "T1250M"
|
|
};
|
|
|
|
enum PackeType { Register = 1, Start, Candidates, Stop };
|
|
static constexpr u8 PKT_MAGIC[] { 0xBA, 0x11, 0x1E, 0x01 };
|
|
|
|
class CapcomIceNetService;
|
|
|
|
class CapcomDirect
|
|
{
|
|
public:
|
|
CapcomDirect();
|
|
~CapcomDirect();
|
|
|
|
private:
|
|
void init();
|
|
void term();
|
|
void onLoadGame();
|
|
void startCapcomDirectService();
|
|
void stopCapcomDirectService();
|
|
void onNetworkChange();
|
|
static void onEvent(Event event, void *arg);
|
|
void resetModemCallbacks();
|
|
std::string getBattleCode();
|
|
|
|
bool active = false;
|
|
bool caller = false;
|
|
uint8_t gameId = 0;
|
|
std::unique_ptr<CapcomIceNetService> capcomService;
|
|
};
|
|
|
|
static CapcomDirect capcom;
|
|
|
|
class CapcomIceNetService : public Service
|
|
{
|
|
public:
|
|
bool start() override {
|
|
// Fake
|
|
return true;
|
|
}
|
|
|
|
void realStart()
|
|
{
|
|
juice_set_log_level(JUICE_LOG_LEVEL_INFO);
|
|
juice_set_log_handler(juiceLogHandler);
|
|
|
|
initStatus = std::async(std::launch::async, [this]()
|
|
{
|
|
ice::STUNConfig cfg = ice::STUNConfig::get();
|
|
|
|
juice_config_t config {};
|
|
config.concurrency_mode = JUICE_CONCURRENCY_MODE_POLL;
|
|
config.stun_server_host = cfg.stun_host.c_str();
|
|
config.stun_server_port = cfg.stun_port;
|
|
|
|
juice_turn_server_t turnServer {};
|
|
turnServer.host = cfg.turn_host.c_str();
|
|
turnServer.port = cfg.turn_port;
|
|
turnServer.username = cfg.turn_username.c_str();
|
|
turnServer.password = cfg.turn_password.c_str();
|
|
config.turn_servers = &turnServer;
|
|
config.turn_servers_count = 1;
|
|
|
|
config.cb_state_changed = [](juice_agent_t *agent, juice_state_t state, void *user_ptr) {
|
|
((CapcomIceNetService *)user_ptr)->onJuiceStateChanged(state);
|
|
};
|
|
config.cb_recv = [](juice_agent_t *agent, const char *data, size_t size, void *user_ptr) {
|
|
((CapcomIceNetService *)user_ptr)->onJuiceReceive(data, size);
|
|
};
|
|
config.cb_gathering_done = [](juice_agent_t *agent, void *user_ptr) {
|
|
((CapcomIceNetService *)user_ptr)->onJuiceGatheringDone();
|
|
};
|
|
config.user_ptr = this;
|
|
|
|
agent = juice_create(&config);
|
|
if (agent == nullptr) {
|
|
ERROR_LOG(NETWORK, "Juice agent creation failed");
|
|
return false;
|
|
}
|
|
juice_gather_candidates(agent);
|
|
state = Gathering;
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void sendRegister(int game, bool caller, u32 battleCode)
|
|
{
|
|
this->caller = caller;
|
|
serverConn = std::make_unique<BattleServerConn>(*this);
|
|
serverConn->connect(game, caller, battleCode);
|
|
}
|
|
|
|
void stop() override
|
|
{
|
|
if (state == Connected)
|
|
{
|
|
INFO_LOG(NETWORK, "Stopping Capcom Net service");
|
|
flushTxQueue();
|
|
sendStop();
|
|
state = Closed;
|
|
txBuffering.stop();
|
|
if (getJuiceAgent() != nullptr) {
|
|
juice_destroy(agent);
|
|
agent = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void realStop()
|
|
{
|
|
state = Connected; // make sure stop() does stop
|
|
stop();
|
|
if (serverConn != nullptr) {
|
|
serverConn->disconnect();
|
|
serverConn.reset();
|
|
}
|
|
}
|
|
|
|
void sendStart()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> _(mutex);
|
|
if (state != Syncing) {
|
|
startSent = true;
|
|
return;
|
|
}
|
|
}
|
|
if (serverConn == nullptr)
|
|
ERROR_LOG(NETWORK, "Can't send Start: serverConn is null");
|
|
else
|
|
serverConn->sendStart();
|
|
}
|
|
|
|
bool receiveStart()
|
|
{
|
|
if (serverConn == nullptr)
|
|
return false;
|
|
return serverConn->receiveStart();
|
|
}
|
|
|
|
void sendStop()
|
|
{
|
|
u8 data[6] {};
|
|
memcpy(data, PKT_MAGIC, sizeof(PKT_MAGIC));
|
|
data[4] = Stop;
|
|
juice_send(getJuiceAgent(), (const char *)data, sizeof(data));
|
|
}
|
|
|
|
void writeModem(u8 b) override
|
|
{
|
|
// Games packet sizes (V.42 payload):
|
|
// mvsc2 4
|
|
// pstone2 34
|
|
// sf3strike3 16
|
|
// sfzero3 16
|
|
// spawn 20
|
|
// vampire 6
|
|
// tennis 20
|
|
// cvspro 16
|
|
// jojo 11, 17 or 23
|
|
// pjustice 16
|
|
// ssf8 16
|
|
// techromancer 20
|
|
// taisenng 28
|
|
// hmgeo 12
|
|
// puzzle 16
|
|
|
|
if (state == Closed)
|
|
throw ConnectionClosed();
|
|
txQueue.push(b);
|
|
txBuffering.reset();
|
|
if (state == Syncing)
|
|
state = Connected;
|
|
}
|
|
|
|
void flushTxQueue()
|
|
{
|
|
if (txQueue.empty()
|
|
|| (state != Syncing && state != Connected))
|
|
return;
|
|
std::string data;
|
|
data.reserve(txQueue.size());
|
|
while (!txQueue.empty())
|
|
data.push_back((char)txQueue.pop());
|
|
int ret = juice_send(getJuiceAgent(), data.c_str(), data.size());
|
|
if (ret)
|
|
WARN_LOG(NETWORK, "juice send failed: %d", ret);
|
|
}
|
|
|
|
int readModem() override
|
|
{
|
|
if (rxQueue.empty())
|
|
{
|
|
if (state == Closed)
|
|
throw ConnectionClosed();
|
|
else
|
|
return -1;
|
|
}
|
|
u8 c = rxQueue.pop();
|
|
if (state == Syncing)
|
|
state = Connected;
|
|
return c;
|
|
}
|
|
int modemAvailable() override
|
|
{
|
|
if (rxQueue.empty() && state == Closed)
|
|
throw ConnectionClosed();
|
|
return rxQueue.size();
|
|
}
|
|
|
|
juice_agent_t *getJuiceAgent()
|
|
{
|
|
if (initStatus.valid())
|
|
if (!initStatus.get())
|
|
return nullptr;
|
|
return agent;
|
|
}
|
|
|
|
private:
|
|
void onJuiceGatheringDone()
|
|
{
|
|
if (agent == nullptr)
|
|
return;
|
|
char sdp[JUICE_MAX_SDP_STRING_LEN];
|
|
juice_get_local_description(agent, sdp, JUICE_MAX_SDP_STRING_LEN);
|
|
|
|
if (serverConn == nullptr) {
|
|
ERROR_LOG(NETWORK, "Can't send candidates: serverConn is null");
|
|
return;
|
|
}
|
|
serverConn->sendCandidates(sdp);
|
|
}
|
|
|
|
void onJuiceStateChanged(juice_state_t state)
|
|
{
|
|
if (state == JUICE_STATE_COMPLETED)
|
|
{
|
|
char local[128] {};
|
|
juice_get_selected_candidates(agent, local, sizeof(local), NULL, 0);
|
|
INFO_LOG(NETWORK, "Juice connection completed: using %s", local);
|
|
std::string connType;
|
|
if (strstr(local, "typ host") != nullptr)
|
|
connType = i18n::Ts("Direct peer to peer");
|
|
else if (strstr(local, "typ relay") != nullptr)
|
|
connType = i18n::Ts("Using relay");
|
|
else
|
|
// typ srflx: server reflexive (NAT)
|
|
// typ prflx: peer reflexive (NAT)
|
|
connType = i18n::Ts("NAT");
|
|
os_notify(i18n::T("Connected to peer"), 5000, connType.c_str());
|
|
|
|
bool startSendNow = false;
|
|
{
|
|
std::lock_guard<std::mutex> _(mutex);
|
|
startSendNow = startSent;
|
|
this->state = Syncing;
|
|
}
|
|
if (!caller && startSendNow)
|
|
{
|
|
if (serverConn == nullptr) {
|
|
ERROR_LOG(NETWORK, "Can't send start: serverConn is null");
|
|
return;
|
|
}
|
|
serverConn->sendStart();
|
|
}
|
|
txBuffering.start();
|
|
}
|
|
else if (state == JUICE_STATE_FAILED)
|
|
{
|
|
WARN_LOG(NETWORK, "ICE connection failed or closed");
|
|
this->state = Closed;
|
|
}
|
|
}
|
|
|
|
void onJuiceReceive(const char *data, size_t len)
|
|
{
|
|
if (len >= 6 && data[4] == Stop && !memcmp(data, PKT_MAGIC, sizeof(PKT_MAGIC)))
|
|
{
|
|
if (state == Connected) {
|
|
INFO_LOG(NETWORK, "Stop received from peer");
|
|
state = Closed;
|
|
}
|
|
return;
|
|
}
|
|
while (len--)
|
|
rxQueue.push(*data++);
|
|
}
|
|
|
|
static void juiceLogHandler(juice_log_level_t jlevel, const char *message)
|
|
{
|
|
LogTypes::LOG_LEVELS level;
|
|
switch (jlevel)
|
|
{
|
|
case JUICE_LOG_LEVEL_NONE:
|
|
case JUICE_LOG_LEVEL_VERBOSE:
|
|
case JUICE_LOG_LEVEL_DEBUG:
|
|
default:
|
|
level = LogTypes::LOG_LEVELS::LDEBUG;
|
|
break;
|
|
case JUICE_LOG_LEVEL_INFO:
|
|
level = LogTypes::LOG_LEVELS::LINFO;
|
|
break;
|
|
case JUICE_LOG_LEVEL_WARN:
|
|
if (strstr(message, "TURN CreatePermission") != nullptr)
|
|
// error pops up a lot with standard.relay.metered.ca
|
|
level = LogTypes::LOG_LEVELS::LINFO;
|
|
else
|
|
level = LogTypes::LOG_LEVELS::LWARNING;
|
|
break;
|
|
case JUICE_LOG_LEVEL_ERROR:
|
|
case JUICE_LOG_LEVEL_FATAL:
|
|
level = LogTypes::LOG_LEVELS::LERROR;
|
|
break;
|
|
}
|
|
GenericLog(level, LogTypes::LOG_TYPE::NETWORK, __FILE__, __LINE__, "%s", message);
|
|
}
|
|
|
|
class BattleServerConn
|
|
{
|
|
public:
|
|
BattleServerConn(CapcomIceNetService& service)
|
|
: service(service)
|
|
{}
|
|
|
|
void connect(int game, bool caller, u32 battleCode)
|
|
{
|
|
io_context = std::make_unique<asio::io_context>();
|
|
socket = std::make_unique<asio::ip::tcp::socket>(*io_context);
|
|
thread = std::thread([this, game, caller, battleCode]() {
|
|
try {
|
|
// Resolve host name
|
|
std::string hostname = "capcom.flyca.st";
|
|
asio::ip::tcp::resolver resolver(*io_context);
|
|
asio::error_code ec;
|
|
auto it = resolver.resolve(hostname, std::to_string(7658), ec);
|
|
if (ec)
|
|
throw FlycastException(ec.message());
|
|
if (it.empty())
|
|
throw FlycastException(i18n::Ts("Host not found"));
|
|
asio::ip::tcp::endpoint endpoint = *it.begin();
|
|
|
|
// Connect to host
|
|
socket->connect(endpoint, ec);
|
|
if (ec)
|
|
throw FlycastException(ec.message());
|
|
os_notify(i18n::T("Connected to battle server"), 5000, hostname.c_str());
|
|
receive();
|
|
sendRegister(game, caller, battleCode);
|
|
|
|
io_context->run();
|
|
} catch (const FlycastException& e) {
|
|
ERROR_LOG(NETWORK, "Capcom connection error: %s", e.what());
|
|
os_notify(i18n::T("Can't connect to battle server"), 8000, e.what());
|
|
} catch (const std::runtime_error& e) {
|
|
ERROR_LOG(NETWORK, "BattleServerConn::thread: error: %s", e.what());
|
|
}
|
|
});
|
|
}
|
|
|
|
void disconnect()
|
|
{
|
|
close();
|
|
socket.reset();
|
|
if (io_context != nullptr)
|
|
{
|
|
io_context->stop();
|
|
thread.join();
|
|
io_context.reset();
|
|
}
|
|
}
|
|
|
|
void sendStart()
|
|
{
|
|
if (io_context == nullptr)
|
|
return;
|
|
io_context->post([this]() {
|
|
outPacket.insert(outPacket.end(), std::begin(PKT_MAGIC), std::end(PKT_MAGIC));
|
|
outPacket.push_back(2);
|
|
outPacket.push_back(0);
|
|
send();
|
|
});
|
|
}
|
|
|
|
void sendCandidates(const char *cands)
|
|
{
|
|
if (io_context == nullptr)
|
|
return;
|
|
std::string candidates = cands;
|
|
io_context->post([this, candidates]() {
|
|
outPacket.insert(outPacket.end(), std::begin(PKT_MAGIC), std::end(PKT_MAGIC));
|
|
outPacket.push_back(3);
|
|
size_t len = candidates.size();
|
|
if (len <= 0xfe) {
|
|
outPacket.push_back(len);
|
|
}
|
|
else
|
|
{
|
|
outPacket.push_back(0xff);
|
|
outPacket.push_back(len & 0xff);
|
|
outPacket.push_back(len >> 8);
|
|
}
|
|
outPacket.insert(outPacket.end(), (const u8 *)&candidates[0], (const u8 *)&candidates[len]);
|
|
send();
|
|
});
|
|
}
|
|
|
|
bool receiveStart() const {
|
|
return startReceived;
|
|
}
|
|
|
|
private:
|
|
void sendRegister(int game, bool caller, u32 battleCode)
|
|
{
|
|
outPacket.insert(outPacket.end(), std::begin(PKT_MAGIC), std::end(PKT_MAGIC));
|
|
outPacket.push_back(Register);
|
|
outPacket.push_back(6);
|
|
outPacket.push_back(game);
|
|
outPacket.push_back(caller);
|
|
outPacket.insert(outPacket.end(), (const u8 *)&battleCode, (const u8 *)&battleCode + 4);
|
|
send();
|
|
}
|
|
|
|
using iterator = asio::buffers_iterator<asio::const_buffers_1>;
|
|
|
|
std::pair<iterator, bool>
|
|
static packetMatcher(iterator begin, iterator end)
|
|
{
|
|
if (end - begin < 6)
|
|
return std::make_pair(begin, false);
|
|
iterator it = begin;
|
|
u16 len = (u8)*(it + 5);
|
|
if (len == 0xff)
|
|
{
|
|
if (end - begin < 8)
|
|
return std::make_pair(begin, false);
|
|
len = (u8)*(it + 6) | ((u8)*(it + 7) << 8);
|
|
len += 2;
|
|
}
|
|
len += 6;
|
|
if (end - begin < len)
|
|
return std::make_pair(begin, false);
|
|
else
|
|
return std::make_pair(begin + len, true);
|
|
}
|
|
|
|
void receive()
|
|
{
|
|
asio::async_read_until(*socket, asio::dynamic_buffer(packet, 64_KB), packetMatcher,
|
|
std::bind(&BattleServerConn::handlePacket, this,
|
|
asio::placeholders::error,
|
|
asio::placeholders::bytes_transferred));
|
|
}
|
|
void handlePacket(const std::error_code& ec, size_t len)
|
|
{
|
|
if (ec || len == 0)
|
|
{
|
|
if (ec && ec != asio::error::eof && ec != asio::error::operation_aborted)
|
|
ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str());
|
|
close();
|
|
return;
|
|
}
|
|
if (memcmp(packet.data(), PKT_MAGIC, sizeof(PKT_MAGIC)))
|
|
{
|
|
ERROR_LOG(NETWORK, "Invalid magic number in packet");
|
|
close();
|
|
return;
|
|
}
|
|
switch (packet[4])
|
|
{
|
|
case Start:
|
|
startReceived = true;
|
|
break;
|
|
case Candidates:
|
|
{
|
|
unsigned start;
|
|
if (packet[5] == 0xff)
|
|
start = 8;
|
|
else
|
|
start = 6;
|
|
std::string candidates = std::string((const char *)&packet[start], (const char *)&packet[len]);
|
|
INFO_LOG(NETWORK, "Peer candidates: %s", candidates.c_str());
|
|
juice_set_remote_description(service.getJuiceAgent(), candidates.c_str());
|
|
break;
|
|
}
|
|
default:
|
|
ERROR_LOG(NETWORK, "Invalid packet type: %d", packet[4]);
|
|
break;
|
|
}
|
|
packet.erase(packet.begin(), packet.begin() + len);
|
|
receive();
|
|
}
|
|
|
|
void send()
|
|
{
|
|
if (sending)
|
|
return;
|
|
sending = true;
|
|
asio::async_write(*socket, asio::buffer(outPacket),
|
|
std::bind(&BattleServerConn::onSent, this,
|
|
asio::placeholders::error,
|
|
asio::placeholders::bytes_transferred));
|
|
}
|
|
void onSent(const std::error_code& ec, size_t len)
|
|
{
|
|
if (ec || len == 0)
|
|
{
|
|
if (ec && ec != asio::error::eof)
|
|
WARN_LOG(COMMON, "Write error: %s", ec.message().c_str());
|
|
close();
|
|
return;
|
|
}
|
|
sending = false;
|
|
if (len < outPacket.size()) {
|
|
outPacket.erase(outPacket.begin(), outPacket.begin() + len);
|
|
send();
|
|
}
|
|
else {
|
|
outPacket.clear();
|
|
}
|
|
}
|
|
|
|
void close()
|
|
{
|
|
if (socket != nullptr) {
|
|
std::error_code ignored;
|
|
socket->close(ignored);
|
|
}
|
|
}
|
|
|
|
CapcomIceNetService& service;
|
|
std::thread thread;
|
|
std::unique_ptr<asio::io_context> io_context;
|
|
std::unique_ptr<asio::ip::tcp::socket> socket;
|
|
std::vector<u8> packet;
|
|
std::vector<u8> outPacket;
|
|
bool sending = false;
|
|
bool startReceived = false;
|
|
};
|
|
|
|
class TxBuffering
|
|
{
|
|
public:
|
|
TxBuffering(CapcomIceNetService& service, int timeoutMs)
|
|
: service(service), timeoutMs(timeoutMs)
|
|
{}
|
|
|
|
void start()
|
|
{
|
|
stopping = false;
|
|
thread = std::thread([this]() {
|
|
while (true)
|
|
{
|
|
std::unique_lock lk(mutex);
|
|
condvar.wait_for(lk, std::chrono::milliseconds(timeoutMs));
|
|
if (stopping)
|
|
break;
|
|
if (resetting) {
|
|
resetting = false;
|
|
continue;
|
|
}
|
|
lk.unlock();
|
|
service.flushTxQueue();
|
|
}
|
|
});
|
|
}
|
|
void stop()
|
|
{
|
|
if (thread.joinable())
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> _(mutex);
|
|
stopping = true;
|
|
}
|
|
condvar.notify_one();
|
|
thread.join();
|
|
}
|
|
}
|
|
void reset()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> _(mutex);
|
|
resetting = true;
|
|
}
|
|
condvar.notify_one();
|
|
}
|
|
|
|
private:
|
|
CapcomIceNetService& service;
|
|
const int timeoutMs;
|
|
std::thread thread;
|
|
std::mutex mutex;
|
|
std::condition_variable condvar;
|
|
bool stopping = false;
|
|
bool resetting = false;
|
|
};
|
|
|
|
juice_agent_t *agent = nullptr;
|
|
enum { Init, Gathering, Syncing, Connected, Closed }
|
|
state {};
|
|
std::future<bool> initStatus;
|
|
TsQueue<u8> rxQueue;
|
|
TsQueue<u8> txQueue;
|
|
TxBuffering txBuffering { *this, 2 };
|
|
std::unique_ptr<BattleServerConn> serverConn;
|
|
bool caller = false;
|
|
bool startSent = false;
|
|
std::mutex mutex;
|
|
};
|
|
|
|
CapcomDirect::CapcomDirect() {
|
|
init();
|
|
}
|
|
CapcomDirect::~CapcomDirect() {
|
|
term();
|
|
}
|
|
void CapcomDirect::init()
|
|
{
|
|
EventManager::listen(Event::Start, onEvent, this);
|
|
EventManager::listen(Event::Terminate, onEvent, this);
|
|
EventManager::listen(Event::Network, onEvent, this);
|
|
EventManager::listen(Event::LoadState, onEvent, this);
|
|
}
|
|
void CapcomDirect::term()
|
|
{
|
|
EventManager::unlisten(Event::Start, onEvent, this);
|
|
EventManager::unlisten(Event::Terminate, onEvent, this);
|
|
EventManager::unlisten(Event::Network, onEvent, this);
|
|
EventManager::unlisten(Event::LoadState, onEvent, this);
|
|
}
|
|
void CapcomDirect::onLoadGame()
|
|
{
|
|
gameId = 0;
|
|
for (unsigned i = 0; i < std::size(games); i++)
|
|
if (games[i] == settings.content.gameId) {
|
|
gameId = i + 1;
|
|
break;
|
|
}
|
|
if (gameId == 0 && settings.content.gameId == "T1217M")
|
|
// Capcom vs SNK Millennium Fight and M.F. Pro use the same game code
|
|
gameId = 8;
|
|
active = gameId != 0;
|
|
}
|
|
|
|
void CapcomDirect::startCapcomDirectService()
|
|
{
|
|
INFO_LOG(NETWORK, "Capcom network service started");
|
|
capcomService = std::make_unique<CapcomIceNetService>();
|
|
setCustomService(capcomService.get());
|
|
capcomService->realStart();
|
|
std::string battleCode = getBattleCode();
|
|
INFO_LOG(NETWORK, "Capcom battle code: %s (%s)", battleCode.c_str(), caller ? "caller" : "callee");
|
|
u32 shortCode = strtoul(battleCode.substr(0, 8).c_str(), nullptr, 16);
|
|
capcomService->sendRegister(gameId, caller, shortCode);
|
|
if (caller)
|
|
// Keep the phone ringing until we receive Start from the callee
|
|
modemKeepRinging(true);
|
|
modemPeriodicCallback(100_sh4ms, [this]() {
|
|
if (capcomService->receiveStart())
|
|
{
|
|
INFO_LOG(NETWORK, "%s: Start received", caller ? "caller" : "callee");
|
|
modemPeer2Peer(true);
|
|
if (caller) {
|
|
// Pick up the phone and start the handshake
|
|
modemKeepRinging(false);
|
|
}
|
|
else
|
|
{
|
|
// Simulate an incoming call (ringing)
|
|
modemIncomingCall();
|
|
// Wait for the handshake to begin, then send Start to the caller
|
|
modemOnHandshake([this]() {
|
|
capcomService->sendStart();
|
|
modemOnHandshake({});
|
|
});
|
|
}
|
|
modemPeriodicCallback(0, {});
|
|
}
|
|
});
|
|
}
|
|
void CapcomDirect::stopCapcomDirectService()
|
|
{
|
|
if (active)
|
|
resetModemCallbacks();
|
|
if (capcomService != nullptr)
|
|
{
|
|
setCustomService(nullptr);
|
|
capcomService->realStop();
|
|
capcomService.reset();
|
|
INFO_LOG(NETWORK, "Capcom network service stopped");
|
|
}
|
|
}
|
|
|
|
void CapcomDirect::onNetworkChange()
|
|
{
|
|
if (!active)
|
|
return;
|
|
if (!settings.network.online && capcomService == nullptr)
|
|
{
|
|
// detect answering mode
|
|
modemOnAnswerMode([this]() {
|
|
caller = false;
|
|
startCapcomDirectService();
|
|
modemOnAnswerMode({});
|
|
});
|
|
// detect calling party using phone#
|
|
modemOnDial([this](const std::string& number) {
|
|
if (number == "20130201")
|
|
{
|
|
caller = true;
|
|
startCapcomDirectService();
|
|
modemOnAnswerMode({});
|
|
}
|
|
else {
|
|
stopCapcomDirectService();
|
|
}
|
|
});
|
|
modemPeer2Peer(false);
|
|
}
|
|
}
|
|
|
|
void CapcomDirect::onEvent(Event event, void *arg)
|
|
{
|
|
CapcomDirect *self = (CapcomDirect *)arg;
|
|
switch (event)
|
|
{
|
|
case Event::Start:
|
|
self->onLoadGame();
|
|
break;
|
|
case Event::Terminate:
|
|
self->stopCapcomDirectService();
|
|
self->active = false;
|
|
break;
|
|
case Event::Network:
|
|
self->onNetworkChange();
|
|
break;
|
|
case Event::LoadState:
|
|
self->stopCapcomDirectService();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CapcomDirect::resetModemCallbacks()
|
|
{
|
|
modemOnAnswerMode({});
|
|
modemOnDial({});
|
|
modemOnHandshake({});
|
|
modemPeriodicCallback(0, {});
|
|
modemPeer2Peer(false);
|
|
modemKeepRinging(false);
|
|
}
|
|
|
|
std::string CapcomDirect::getBattleCode()
|
|
{
|
|
constexpr u32 addresses[] {
|
|
0x8c2b522c, // mvsc2
|
|
0x0c81d410, // pstone2
|
|
0x8ceadbd4, // sf3strike3
|
|
0x8c223b54, // sfzero3
|
|
0x0c2de580, // spawn
|
|
0x8c340c68, // vampire
|
|
0x0c237a1c, // tennis
|
|
0x8c30f46c, // cvsmfpro
|
|
0x8c8997c8, // jojo
|
|
0x0c3f5974, // pjustice
|
|
0x8c31e6c8, // ssf2
|
|
0x8c4af628, // tech romancer
|
|
0x8c42b320, // taisen net gimmick
|
|
0x0c2d6d00, // hmgeo
|
|
0x0c2450dc, // puzzle2
|
|
};
|
|
if (gameId == 0 || gameId > std::size(addresses))
|
|
return {};
|
|
u32 address;
|
|
if (settings.content.gameId == "T1217M")
|
|
{
|
|
// cvsmf v1 or v2
|
|
if (ip_meta.product_version[1] == '1')
|
|
address = 0x8c316e2c;
|
|
else
|
|
address = 0x8c31762c;
|
|
}
|
|
else {
|
|
address = addresses[(int)gameId - 1];
|
|
}
|
|
std::string battleCode;
|
|
for (int i = 0; i < 14; i++)
|
|
battleCode += addrspace::read8(address + i);
|
|
return battleCode;
|
|
}
|
|
|
|
} // namespace
|
|
#endif // !LIBRETRO || !ANDROID
|