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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user