added basic network play
This commit is contained in:
@ -49,6 +49,9 @@
|
||||
#include "graphics/ui/Font.h"
|
||||
#include "graphics/ui/HelpOverlay.h"
|
||||
|
||||
#include "network/CoopNetButtons.h"
|
||||
#include "network/NetSession.h"
|
||||
|
||||
#include "persistence/Scores.h"
|
||||
|
||||
#include "states/LevelSelectorState.h"
|
||||
@ -259,6 +262,12 @@ struct TetrisApp::Impl {
|
||||
double moveTimerMs = 0.0;
|
||||
double p1MoveTimerMs = 0.0;
|
||||
double p2MoveTimerMs = 0.0;
|
||||
|
||||
// Network coop fixed-tick state (used only when ctx.coopNetEnabled is true)
|
||||
double coopNetAccMs = 0.0;
|
||||
uint32_t coopNetCachedTick = 0xFFFFFFFFu;
|
||||
uint8_t coopNetCachedButtons = 0;
|
||||
uint32_t coopNetLastHashSentTick = 0xFFFFFFFFu;
|
||||
double DAS = 170.0;
|
||||
double ARR = 40.0;
|
||||
SDL_Rect logicalVP{0, 0, LOGICAL_W, LOGICAL_H};
|
||||
@ -1149,6 +1158,10 @@ void TetrisApp::Impl::runLoop()
|
||||
}
|
||||
}
|
||||
|
||||
// State transitions can be triggered from render/update (e.g. menu network handshake).
|
||||
// Keep our cached `state` in sync every frame, not only when events occur.
|
||||
state = stateMgr->getState();
|
||||
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
double frameMs = double(now - lastMs) * 1000.0 / double(SDL_GetPerformanceFrequency());
|
||||
lastMs = now;
|
||||
@ -1309,6 +1322,10 @@ void TetrisApp::Impl::runLoop()
|
||||
|
||||
if (game->isPaused()) {
|
||||
// While paused, suppress all continuous input changes so pieces don't drift.
|
||||
if (ctx.coopNetEnabled && ctx.coopNetSession) {
|
||||
ctx.coopNetSession->poll(0);
|
||||
ctx.coopNetStalled = false;
|
||||
}
|
||||
coopGame->setSoftDropping(CoopGame::PlayerSide::Left, false);
|
||||
coopGame->setSoftDropping(CoopGame::PlayerSide::Right, false);
|
||||
p1MoveTimerMs = 0.0;
|
||||
@ -1318,6 +1335,17 @@ void TetrisApp::Impl::runLoop()
|
||||
p2LeftHeld = false;
|
||||
p2RightHeld = false;
|
||||
} else {
|
||||
const bool coopNetActive = ctx.coopNetEnabled && ctx.coopNetSession;
|
||||
|
||||
// If we just entered network co-op, reset per-session fixed-tick bookkeeping.
|
||||
if (coopNetActive && coopNetCachedTick != 0xFFFFFFFFu && ctx.coopNetTick == 0u) {
|
||||
coopNetAccMs = 0.0;
|
||||
coopNetCachedTick = 0xFFFFFFFFu;
|
||||
coopNetCachedButtons = 0;
|
||||
coopNetLastHashSentTick = 0xFFFFFFFFu;
|
||||
ctx.coopNetStalled = false;
|
||||
}
|
||||
|
||||
// Define canonical key mappings for left and right players
|
||||
const SDL_Scancode leftLeftKey = SDL_SCANCODE_A;
|
||||
const SDL_Scancode leftRightKey = SDL_SCANCODE_D;
|
||||
@ -1327,7 +1355,194 @@ void TetrisApp::Impl::runLoop()
|
||||
const SDL_Scancode rightRightKey = SDL_SCANCODE_RIGHT;
|
||||
const SDL_Scancode rightDownKey = SDL_SCANCODE_DOWN;
|
||||
|
||||
if (!coopVsAI) {
|
||||
if (coopNetActive) {
|
||||
// Network co-op: fixed tick lockstep.
|
||||
// Use a fixed dt so both peers simulate identically.
|
||||
static constexpr double FIXED_DT_MS = 1000.0 / 60.0;
|
||||
static constexpr uint32_t HASH_INTERVAL_TICKS = 60; // ~1s
|
||||
|
||||
ctx.coopNetSession->poll(0);
|
||||
|
||||
// If the connection drops during gameplay, abort back to menu.
|
||||
if (ctx.coopNetSession->state() == NetSession::ConnState::Disconnected ||
|
||||
ctx.coopNetSession->state() == NetSession::ConnState::Error) {
|
||||
const std::string reason = (ctx.coopNetSession->state() == NetSession::ConnState::Error && !ctx.coopNetSession->lastError().empty())
|
||||
? (std::string("NET ERROR: ") + ctx.coopNetSession->lastError())
|
||||
: std::string("NET DISCONNECTED");
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] %s", reason.c_str());
|
||||
|
||||
ctx.coopNetUiStatusText = reason;
|
||||
ctx.coopNetUiStatusRemainingMs = 6000.0;
|
||||
ctx.coopNetEnabled = false;
|
||||
ctx.coopNetStalled = false;
|
||||
ctx.coopNetDesyncDetected = false;
|
||||
ctx.coopNetTick = 0;
|
||||
ctx.coopNetPendingButtons = 0;
|
||||
if (ctx.coopNetSession) {
|
||||
ctx.coopNetSession->shutdown();
|
||||
ctx.coopNetSession.reset();
|
||||
}
|
||||
|
||||
// Ensure we don't remain paused due to a previous net stall/desync.
|
||||
if (game) {
|
||||
game->setPaused(false);
|
||||
}
|
||||
state = AppState::Menu;
|
||||
stateMgr->setState(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
coopNetAccMs = std::min(coopNetAccMs + frameMs, FIXED_DT_MS * 8.0);
|
||||
|
||||
auto buildLocalButtons = [&]() -> uint8_t {
|
||||
uint8_t b = 0;
|
||||
if (ctx.coopNetLocalIsLeft) {
|
||||
if (ks[leftLeftKey]) b |= coopnet::MoveLeft;
|
||||
if (ks[leftRightKey]) b |= coopnet::MoveRight;
|
||||
if (ks[leftDownKey]) b |= coopnet::SoftDrop;
|
||||
} else {
|
||||
if (ks[rightLeftKey]) b |= coopnet::MoveLeft;
|
||||
if (ks[rightRightKey]) b |= coopnet::MoveRight;
|
||||
if (ks[rightDownKey]) b |= coopnet::SoftDrop;
|
||||
}
|
||||
b |= ctx.coopNetPendingButtons;
|
||||
ctx.coopNetPendingButtons = 0;
|
||||
return b;
|
||||
};
|
||||
|
||||
auto applyButtonsForSide = [&](CoopGame::PlayerSide side,
|
||||
uint8_t buttons,
|
||||
bool& leftHeldPrev,
|
||||
bool& rightHeldPrev,
|
||||
double& timer) {
|
||||
const bool leftHeldNow = coopnet::has(buttons, coopnet::MoveLeft);
|
||||
const bool rightHeldNow = coopnet::has(buttons, coopnet::MoveRight);
|
||||
const bool downHeldNow = coopnet::has(buttons, coopnet::SoftDrop);
|
||||
|
||||
coopGame->setSoftDropping(side, downHeldNow);
|
||||
|
||||
int moveDir = 0;
|
||||
if (leftHeldNow && !rightHeldNow) moveDir = -1;
|
||||
else if (rightHeldNow && !leftHeldNow) moveDir = +1;
|
||||
|
||||
if (moveDir != 0) {
|
||||
if ((moveDir == -1 && !leftHeldPrev) || (moveDir == +1 && !rightHeldPrev)) {
|
||||
coopGame->move(side, moveDir);
|
||||
timer = DAS;
|
||||
} else {
|
||||
timer -= FIXED_DT_MS;
|
||||
if (timer <= 0.0) {
|
||||
coopGame->move(side, moveDir);
|
||||
timer += ARR;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timer = 0.0;
|
||||
}
|
||||
|
||||
if (coopnet::has(buttons, coopnet::RotCW)) {
|
||||
coopGame->rotate(side, +1);
|
||||
}
|
||||
if (coopnet::has(buttons, coopnet::RotCCW)) {
|
||||
coopGame->rotate(side, -1);
|
||||
}
|
||||
if (coopnet::has(buttons, coopnet::HardDrop)) {
|
||||
SoundEffectManager::instance().playSound("hard_drop", 0.7f);
|
||||
coopGame->hardDrop(side);
|
||||
}
|
||||
if (coopnet::has(buttons, coopnet::Hold)) {
|
||||
coopGame->holdCurrent(side);
|
||||
}
|
||||
|
||||
leftHeldPrev = leftHeldNow;
|
||||
rightHeldPrev = rightHeldNow;
|
||||
};
|
||||
|
||||
const char* roleStr = ctx.coopNetIsHost ? "HOST" : "CLIENT";
|
||||
|
||||
int safetySteps = 0;
|
||||
bool advancedTick = false;
|
||||
ctx.coopNetStalled = false;
|
||||
while (coopNetAccMs >= FIXED_DT_MS && safetySteps++ < 8) {
|
||||
const uint32_t tick = ctx.coopNetTick;
|
||||
|
||||
if (coopNetCachedTick != tick) {
|
||||
coopNetCachedTick = tick;
|
||||
coopNetCachedButtons = buildLocalButtons();
|
||||
if (!ctx.coopNetSession->sendLocalInput(tick, coopNetCachedButtons)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP][%s] sendLocalInput failed (tick=%u)",
|
||||
roleStr,
|
||||
tick);
|
||||
}
|
||||
}
|
||||
|
||||
auto remoteButtonsOpt = ctx.coopNetSession->getRemoteButtons(tick);
|
||||
if (!remoteButtonsOpt.has_value()) {
|
||||
if (!ctx.coopNetStalled) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP][%s] STALL begin waitingForTick=%u",
|
||||
roleStr,
|
||||
tick);
|
||||
}
|
||||
ctx.coopNetStalled = true;
|
||||
break; // lockstep stall
|
||||
}
|
||||
|
||||
const uint8_t remoteButtons = remoteButtonsOpt.value();
|
||||
const bool localIsLeft = ctx.coopNetLocalIsLeft;
|
||||
|
||||
if (localIsLeft) {
|
||||
applyButtonsForSide(CoopGame::PlayerSide::Left, coopNetCachedButtons, p1LeftHeld, p1RightHeld, p1MoveTimerMs);
|
||||
applyButtonsForSide(CoopGame::PlayerSide::Right, remoteButtons, p2LeftHeld, p2RightHeld, p2MoveTimerMs);
|
||||
} else {
|
||||
applyButtonsForSide(CoopGame::PlayerSide::Right, coopNetCachedButtons, p2LeftHeld, p2RightHeld, p2MoveTimerMs);
|
||||
applyButtonsForSide(CoopGame::PlayerSide::Left, remoteButtons, p1LeftHeld, p1RightHeld, p1MoveTimerMs);
|
||||
}
|
||||
|
||||
coopGame->tickGravity(FIXED_DT_MS);
|
||||
coopGame->updateVisualEffects(FIXED_DT_MS);
|
||||
|
||||
if ((tick % HASH_INTERVAL_TICKS) == 0 && coopNetLastHashSentTick != tick) {
|
||||
coopNetLastHashSentTick = tick;
|
||||
const uint64_t hash = coopGame->computeStateHash();
|
||||
if (!ctx.coopNetSession->sendStateHash(tick, hash)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP][%s] sendStateHash failed (tick=%u hash=0x%016llX)",
|
||||
roleStr,
|
||||
tick,
|
||||
(unsigned long long)hash);
|
||||
}
|
||||
auto rh = ctx.coopNetSession->takeRemoteHash(tick);
|
||||
if (rh.has_value() && rh.value() != hash) {
|
||||
ctx.coopNetDesyncDetected = true;
|
||||
ctx.coopNetUiStatusText = "NET DESYNC";
|
||||
ctx.coopNetUiStatusRemainingMs = 8000.0;
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP][%s] DESYNC detected at tick=%u local=0x%016llX remote=0x%016llX",
|
||||
roleStr,
|
||||
tick,
|
||||
(unsigned long long)hash,
|
||||
(unsigned long long)rh.value());
|
||||
game->setPaused(true);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.coopNetTick++;
|
||||
advancedTick = true;
|
||||
coopNetAccMs -= FIXED_DT_MS;
|
||||
}
|
||||
|
||||
if (advancedTick) {
|
||||
if (ctx.coopNetStalled) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP][%s] STALL end atTick=%u",
|
||||
roleStr,
|
||||
ctx.coopNetTick);
|
||||
}
|
||||
ctx.coopNetStalled = false;
|
||||
}
|
||||
} else if (!coopVsAI) {
|
||||
// Standard two-player: left uses WASD, right uses arrow keys
|
||||
handleSide(CoopGame::PlayerSide::Left, p1LeftHeld, p1RightHeld, p1MoveTimerMs, leftLeftKey, leftRightKey, leftDownKey);
|
||||
handleSide(CoopGame::PlayerSide::Right, p2LeftHeld, p2RightHeld, p2MoveTimerMs, rightLeftKey, rightRightKey, rightDownKey);
|
||||
@ -1357,8 +1572,10 @@ void TetrisApp::Impl::runLoop()
|
||||
p2RightHeld = ks[rightRightKey];
|
||||
}
|
||||
|
||||
coopGame->tickGravity(frameMs);
|
||||
coopGame->updateVisualEffects(frameMs);
|
||||
if (!coopNetActive) {
|
||||
coopGame->tickGravity(frameMs);
|
||||
coopGame->updateVisualEffects(frameMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (coopGame->isGameOver()) {
|
||||
@ -1387,6 +1604,12 @@ void TetrisApp::Impl::runLoop()
|
||||
}
|
||||
state = AppState::GameOver;
|
||||
stateMgr->setState(state);
|
||||
|
||||
if (ctx.coopNetSession) {
|
||||
ctx.coopNetSession->shutdown();
|
||||
ctx.coopNetSession.reset();
|
||||
}
|
||||
ctx.coopNetEnabled = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user