added basic network play
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
#include "MenuState.h"
|
||||
#include "persistence/Scores.h"
|
||||
#include "../network/supabase_client.h"
|
||||
#include "../network/NetSession.h"
|
||||
#include "graphics/Font.h"
|
||||
#include "../graphics/ui/HelpOverlay.h"
|
||||
#include "../core/GlobalState.h"
|
||||
@ -16,6 +17,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
|
||||
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
||||
// This allows the UI to adapt when the window is resized or goes fullscreen
|
||||
@ -141,6 +143,17 @@ void MenuState::showCoopSetupPanel(bool show, bool resumeMusic) {
|
||||
coopSetupAnimating = true;
|
||||
coopSetupDirection = 1;
|
||||
coopSetupSelected = (ctx.coopVsAI && *ctx.coopVsAI) ? 1 : 0;
|
||||
coopSetupStep = CoopSetupStep::ChoosePartner;
|
||||
coopNetworkRoleSelected = 0;
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StopTextInput(focusWin);
|
||||
}
|
||||
if (coopNetworkSession) {
|
||||
coopNetworkSession->shutdown();
|
||||
coopNetworkSession.reset();
|
||||
}
|
||||
coopSetupRectsValid = false;
|
||||
selectedButton = static_cast<int>(ui::BottomMenuItem::Cooperate);
|
||||
// Ensure the transition value is non-zero so render code can show
|
||||
@ -152,6 +165,19 @@ void MenuState::showCoopSetupPanel(bool show, bool resumeMusic) {
|
||||
coopSetupAnimating = true;
|
||||
coopSetupDirection = -1;
|
||||
coopSetupRectsValid = false;
|
||||
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StopTextInput(focusWin);
|
||||
}
|
||||
|
||||
// Cancel any pending network session if the coop setup is being closed.
|
||||
if (coopNetworkSession) {
|
||||
coopNetworkSession->shutdown();
|
||||
coopNetworkSession.reset();
|
||||
}
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
coopSetupStep = CoopSetupStep::ChoosePartner;
|
||||
// Resume menu music only when requested (ESC should pass resumeMusic=false)
|
||||
if (resumeMusic && ctx.musicEnabled && *ctx.musicEnabled) {
|
||||
Audio::instance().playMenuMusic();
|
||||
@ -280,58 +306,196 @@ void MenuState::onExit() {
|
||||
}
|
||||
|
||||
void MenuState::handleEvent(const SDL_Event& e) {
|
||||
// Text input for network IP entry (only while coop setup panel is active).
|
||||
if ((coopSetupVisible || coopSetupAnimating) && coopSetupTransition > 0.0 && e.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
std::string& target = (coopNetworkRoleSelected == 0) ? coopNetworkBindAddress : coopNetworkJoinAddress;
|
||||
if (target.size() < 64) {
|
||||
target += e.text.text;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Coop setup panel navigation (modal within the menu)
|
||||
// Handle this FIRST and consume key events so the main menu navigation doesn't interfere.
|
||||
// Note: Do not require !repeat here; some keyboards/OS configs may emit Enter with repeat.
|
||||
if ((coopSetupVisible || coopSetupAnimating) && coopSetupTransition > 0.0 && e.type == SDL_EVENT_KEY_DOWN) {
|
||||
// Coop setup panel navigation (modal within the menu)
|
||||
switch (e.key.scancode) {
|
||||
case SDL_SCANCODE_UP:
|
||||
case SDL_SCANCODE_DOWN:
|
||||
// Do NOT allow up/down to change anything while this panel is active
|
||||
return;
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
// When in a nested network step, go back one step; otherwise close the panel.
|
||||
if (coopSetupStep == CoopSetupStep::NetworkChooseRole) {
|
||||
coopSetupStep = CoopSetupStep::ChoosePartner;
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StopTextInput(focusWin);
|
||||
}
|
||||
coopSetupStep = CoopSetupStep::NetworkChooseRole;
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkWaiting) {
|
||||
if (coopNetworkSession) {
|
||||
coopNetworkSession->shutdown();
|
||||
coopNetworkSession.reset();
|
||||
}
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StopTextInput(focusWin);
|
||||
}
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
coopSetupStep = CoopSetupStep::NetworkChooseRole;
|
||||
return;
|
||||
}
|
||||
showCoopSetupPanel(false, false);
|
||||
return;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
case SDL_SCANCODE_A:
|
||||
coopSetupSelected = 0;
|
||||
buttonFlash = 1.0;
|
||||
if (coopSetupStep == CoopSetupStep::ChoosePartner) {
|
||||
// 3-way selection: LOCAL / AI / NETWORK
|
||||
coopSetupSelected = (coopSetupSelected + 3 - 1) % 3;
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkChooseRole) {
|
||||
coopNetworkRoleSelected = (coopNetworkRoleSelected + 2 - 1) % 2;
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
case SDL_SCANCODE_D:
|
||||
coopSetupSelected = 1;
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
// Do NOT allow up/down to change anything
|
||||
case SDL_SCANCODE_UP:
|
||||
case SDL_SCANCODE_DOWN:
|
||||
return;
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
showCoopSetupPanel(false, false);
|
||||
if (coopSetupStep == CoopSetupStep::ChoosePartner) {
|
||||
coopSetupSelected = (coopSetupSelected + 1) % 3;
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkChooseRole) {
|
||||
coopNetworkRoleSelected = (coopNetworkRoleSelected + 1) % 2;
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
}
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
std::string& target = (coopNetworkRoleSelected == 0) ? coopNetworkBindAddress : coopNetworkJoinAddress;
|
||||
if (!target.empty()) target.pop_back();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
case SDL_SCANCODE_SPACE:
|
||||
{
|
||||
const bool useAI = (coopSetupSelected == 1);
|
||||
if (ctx.coopVsAI) {
|
||||
*ctx.coopVsAI = useAI;
|
||||
}
|
||||
if (ctx.game) {
|
||||
ctx.game->setMode(GameMode::Cooperate);
|
||||
ctx.game->reset(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
}
|
||||
if (ctx.coopGame) {
|
||||
ctx.coopGame->reset(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
// Existing flows (Local 2P / AI) are preserved exactly.
|
||||
if (coopSetupStep == CoopSetupStep::ChoosePartner && (coopSetupSelected == 0 || coopSetupSelected == 1)) {
|
||||
const bool useAI = (coopSetupSelected == 1);
|
||||
if (ctx.coopVsAI) {
|
||||
*ctx.coopVsAI = useAI;
|
||||
}
|
||||
if (ctx.game) {
|
||||
ctx.game->setMode(GameMode::Cooperate);
|
||||
ctx.game->reset(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
}
|
||||
if (ctx.coopGame) {
|
||||
ctx.coopGame->reset(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
}
|
||||
|
||||
// Close the panel without restarting menu music; gameplay will take over.
|
||||
showCoopSetupPanel(false, false);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"MenuState: coop start via key, selected=%d, startPlayTransition_present=%d, stateManager=%p",
|
||||
coopSetupSelected,
|
||||
ctx.startPlayTransition ? 1 : 0,
|
||||
(void*)ctx.stateManager);
|
||||
|
||||
if (ctx.startPlayTransition) {
|
||||
ctx.startPlayTransition();
|
||||
} else if (ctx.stateManager) {
|
||||
ctx.stateManager->setState(AppState::Playing);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Close the panel without restarting menu music; gameplay will take over.
|
||||
showCoopSetupPanel(false, false);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState: coop start via key, selected=%d, startPlayTransition_present=%d, stateManager=%p", coopSetupSelected, ctx.startPlayTransition ? 1 : 0, (void*)ctx.stateManager);
|
||||
|
||||
if (ctx.startPlayTransition) {
|
||||
ctx.startPlayTransition();
|
||||
} else if (ctx.stateManager) {
|
||||
ctx.stateManager->setState(AppState::Playing);
|
||||
// Network flow (new): choose host/join, confirm connection before starting.
|
||||
if (coopSetupStep == CoopSetupStep::ChoosePartner && coopSetupSelected == 2) {
|
||||
coopSetupStep = CoopSetupStep::NetworkChooseRole;
|
||||
coopNetworkRoleSelected = 0;
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
if (coopNetworkSession) {
|
||||
coopNetworkSession->shutdown();
|
||||
coopNetworkSession.reset();
|
||||
}
|
||||
buttonFlash = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (coopSetupStep == CoopSetupStep::NetworkChooseRole) {
|
||||
// First, let the user enter the address (bind for host, remote for join).
|
||||
coopSetupStep = CoopSetupStep::NetworkEnterAddress;
|
||||
coopNetworkStatusText.clear();
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StartTextInput(focusWin);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
coopNetworkHandshakeSent = false;
|
||||
coopNetworkStatusText.clear();
|
||||
coopNetworkSession = std::make_unique<NetSession>();
|
||||
|
||||
const uint16_t port = coopNetworkPort;
|
||||
bool ok = false;
|
||||
if (coopNetworkRoleSelected == 0) {
|
||||
const std::string bindIp = coopNetworkBindAddress;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] HOST start bind=%s port=%u", bindIp.c_str(), (unsigned)port);
|
||||
ok = coopNetworkSession->host(bindIp, port);
|
||||
coopNetworkStatusText = ok ? "WAITING FOR PLAYER..." : ("HOST FAILED: " + coopNetworkSession->lastError());
|
||||
} else {
|
||||
const std::string joinIp = coopNetworkJoinAddress.empty() ? std::string("127.0.0.1") : coopNetworkJoinAddress;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] JOIN start remote=%s port=%u", joinIp.c_str(), (unsigned)port);
|
||||
ok = coopNetworkSession->join(joinIp, port);
|
||||
coopNetworkStatusText = ok ? "CONNECTING..." : ("JOIN FAILED: " + coopNetworkSession->lastError());
|
||||
}
|
||||
|
||||
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
|
||||
SDL_StopTextInput(focusWin);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
coopSetupStep = CoopSetupStep::NetworkWaiting;
|
||||
} else {
|
||||
// Stay on role choice screen so user can back out.
|
||||
coopNetworkSession.reset();
|
||||
coopSetupStep = CoopSetupStep::NetworkChooseRole;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// While waiting for connection, Enter does nothing.
|
||||
return;
|
||||
}
|
||||
default:
|
||||
// Allow all other keys to be pressed, but don't let them affect the main menu while coop is open.
|
||||
// Allow other keys, but don't let them affect the main menu while coop is open.
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -796,6 +960,15 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
|
||||
void MenuState::update(double frameMs) {
|
||||
// Transient network status message (e.g., disconnect) shown on return to menu.
|
||||
if (ctx.coopNetUiStatusRemainingMs > 0.0) {
|
||||
ctx.coopNetUiStatusRemainingMs -= frameMs;
|
||||
if (ctx.coopNetUiStatusRemainingMs <= 0.0) {
|
||||
ctx.coopNetUiStatusRemainingMs = 0.0;
|
||||
ctx.coopNetUiStatusText.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Update logo animation counter
|
||||
GlobalState::instance().logoAnimCounter += frameMs;
|
||||
// Advance options panel animation if active
|
||||
@ -1056,6 +1229,15 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
float titleX = (LOGICAL_W - (float)tW) * 0.5f + contentOffsetX;
|
||||
useFont->draw(renderer, titleX, scoresStartY, smallTitle, titleScale, SDL_Color{200,220,230,255});
|
||||
scoresStartY += (float)tH + 12.0f;
|
||||
|
||||
if (!ctx.coopNetUiStatusText.empty() && ctx.coopNetUiStatusRemainingMs > 0.0) {
|
||||
float msgScale = 0.75f;
|
||||
int mW = 0, mH = 0;
|
||||
useFont->measure(ctx.coopNetUiStatusText, msgScale, mW, mH);
|
||||
float msgX = (LOGICAL_W - (float)mW) * 0.5f + contentOffsetX;
|
||||
useFont->draw(renderer, msgX, scoresStartY, ctx.coopNetUiStatusText, msgScale, SDL_Color{255, 224, 130, 255});
|
||||
scoresStartY += (float)mH + 10.0f;
|
||||
}
|
||||
}
|
||||
static const std::vector<ScoreEntry> EMPTY_SCORES;
|
||||
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
|
||||
@ -1358,18 +1540,20 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
// highscores area (not sliding offscreen with the scores).
|
||||
const float panelBaseY = scoresStartY - 20.0f;
|
||||
|
||||
// Make the choice buttons smaller, add more spacing, and raise them higher
|
||||
const float btnW2 = std::min(300.0f, panelW * 0.30f);
|
||||
// Choice buttons (partner selection) and nested network host/join UI
|
||||
const float btnH2 = 60.0f;
|
||||
const float gap = 96.0f;
|
||||
// Shift the image and buttons to the right for layout balance (reduced)
|
||||
const float shiftX = 20.0f; // move right by 30px (moved 20px left from previous)
|
||||
const float bx = panelBaseX + (panelW - (btnW2 * 2.0f + gap)) * 0.5f + shiftX;
|
||||
const float gap = 34.0f;
|
||||
const float btnW2 = std::min(280.0f, (panelW - gap * 2.0f) / 3.0f);
|
||||
const float totalChoiceW = btnW2 * 3.0f + gap * 2.0f;
|
||||
// Shift the image and buttons slightly for layout balance
|
||||
const float shiftX = 20.0f;
|
||||
const float bx = panelBaseX + (panelW - totalChoiceW) * 0.5f + shiftX;
|
||||
// Move the buttons up by ~80px to sit closer under the logo
|
||||
const float by = panelBaseY + (panelH - btnH2) * 0.5f - 80.0f;
|
||||
|
||||
coopSetupBtnRects[0] = SDL_FRect{ bx, by, btnW2, btnH2 };
|
||||
coopSetupBtnRects[1] = SDL_FRect{ bx + btnW2 + gap, by, btnW2, btnH2 };
|
||||
coopSetupBtnRects[1] = SDL_FRect{ bx + (btnW2 + gap), by, btnW2, btnH2 };
|
||||
coopSetupBtnRects[2] = SDL_FRect{ bx + (btnW2 + gap) * 2.0f, by, btnW2, btnH2 };
|
||||
coopSetupRectsValid = true;
|
||||
|
||||
SDL_Color bg{ 24, 36, 52, 220 };
|
||||
@ -1392,13 +1576,13 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
}
|
||||
}
|
||||
|
||||
// If the image loaded, render it centered above the two choice buttons
|
||||
// If the image loaded, render it centered above the choice buttons
|
||||
// Compute fade alpha from the coop transition so it can be used for image, text and buttons
|
||||
float alphaFactor = static_cast<float>(coopSetupTransition);
|
||||
if (alphaFactor < 0.0f) alphaFactor = 0.0f;
|
||||
if (alphaFactor > 1.0f) alphaFactor = 1.0f;
|
||||
if (coopInfoTexture && coopInfoTexW > 0 && coopInfoTexH > 0) {
|
||||
float totalW = btnW2 * 2.0f + gap;
|
||||
float totalW = totalChoiceW;
|
||||
// Increase allowed image width by ~15% (was 0.75 of totalW)
|
||||
const float scaleFactor = 0.75f * 1.25f; // ~0.8625
|
||||
float maxImgW = totalW * scaleFactor;
|
||||
@ -1479,10 +1663,107 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
float buttonFade = rawBtn * rawBtn;
|
||||
SDL_Color bgA = bg; bgA.a = static_cast<Uint8>(std::round(bgA.a * buttonFade));
|
||||
SDL_Color borderA = border; borderA.a = static_cast<Uint8>(std::round(borderA.a * buttonFade));
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont, coopSetupBtnRects[0].x + btnW2 * 0.5f, coopSetupBtnRects[0].y + btnH2 * 0.5f,
|
||||
btnW2, btnH2, "2 PLAYERS", false, coopSetupSelected == 0, bgA, borderA, false, nullptr);
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont, coopSetupBtnRects[1].x + btnW2 * 0.5f, coopSetupBtnRects[1].y + btnH2 * 0.5f,
|
||||
btnW2, btnH2, "COMPUTER (AI)", false, coopSetupSelected == 1, bgA, borderA, false, nullptr);
|
||||
|
||||
// Step 1: choose partner mode
|
||||
if (coopSetupStep == CoopSetupStep::ChoosePartner) {
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont,
|
||||
coopSetupBtnRects[0].x + btnW2 * 0.5f,
|
||||
coopSetupBtnRects[0].y + btnH2 * 0.5f,
|
||||
btnW2, btnH2,
|
||||
"LOCAL CO-OP",
|
||||
false,
|
||||
coopSetupSelected == 0,
|
||||
bgA,
|
||||
borderA,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont,
|
||||
coopSetupBtnRects[1].x + btnW2 * 0.5f,
|
||||
coopSetupBtnRects[1].y + btnH2 * 0.5f,
|
||||
btnW2, btnH2,
|
||||
"AI PARTNER",
|
||||
false,
|
||||
coopSetupSelected == 1,
|
||||
bgA,
|
||||
borderA,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont,
|
||||
coopSetupBtnRects[2].x + btnW2 * 0.5f,
|
||||
coopSetupBtnRects[2].y + btnH2 * 0.5f,
|
||||
btnW2, btnH2,
|
||||
"2 PLAYER (NETWORK)",
|
||||
false,
|
||||
coopSetupSelected == 2,
|
||||
bgA,
|
||||
borderA,
|
||||
false,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
// Step 2: network host/join selection and address entry
|
||||
if (coopSetupStep == CoopSetupStep::NetworkChooseRole || coopSetupStep == CoopSetupStep::NetworkEnterAddress || coopSetupStep == CoopSetupStep::NetworkWaiting) {
|
||||
// Draw two buttons centered under the main row.
|
||||
const float roleBtnW = std::min(280.0f, panelW * 0.30f);
|
||||
const float roleGap = 48.0f;
|
||||
const float roleTotalW = roleBtnW * 2.0f + roleGap;
|
||||
const float roleX = panelBaseX + (panelW - roleTotalW) * 0.5f + shiftX;
|
||||
const float roleY = by + btnH2 + 18.0f;
|
||||
|
||||
SDL_FRect hostRect{ roleX, roleY, roleBtnW, btnH2 };
|
||||
SDL_FRect joinRect{ roleX + roleBtnW + roleGap, roleY, roleBtnW, btnH2 };
|
||||
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont,
|
||||
hostRect.x + roleBtnW * 0.5f,
|
||||
hostRect.y + btnH2 * 0.5f,
|
||||
roleBtnW,
|
||||
btnH2,
|
||||
"HOST GAME",
|
||||
false,
|
||||
coopNetworkRoleSelected == 0,
|
||||
bgA,
|
||||
borderA,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont,
|
||||
joinRect.x + roleBtnW * 0.5f,
|
||||
joinRect.y + btnH2 * 0.5f,
|
||||
roleBtnW,
|
||||
btnH2,
|
||||
"JOIN GAME",
|
||||
false,
|
||||
coopNetworkRoleSelected == 1,
|
||||
bgA,
|
||||
borderA,
|
||||
false,
|
||||
nullptr);
|
||||
|
||||
FontAtlas* f = ctx.font ? ctx.font : ctx.pixelFont;
|
||||
if (f) {
|
||||
SDL_Color infoCol{200, 220, 230, static_cast<Uint8>(std::round(220.0f * buttonFade))};
|
||||
char endpoint[256];
|
||||
std::snprintf(endpoint, sizeof(endpoint), "PORT %u HOST IP %s JOIN IP %s",
|
||||
(unsigned)coopNetworkPort,
|
||||
coopNetworkBindAddress.c_str(),
|
||||
coopNetworkJoinAddress.c_str());
|
||||
f->draw(renderer, panelBaseX + 60.0f, roleY + btnH2 + 12.0f, endpoint, 0.90f, infoCol);
|
||||
|
||||
if (coopSetupStep == CoopSetupStep::NetworkWaiting && !coopNetworkStatusText.empty()) {
|
||||
SDL_Color statusCol{255, 215, 80, static_cast<Uint8>(std::round(240.0f * buttonFade))};
|
||||
f->draw(renderer, panelBaseX + 60.0f, roleY + btnH2 + 44.0f, coopNetworkStatusText, 1.00f, statusCol);
|
||||
} else if (coopSetupStep == CoopSetupStep::NetworkEnterAddress) {
|
||||
SDL_Color hintCol{160, 190, 210, static_cast<Uint8>(std::round(200.0f * buttonFade))};
|
||||
const char* label = (coopNetworkRoleSelected == 0) ? "TYPE HOST IP (BIND) THEN ENTER" : "TYPE JOIN IP THEN ENTER";
|
||||
f->draw(renderer, panelBaseX + 60.0f, roleY + btnH2 + 44.0f, label, 0.82f, hintCol);
|
||||
} else {
|
||||
SDL_Color hintCol{160, 190, 210, static_cast<Uint8>(std::round(200.0f * buttonFade))};
|
||||
f->draw(renderer, panelBaseX + 60.0f, roleY + btnH2 + 44.0f, "PRESS ENTER TO EDIT/CONFIRM ESC TO GO BACK", 0.82f, hintCol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: slide-up COOP panel intentionally removed. Only the inline
|
||||
// highscores-area choice buttons are shown when coop setup is active.
|
||||
@ -1840,6 +2121,108 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
{
|
||||
FILE* f = fopen("spacetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render exit\n"); fclose(f); }
|
||||
}
|
||||
|
||||
// Network coop flow polling (non-blocking)
|
||||
if (coopSetupAnimating || coopSetupVisible) {
|
||||
if (coopSetupStep == CoopSetupStep::NetworkWaiting && coopNetworkSession) {
|
||||
coopNetworkSession->poll(0);
|
||||
|
||||
// Update status depending on connection and role.
|
||||
if (!coopNetworkSession->isConnected()) {
|
||||
// Keep existing text (WAITING/CONNECTING) unless an error occurs.
|
||||
} else {
|
||||
// Host sends handshake after peer connects.
|
||||
if (coopNetworkRoleSelected == 0 && !coopNetworkHandshakeSent) {
|
||||
std::random_device rd;
|
||||
uint32_t seed = static_cast<uint32_t>(rd());
|
||||
if (seed == 0u) seed = 1u;
|
||||
const uint8_t startLevel = static_cast<uint8_t>(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
NetSession::Handshake hs{ seed, 0u, startLevel };
|
||||
if (coopNetworkSession->sendHandshake(hs)) {
|
||||
coopNetworkHandshakeSent = true;
|
||||
ctx.coopNetRngSeed = seed;
|
||||
coopNetworkStatusText = "CONNECTED";
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] HOST handshake sent seed=%u level=%u", seed, (unsigned)startLevel);
|
||||
} else {
|
||||
coopNetworkStatusText = "HANDSHAKE FAILED";
|
||||
}
|
||||
}
|
||||
|
||||
// Client waits for handshake.
|
||||
if (coopNetworkRoleSelected == 1) {
|
||||
auto hs = coopNetworkSession->takeReceivedHandshake();
|
||||
if (hs.has_value()) {
|
||||
coopNetworkStatusText = "CONNECTED";
|
||||
coopNetworkHandshakeSent = true;
|
||||
ctx.coopNetRngSeed = hs->rngSeed;
|
||||
if (ctx.startLevelSelection) {
|
||||
*ctx.startLevelSelection = static_cast<int>(hs->startLevel);
|
||||
}
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[NET COOP] CLIENT handshake recv seed=%u level=%u", hs->rngSeed, (unsigned)hs->startLevel);
|
||||
} else {
|
||||
coopNetworkStatusText = "CONNECTED - WAITING FOR HOST...";
|
||||
}
|
||||
}
|
||||
|
||||
// Confirmed connection => start COOPERATE (network) session.
|
||||
// Note: gameplay/network input injection is implemented separately.
|
||||
if (coopNetworkHandshakeSent) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"[NET COOP] START gameplay (role=%s localIsLeft=%d seed=%u level=%u)",
|
||||
(coopNetworkRoleSelected == 0 ? "HOST" : "CLIENT"),
|
||||
(coopNetworkRoleSelected == 0 ? 1 : 0),
|
||||
(unsigned)ctx.coopNetRngSeed,
|
||||
(unsigned)(ctx.startLevelSelection ? *ctx.startLevelSelection : 0));
|
||||
// Hand off the session to gameplay.
|
||||
if (ctx.coopNetSession) {
|
||||
ctx.coopNetSession->shutdown();
|
||||
ctx.coopNetSession.reset();
|
||||
}
|
||||
|
||||
ctx.coopNetEnabled = true;
|
||||
ctx.coopNetIsHost = (coopNetworkRoleSelected == 0);
|
||||
ctx.coopNetLocalIsLeft = (coopNetworkRoleSelected == 0);
|
||||
ctx.coopNetTick = 0;
|
||||
ctx.coopNetPendingButtons = 0;
|
||||
ctx.coopNetDesyncDetected = false;
|
||||
|
||||
const uint32_t seed = (ctx.coopNetRngSeed == 0u) ? 1u : ctx.coopNetRngSeed;
|
||||
const uint8_t startLevel = static_cast<uint8_t>(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
|
||||
if (ctx.coopVsAI) {
|
||||
*ctx.coopVsAI = false;
|
||||
}
|
||||
if (ctx.game) {
|
||||
ctx.game->setMode(GameMode::Cooperate);
|
||||
ctx.game->reset(ctx.startLevelSelection ? *ctx.startLevelSelection : 0);
|
||||
}
|
||||
if (ctx.coopGame) {
|
||||
// Deterministic reset for network coop.
|
||||
ctx.coopGame->resetDeterministic(startLevel, seed);
|
||||
}
|
||||
|
||||
// Transfer ownership of the active session.
|
||||
ctx.coopNetSession = std::move(coopNetworkSession);
|
||||
|
||||
// Close the panel without restarting menu music; gameplay will take over.
|
||||
showCoopSetupPanel(false, false);
|
||||
|
||||
// For network lockstep, do NOT run the menu->play countdown/fade.
|
||||
// Any local countdown introduces drift and stalls.
|
||||
if (ctx.gameplayCountdownActive) *ctx.gameplayCountdownActive = false;
|
||||
if (ctx.menuPlayCountdownArmed) *ctx.menuPlayCountdownArmed = false;
|
||||
if (ctx.game) ctx.game->setPaused(false);
|
||||
|
||||
if (ctx.stateManager) {
|
||||
ctx.stateManager->setState(AppState::Playing);
|
||||
} else if (ctx.startPlayTransition) {
|
||||
// Fallback if state manager is unavailable.
|
||||
ctx.startPlayTransition();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
#pragma once
|
||||
#include "State.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class NetSession;
|
||||
|
||||
class MenuState : public State {
|
||||
public:
|
||||
MenuState(StateContext& ctx);
|
||||
@ -105,8 +111,27 @@ private:
|
||||
double coopSetupTransition = 0.0; // 0..1
|
||||
double coopSetupTransitionDurationMs = 320.0;
|
||||
int coopSetupDirection = 1; // 1 show, -1 hide
|
||||
int coopSetupSelected = 0; // 0 = 2 players, 1 = AI
|
||||
SDL_FRect coopSetupBtnRects[2]{};
|
||||
// 0 = Local co-op (2 players), 1 = AI partner, 2 = 2 player (network)
|
||||
int coopSetupSelected = 0;
|
||||
|
||||
enum class CoopSetupStep {
|
||||
ChoosePartner,
|
||||
NetworkChooseRole,
|
||||
NetworkEnterAddress,
|
||||
NetworkWaiting,
|
||||
};
|
||||
CoopSetupStep coopSetupStep = CoopSetupStep::ChoosePartner;
|
||||
|
||||
// Network sub-flow state (only used when coopSetupSelected == 2)
|
||||
int coopNetworkRoleSelected = 0; // 0 = host, 1 = join
|
||||
std::string coopNetworkBindAddress = "0.0.0.0";
|
||||
std::string coopNetworkJoinAddress = "127.0.0.1";
|
||||
uint16_t coopNetworkPort = 7777;
|
||||
bool coopNetworkHandshakeSent = false;
|
||||
std::string coopNetworkStatusText;
|
||||
std::unique_ptr<NetSession> coopNetworkSession;
|
||||
|
||||
SDL_FRect coopSetupBtnRects[3]{};
|
||||
bool coopSetupRectsValid = false;
|
||||
// Optional cooperative info image shown when coop setup panel is active
|
||||
SDL_Texture* coopInfoTexture = nullptr;
|
||||
|
||||
@ -6,9 +6,11 @@
|
||||
#include "../persistence/Scores.h"
|
||||
#include "../audio/Audio.h"
|
||||
#include "../audio/SoundEffect.h"
|
||||
#include "../graphics/Font.h"
|
||||
#include "../graphics/renderers/GameRenderer.h"
|
||||
#include "../core/Settings.h"
|
||||
#include "../core/Config.h"
|
||||
#include "../network/CoopNetButtons.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// File-scope transport/spawn detection state
|
||||
@ -24,9 +26,17 @@ void PlayingState::onEnter() {
|
||||
if (ctx.game->getMode() == GameMode::Endless || ctx.game->getMode() == GameMode::Cooperate) {
|
||||
if (ctx.startLevelSelection) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
||||
ctx.game->reset(*ctx.startLevelSelection);
|
||||
if (ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame) {
|
||||
ctx.coopGame->reset(*ctx.startLevelSelection);
|
||||
const bool coopNetActive = (ctx.game->getMode() == GameMode::Cooperate) && ctx.coopNetEnabled && ctx.coopNetSession;
|
||||
|
||||
// For network co-op, MenuState already performed a deterministic reset using the negotiated seed.
|
||||
// Re-resetting here would overwrite it (and will desync).
|
||||
if (!coopNetActive) {
|
||||
ctx.game->reset(*ctx.startLevelSelection);
|
||||
if (ctx.game->getMode() == GameMode::Cooperate && ctx.coopGame) {
|
||||
ctx.coopGame->reset(*ctx.startLevelSelection);
|
||||
}
|
||||
} else {
|
||||
ctx.game->setPaused(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -46,6 +56,18 @@ void PlayingState::onExit() {
|
||||
SDL_DestroyTexture(m_renderTarget);
|
||||
m_renderTarget = nullptr;
|
||||
}
|
||||
|
||||
// If we are leaving gameplay during network co-op, tear down the session so
|
||||
// hosting/joining again works without restarting the app.
|
||||
if (ctx.coopNetSession) {
|
||||
ctx.coopNetSession->shutdown();
|
||||
ctx.coopNetSession.reset();
|
||||
}
|
||||
ctx.coopNetEnabled = false;
|
||||
ctx.coopNetStalled = false;
|
||||
ctx.coopNetDesyncDetected = false;
|
||||
ctx.coopNetTick = 0;
|
||||
ctx.coopNetPendingButtons = 0;
|
||||
}
|
||||
|
||||
void PlayingState::handleEvent(const SDL_Event& e) {
|
||||
@ -135,6 +157,10 @@ void PlayingState::handleEvent(const SDL_Event& e) {
|
||||
|
||||
// Pause toggle (P) - matches classic behavior; disabled during countdown
|
||||
if (e.key.scancode == SDL_SCANCODE_P) {
|
||||
// Network co-op uses lockstep; local pause would desync/stall the peer.
|
||||
if (ctx.coopNetEnabled && ctx.coopNetSession) {
|
||||
return;
|
||||
}
|
||||
const bool countdown = (ctx.gameplayCountdownActive && *ctx.gameplayCountdownActive) ||
|
||||
(ctx.menuPlayCountdownArmed && *ctx.menuPlayCountdownArmed);
|
||||
if (!countdown) {
|
||||
@ -149,6 +175,49 @@ void PlayingState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
|
||||
if (coopActive && ctx.coopGame) {
|
||||
// Network co-op: route one-shot actions into a pending bitmask for lockstep.
|
||||
if (ctx.coopNetEnabled && ctx.coopNetSession) {
|
||||
const bool localIsLeft = ctx.coopNetLocalIsLeft;
|
||||
const SDL_Scancode sc = e.key.scancode;
|
||||
if (localIsLeft) {
|
||||
if (sc == SDL_SCANCODE_W) {
|
||||
ctx.coopNetPendingButtons |= coopnet::RotCW;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_Q) {
|
||||
ctx.coopNetPendingButtons |= coopnet::RotCCW;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_LSHIFT || sc == SDL_SCANCODE_E) {
|
||||
ctx.coopNetPendingButtons |= coopnet::HardDrop;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_LCTRL) {
|
||||
ctx.coopNetPendingButtons |= coopnet::Hold;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (sc == SDL_SCANCODE_UP) {
|
||||
const bool upIsCW = Settings::instance().isUpRotateClockwise();
|
||||
ctx.coopNetPendingButtons |= upIsCW ? coopnet::RotCW : coopnet::RotCCW;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_RALT) {
|
||||
ctx.coopNetPendingButtons |= coopnet::RotCCW;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_SPACE || sc == SDL_SCANCODE_RSHIFT) {
|
||||
ctx.coopNetPendingButtons |= coopnet::HardDrop;
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_RCTRL) {
|
||||
ctx.coopNetPendingButtons |= coopnet::Hold;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If coopNet is active, suppress local co-op direct action keys.
|
||||
}
|
||||
|
||||
const bool coopAIEnabled = (ctx.coopVsAI && *ctx.coopVsAI);
|
||||
|
||||
// Player 1 (left): when AI is enabled it controls the left side so
|
||||
@ -313,6 +382,31 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
// But countdown should definitely NOT show the "PAUSED" overlay.
|
||||
bool shouldBlur = paused && !countdown && !challengeClearFx;
|
||||
|
||||
auto renderNetOverlay = [&]() {
|
||||
if (!coopActive || !ctx.coopNetEnabled || !ctx.pixelFont) return;
|
||||
if (!ctx.coopNetDesyncDetected && !ctx.coopNetStalled) return;
|
||||
|
||||
const char* text = ctx.coopNetDesyncDetected ? "NET: DESYNC" : "NET: STALLED";
|
||||
SDL_Color textColor = ctx.coopNetDesyncDetected ? SDL_Color{255, 230, 180, 255} : SDL_Color{255, 224, 130, 255};
|
||||
float scale = 0.75f;
|
||||
int tw = 0, th = 0;
|
||||
ctx.pixelFont->measure(text, scale, tw, th);
|
||||
|
||||
SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE;
|
||||
SDL_GetRenderDrawBlendMode(renderer, &prevBlend);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
|
||||
const float pad = 8.0f;
|
||||
const float x = 18.0f;
|
||||
const float y = 14.0f;
|
||||
SDL_FRect bg{ x - pad, y - pad, (float)tw + pad * 2.0f, (float)th + pad * 2.0f };
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
|
||||
SDL_RenderFillRect(renderer, &bg);
|
||||
ctx.pixelFont->draw(renderer, x, y, text, scale, textColor);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, prevBlend);
|
||||
};
|
||||
|
||||
if (shouldBlur && m_renderTarget) {
|
||||
// Render game to texture
|
||||
SDL_SetRenderTarget(renderer, m_renderTarget);
|
||||
@ -421,6 +515,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
SDL_SetRenderViewport(renderer, &oldVP);
|
||||
SDL_SetRenderScale(renderer, oldSX, oldSY);
|
||||
|
||||
// Net overlay (on top of blurred game, under pause/exit overlays)
|
||||
renderNetOverlay();
|
||||
|
||||
// Draw overlays
|
||||
if (exitPopup) {
|
||||
GameRenderer::renderExitPopup(
|
||||
@ -466,6 +563,9 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
||||
(float)winW,
|
||||
(float)winH
|
||||
);
|
||||
|
||||
// Net overlay (on top of coop HUD)
|
||||
renderNetOverlay();
|
||||
} else {
|
||||
GameRenderer::renderPlayingState(
|
||||
renderer,
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../network/NetSession.h"
|
||||
|
||||
// Forward declarations for frequently used types
|
||||
class Game;
|
||||
@ -81,6 +84,20 @@ struct StateContext {
|
||||
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
|
||||
// Coop setting: when true, COOPERATE runs with a computer-controlled right player.
|
||||
bool* coopVsAI = nullptr;
|
||||
|
||||
// COOPERATE (network) --------------------------------------------------
|
||||
// These fields are only meaningful when `coopNetEnabled` is true.
|
||||
bool coopNetEnabled = false;
|
||||
bool coopNetIsHost = false;
|
||||
bool coopNetLocalIsLeft = true; // host = left (WASD), client = right (arrows)
|
||||
uint32_t coopNetRngSeed = 0;
|
||||
uint32_t coopNetTick = 0;
|
||||
uint8_t coopNetPendingButtons = 0; // one-shot actions captured from keydown (rotate/hold/harddrop)
|
||||
bool coopNetStalled = false; // true when waiting for remote input for current tick
|
||||
bool coopNetDesyncDetected = false;
|
||||
std::string coopNetUiStatusText; // transient status shown in menu after net abort
|
||||
double coopNetUiStatusRemainingMs = 0.0;
|
||||
std::unique_ptr<NetSession> coopNetSession;
|
||||
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
|
||||
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
|
||||
std::function<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable
|
||||
|
||||
Reference in New Issue
Block a user