/*
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 .
*/
#include "types.h"
#include
#include "rawmodem.h"
#include "emulator.h"
#include "util/tsqueue.h"
#include "oslib/i18n.h"
#include "oslib/oslib.h"
#include "cfg/cfg.h"
// Inspired by serialbridge.cpp by awarmplace (https://github.com/awarmplace)
namespace net::modbba
{
static TsQueue toModem;
class GameSocket
{
public:
GameSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint,
const std::string& endpointName = "")
: socket(io_context)
{
asio::error_code ec;
socket.connect(endpoint, ec);
if (ec)
throw FlycastException(ec.message());
os_notify(i18n::T("Connected to game server"), 5000, endpointName.c_str());
receive();
}
void send(u8 b)
{
if (sendBufSize == sendBuffer.size()) {
WARN_LOG(NETWORK, "Output buffer overflow");
return;
}
sendBuffer[sendBufSize++] = b;
doSend();
}
protected:
void receive()
{
socket.async_read_some(asio::buffer(recvBuffer),
[this](const std::error_code& ec, size_t len)
{
if (ec || len == 0)
{
if (ec && ec != asio::error::eof)
ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str());
close();
return;
}
for (size_t i = 0; i < len; i++)
toModem.push(recvBuffer[i]);
receive();
});
}
void doSend()
{
if (sending)
return;
sending = true;
asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize),
[this](const std::error_code& ec, size_t len)
{
if (ec)
{
ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str());
close();
return;
}
sending = false;
sendBufSize -= len;
if (sendBufSize > 0) {
memmove(&sendBuffer[0], &sendBuffer[len], sendBufSize);
doSend();
}
});
}
void close() {
std::error_code ignored;
socket.close(ignored);
}
asio::ip::tcp::socket socket;
std::array recvBuffer;
std::array sendBuffer;
u32 sendBufSize = 0;
bool sending = false;
};
class RawModemThread
{
public:
void start(uint16_t port)
{
if (thread.joinable())
return;
this->port = port;
io_context = std::make_unique();
thread = std::thread(&RawModemThread::run, this);
}
void stop()
{
if (!thread.joinable())
return;
io_context->stop();
thread.join();
gameSocket.reset();
io_context.reset();
os_notify(i18n::T("Network disconnected"), 3000);
}
void sendModem(u8 v)
{
if (io_context == nullptr)
return;
io_context->post([this, v]() {
if (gameSocket != nullptr)
gameSocket->send(v);
});
}
private:
void run();
void connect(const std::string& hostname);
uint16_t port = 0;
std::thread thread;
std::unique_ptr io_context;
std::unique_ptr gameSocket;
friend class RawModemService;
};
static RawModemThread thread;
void RawModemThread::run()
{
toModem.clear();
try {
#ifndef LIBRETRO
connect(config::loadStr("network", "DCNetServer", "dcnet.flyca.st"));
#else
connect("dcnet.flyca.st");
#endif
io_context->run();
} catch (const FlycastException& e) {
ERROR_LOG(NETWORK, "RawModem connection error: %s", e.what());
os_notify(i18n::T("Can't connect to game server"), 8000, e.what());
} catch (const std::runtime_error& e) {
ERROR_LOG(NETWORK, "RawModemThread::run error: %s", e.what());
}
}
void RawModemThread::connect(const std::string& hostname)
{
asio::ip::tcp::resolver resolver(*io_context);
asio::error_code ec;
auto it = resolver.resolve(hostname, std::to_string(port), ec);
if (ec)
throw FlycastException(ec.message());
if (it.empty())
throw FlycastException(i18n::Ts("Host not found"));
asio::ip::tcp::endpoint endpoint = *it.begin();
gameSocket = std::make_unique(*io_context, endpoint, hostname);
}
bool RawModemService::start()
{
emu.setNetworkState(true);
thread.start(port);
return true;
}
void RawModemService::stop()
{
thread.stop();
emu.setNetworkState(false);
}
void RawModemService::writeModem(u8 b) {
thread.sendModem(b);
}
int RawModemService::readModem()
{
if (toModem.empty())
return -1;
else
return toModem.pop();
}
int RawModemService::modemAvailable() {
return toModem.size();
}
} // namespace net::modbba