added basic network play
This commit is contained in:
21
src/network/CoopNetButtons.h
Normal file
21
src/network/CoopNetButtons.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace coopnet {
|
||||
// 8-bit input mask carried in NetSession::InputFrame.
|
||||
// Keep in sync across capture/apply on both peers.
|
||||
enum Buttons : uint8_t {
|
||||
MoveLeft = 1u << 0,
|
||||
MoveRight = 1u << 1,
|
||||
SoftDrop = 1u << 2,
|
||||
RotCW = 1u << 3,
|
||||
RotCCW = 1u << 4,
|
||||
HardDrop = 1u << 5,
|
||||
Hold = 1u << 6,
|
||||
};
|
||||
|
||||
inline bool has(uint8_t mask, Buttons b) {
|
||||
return (mask & static_cast<uint8_t>(b)) != 0;
|
||||
}
|
||||
}
|
||||
324
src/network/NetSession.cpp
Normal file
324
src/network/NetSession.cpp
Normal file
@ -0,0 +1,324 @@
|
||||
#include "NetSession.h"
|
||||
|
||||
#include <enet/enet.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t kChannelReliable = 0;
|
||||
|
||||
static bool netLogVerboseEnabled() {
|
||||
// Set environment variable / hint: SPACETRIS_NET_LOG=1
|
||||
const char* v = SDL_GetHint("SPACETRIS_NET_LOG");
|
||||
return v && v[0] == '1';
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void append(std::vector<uint8_t>& out, const T& value) {
|
||||
const uint8_t* p = reinterpret_cast<const uint8_t*>(&value);
|
||||
out.insert(out.end(), p, p + sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static bool read(const uint8_t* data, size_t size, size_t& off, T& out) {
|
||||
if (off + sizeof(T) > size) return false;
|
||||
std::memcpy(&out, data + off, sizeof(T));
|
||||
off += sizeof(T);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
NetSession::NetSession() = default;
|
||||
|
||||
NetSession::~NetSession() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
bool NetSession::ensureEnetInitialized() {
|
||||
static bool s_inited = false;
|
||||
if (s_inited) return true;
|
||||
if (enet_initialize() != 0) {
|
||||
setError("enet_initialize failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
s_inited = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NetSession::setError(const std::string& msg) {
|
||||
m_lastError = msg;
|
||||
}
|
||||
|
||||
bool NetSession::host(const std::string& bindHost, uint16_t port) {
|
||||
shutdown();
|
||||
if (!ensureEnetInitialized()) return false;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] host(bind='%s', port=%u)", bindHost.c_str(), (unsigned)port);
|
||||
|
||||
ENetAddress address{};
|
||||
address.host = ENET_HOST_ANY;
|
||||
address.port = port;
|
||||
|
||||
if (!bindHost.empty() && bindHost != "0.0.0.0") {
|
||||
if (enet_address_set_host(&address, bindHost.c_str()) != 0) {
|
||||
setError("enet_address_set_host (bind) failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 1 peer, 2 channels (reserve extra)
|
||||
m_host = enet_host_create(&address, 1, 2, 0, 0);
|
||||
if (!m_host) {
|
||||
setError("enet_host_create (host) failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_mode = Mode::Host;
|
||||
m_state = ConnState::Connecting;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NetSession::join(const std::string& hostNameOrIp, uint16_t port) {
|
||||
shutdown();
|
||||
if (!ensureEnetInitialized()) return false;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] join(remote='%s', port=%u)", hostNameOrIp.c_str(), (unsigned)port);
|
||||
|
||||
m_host = enet_host_create(nullptr, 1, 2, 0, 0);
|
||||
if (!m_host) {
|
||||
setError("enet_host_create (client) failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
ENetAddress address{};
|
||||
if (enet_address_set_host(&address, hostNameOrIp.c_str()) != 0) {
|
||||
setError("enet_address_set_host failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
address.port = port;
|
||||
|
||||
m_peer = enet_host_connect(m_host, &address, 2, 0);
|
||||
if (!m_peer) {
|
||||
setError("enet_host_connect failed");
|
||||
m_state = ConnState::Error;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_mode = Mode::Client;
|
||||
m_state = ConnState::Connecting;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NetSession::shutdown() {
|
||||
if (m_host || m_peer) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] shutdown(mode=%d state=%d)", (int)m_mode, (int)m_state);
|
||||
}
|
||||
|
||||
m_remoteInputs.clear();
|
||||
m_remoteHashes.clear();
|
||||
m_receivedHandshake.reset();
|
||||
|
||||
m_inputsSent = 0;
|
||||
m_inputsReceived = 0;
|
||||
m_hashesSent = 0;
|
||||
m_hashesReceived = 0;
|
||||
m_handshakesSent = 0;
|
||||
m_handshakesReceived = 0;
|
||||
m_lastRecvInputTick = 0xFFFFFFFFu;
|
||||
m_lastRecvHashTick = 0xFFFFFFFFu;
|
||||
m_lastStatsLogMs = 0;
|
||||
|
||||
if (m_peer) {
|
||||
enet_peer_disconnect(m_peer, 0);
|
||||
m_peer = nullptr;
|
||||
}
|
||||
|
||||
if (m_host) {
|
||||
enet_host_destroy(m_host);
|
||||
m_host = nullptr;
|
||||
}
|
||||
|
||||
m_mode = Mode::None;
|
||||
m_state = ConnState::Disconnected;
|
||||
m_lastError.clear();
|
||||
}
|
||||
|
||||
void NetSession::poll(uint32_t timeoutMs) {
|
||||
if (!m_host) return;
|
||||
|
||||
ENetEvent event{};
|
||||
while (enet_host_service(m_host, &event, static_cast<enet_uint32>(timeoutMs)) > 0) {
|
||||
switch (event.type) {
|
||||
case ENET_EVENT_TYPE_CONNECT:
|
||||
m_peer = event.peer;
|
||||
m_state = ConnState::Connected;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] connected (mode=%d)", (int)m_mode);
|
||||
break;
|
||||
case ENET_EVENT_TYPE_RECEIVE:
|
||||
if (event.packet) {
|
||||
handlePacket(event.packet->data, event.packet->dataLength);
|
||||
enet_packet_destroy(event.packet);
|
||||
}
|
||||
break;
|
||||
case ENET_EVENT_TYPE_DISCONNECT:
|
||||
m_peer = nullptr;
|
||||
m_state = ConnState::Disconnected;
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[NET] disconnected");
|
||||
break;
|
||||
case ENET_EVENT_TYPE_NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// After first event, do non-blocking passes.
|
||||
timeoutMs = 0;
|
||||
}
|
||||
|
||||
// Rate-limited stats log (opt-in)
|
||||
if (netLogVerboseEnabled()) {
|
||||
const uint32_t nowMs = SDL_GetTicks();
|
||||
if (m_lastStatsLogMs == 0) m_lastStatsLogMs = nowMs;
|
||||
if (nowMs - m_lastStatsLogMs >= 1000u) {
|
||||
m_lastStatsLogMs = nowMs;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET] stats: sent(in=%u hash=%u hs=%u) recv(in=%u hash=%u hs=%u) lastRecv(inTick=%u hashTick=%u) state=%d",
|
||||
m_inputsSent,
|
||||
m_hashesSent,
|
||||
m_handshakesSent,
|
||||
m_inputsReceived,
|
||||
m_hashesReceived,
|
||||
m_handshakesReceived,
|
||||
m_lastRecvInputTick,
|
||||
m_lastRecvHashTick,
|
||||
(int)m_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool NetSession::sendBytesReliable(const void* data, size_t size) {
|
||||
if (!m_peer) return false;
|
||||
ENetPacket* packet = enet_packet_create(data, size, ENET_PACKET_FLAG_RELIABLE);
|
||||
if (!packet) return false;
|
||||
if (enet_peer_send(m_peer, kChannelReliable, packet) != 0) {
|
||||
enet_packet_destroy(packet);
|
||||
return false;
|
||||
}
|
||||
// Let the caller decide flush cadence; but for tiny control packets, flushing is cheap.
|
||||
enet_host_flush(m_host);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NetSession::sendHandshake(const Handshake& hs) {
|
||||
if (m_mode != Mode::Host) return false;
|
||||
m_handshakesSent++;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendHandshake(seed=%u startTick=%u startLevel=%u)", hs.rngSeed, hs.startTick, (unsigned)hs.startLevel);
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(1 + sizeof(uint32_t) * 2 + sizeof(uint8_t));
|
||||
buf.push_back(static_cast<uint8_t>(MsgType::Handshake));
|
||||
append(buf, hs.rngSeed);
|
||||
append(buf, hs.startTick);
|
||||
append(buf, hs.startLevel);
|
||||
return sendBytesReliable(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
std::optional<NetSession::Handshake> NetSession::takeReceivedHandshake() {
|
||||
auto out = m_receivedHandshake;
|
||||
m_receivedHandshake.reset();
|
||||
return out;
|
||||
}
|
||||
|
||||
bool NetSession::sendLocalInput(uint32_t tick, uint8_t buttons) {
|
||||
m_inputsSent++;
|
||||
if (netLogVerboseEnabled() && (tick % 60u) == 0u) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendInput(tick=%u buttons=0x%02X)", tick, (unsigned)buttons);
|
||||
}
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(1 + sizeof(uint32_t) + sizeof(uint8_t));
|
||||
buf.push_back(static_cast<uint8_t>(MsgType::Input));
|
||||
append(buf, tick);
|
||||
append(buf, buttons);
|
||||
return sendBytesReliable(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
std::optional<uint8_t> NetSession::getRemoteButtons(uint32_t tick) const {
|
||||
auto it = m_remoteInputs.find(tick);
|
||||
if (it == m_remoteInputs.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bool NetSession::sendStateHash(uint32_t tick, uint64_t hash) {
|
||||
m_hashesSent++;
|
||||
if (netLogVerboseEnabled()) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] sendHash(tick=%u hash=%llu)", tick, (unsigned long long)hash);
|
||||
}
|
||||
std::vector<uint8_t> buf;
|
||||
buf.reserve(1 + sizeof(uint32_t) + sizeof(uint64_t));
|
||||
buf.push_back(static_cast<uint8_t>(MsgType::Hash));
|
||||
append(buf, tick);
|
||||
append(buf, hash);
|
||||
return sendBytesReliable(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
std::optional<uint64_t> NetSession::takeRemoteHash(uint32_t tick) {
|
||||
auto it = m_remoteHashes.find(tick);
|
||||
if (it == m_remoteHashes.end()) return std::nullopt;
|
||||
uint64_t v = it->second;
|
||||
m_remoteHashes.erase(it);
|
||||
return v;
|
||||
}
|
||||
|
||||
void NetSession::handlePacket(const uint8_t* data, size_t size) {
|
||||
if (!data || size < 1) return;
|
||||
size_t off = 0;
|
||||
uint8_t typeByte = 0;
|
||||
if (!read(data, size, off, typeByte)) return;
|
||||
|
||||
MsgType t = static_cast<MsgType>(typeByte);
|
||||
switch (t) {
|
||||
case MsgType::Handshake: {
|
||||
Handshake hs{};
|
||||
if (!read(data, size, off, hs.rngSeed)) return;
|
||||
if (!read(data, size, off, hs.startTick)) return;
|
||||
if (!read(data, size, off, hs.startLevel)) return;
|
||||
m_receivedHandshake = hs;
|
||||
m_handshakesReceived++;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvHandshake(seed=%u startTick=%u startLevel=%u)", hs.rngSeed, hs.startTick, (unsigned)hs.startLevel);
|
||||
break;
|
||||
}
|
||||
case MsgType::Input: {
|
||||
uint32_t tick = 0;
|
||||
uint8_t buttons = 0;
|
||||
if (!read(data, size, off, tick)) return;
|
||||
if (!read(data, size, off, buttons)) return;
|
||||
m_remoteInputs[tick] = buttons;
|
||||
m_inputsReceived++;
|
||||
m_lastRecvInputTick = tick;
|
||||
if (netLogVerboseEnabled() && (tick % 60u) == 0u) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvInput(tick=%u buttons=0x%02X)", tick, (unsigned)buttons);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MsgType::Hash: {
|
||||
uint32_t tick = 0;
|
||||
uint64_t hash = 0;
|
||||
if (!read(data, size, off, tick)) return;
|
||||
if (!read(data, size, off, hash)) return;
|
||||
m_remoteHashes[tick] = hash;
|
||||
m_hashesReceived++;
|
||||
m_lastRecvHashTick = tick;
|
||||
if (netLogVerboseEnabled()) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET] recvHash(tick=%u hash=%llu)", tick, (unsigned long long)hash);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
118
src/network/NetSession.h
Normal file
118
src/network/NetSession.h
Normal file
@ -0,0 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
struct _ENetHost;
|
||||
struct _ENetPeer;
|
||||
|
||||
// Lockstep networking session for COOPERATE (network) mode.
|
||||
//
|
||||
// Design goals:
|
||||
// - Non-blocking polling (caller drives poll from the main loop)
|
||||
// - Reliable, ordered delivery for inputs and control messages
|
||||
// - Host provides seed + start tick (handshake)
|
||||
// - Only inputs/state hashes are exchanged (no board sync)
|
||||
class NetSession {
|
||||
public:
|
||||
enum class Mode {
|
||||
None,
|
||||
Host,
|
||||
Client,
|
||||
};
|
||||
|
||||
enum class ConnState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Error,
|
||||
};
|
||||
|
||||
struct Handshake {
|
||||
uint32_t rngSeed = 0;
|
||||
uint32_t startTick = 0;
|
||||
uint8_t startLevel = 0;
|
||||
};
|
||||
|
||||
struct InputFrame {
|
||||
uint32_t tick = 0;
|
||||
uint8_t buttons = 0;
|
||||
};
|
||||
|
||||
NetSession();
|
||||
~NetSession();
|
||||
|
||||
NetSession(const NetSession&) = delete;
|
||||
NetSession& operator=(const NetSession&) = delete;
|
||||
|
||||
// If bindHost is empty or "0.0.0.0", binds to ENET_HOST_ANY.
|
||||
bool host(const std::string& bindHost, uint16_t port);
|
||||
bool join(const std::string& hostNameOrIp, uint16_t port);
|
||||
void shutdown();
|
||||
|
||||
void poll(uint32_t timeoutMs = 0);
|
||||
|
||||
Mode mode() const { return m_mode; }
|
||||
ConnState state() const { return m_state; }
|
||||
bool isConnected() const { return m_state == ConnState::Connected; }
|
||||
|
||||
// Host-only: send handshake once the peer connects.
|
||||
bool sendHandshake(const Handshake& hs);
|
||||
|
||||
// Client-only: becomes available once received from host.
|
||||
std::optional<Handshake> takeReceivedHandshake();
|
||||
|
||||
// Input exchange --------------------------------------------------------
|
||||
// Send local input for a given simulation tick.
|
||||
bool sendLocalInput(uint32_t tick, uint8_t buttons);
|
||||
|
||||
// Returns the last received remote input for a tick (if any).
|
||||
std::optional<uint8_t> getRemoteButtons(uint32_t tick) const;
|
||||
|
||||
// Hash exchange (for desync detection) ---------------------------------
|
||||
bool sendStateHash(uint32_t tick, uint64_t hash);
|
||||
std::optional<uint64_t> takeRemoteHash(uint32_t tick);
|
||||
|
||||
// Diagnostics
|
||||
std::string lastError() const { return m_lastError; }
|
||||
|
||||
private:
|
||||
enum class MsgType : uint8_t {
|
||||
Handshake = 1,
|
||||
Input = 2,
|
||||
Hash = 3,
|
||||
};
|
||||
|
||||
bool ensureEnetInitialized();
|
||||
void setError(const std::string& msg);
|
||||
|
||||
bool sendBytesReliable(const void* data, size_t size);
|
||||
void handlePacket(const uint8_t* data, size_t size);
|
||||
|
||||
Mode m_mode = Mode::None;
|
||||
ConnState m_state = ConnState::Disconnected;
|
||||
|
||||
_ENetHost* m_host = nullptr;
|
||||
_ENetPeer* m_peer = nullptr;
|
||||
|
||||
std::string m_lastError;
|
||||
|
||||
std::optional<Handshake> m_receivedHandshake;
|
||||
|
||||
std::unordered_map<uint32_t, uint8_t> m_remoteInputs;
|
||||
std::unordered_map<uint32_t, uint64_t> m_remoteHashes;
|
||||
|
||||
// Debug logging (rate-limited)
|
||||
uint32_t m_inputsSent = 0;
|
||||
uint32_t m_inputsReceived = 0;
|
||||
uint32_t m_hashesSent = 0;
|
||||
uint32_t m_hashesReceived = 0;
|
||||
uint32_t m_handshakesSent = 0;
|
||||
uint32_t m_handshakesReceived = 0;
|
||||
uint32_t m_lastRecvInputTick = 0xFFFFFFFFu;
|
||||
uint32_t m_lastRecvHashTick = 0xFFFFFFFFu;
|
||||
uint32_t m_lastStatsLogMs = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user