MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
tetris.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cmath>
7#include <cstdint>
8#include <cstdlib>
9#include <filesystem>
10#include <format>
11#include <fstream>
12#include <initializer_list>
13#include <iostream>
14#include <memory>
15#include <random>
16#include <sstream>
17#include <string>
18#include <vector>
19
20#include <glm/ext/matrix_clip_space.hpp>
21#include <glm/ext/matrix_transform.hpp>
22#include <glm/glm.hpp>
23
24#include "mxnetwork/socket.hpp"
25#include "mxvk/argz.hpp"
26#include "mxvk/mxvk.hpp"
29#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
30#include "mxvk/mxvk_sound.hpp"
31#endif
32
33#ifndef tetris_ASSET_DIR
34#define tetris_ASSET_DIR "."
35#endif
36
37namespace {
38
39 constexpr int boardWidth = 10;
40 constexpr int boardHeight = 20;
41 constexpr float cubeScale = 0.085f;
42 constexpr float cubeSpacing = cubeScale * 1.0f;
43 constexpr std::size_t nameMaxLength = 10;
44 constexpr char nameCharacters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
45 constexpr std::size_t nameCharacterCount = sizeof(nameCharacters) - 1;
46 constexpr std::string_view multiplayerPort = "37373";
47
48 struct Cell {
49 int color = -1;
50 };
51
52 struct Block {
53 int x = 0;
54 int y = 0;
55 };
56
58 std::array<Block, 4> blocks;
59 int color = 0;
60 };
61
62 struct ActivePiece {
63 std::array<Block, 4> blocks;
64 int x = 0;
65 int y = 0;
66 int color = 0;
67 };
68
70 std::array<Block, 4> blocks{};
71 int color = 0;
72 };
73
74 struct BlockModel {
75 std::unique_ptr<mxvk::VKAbstractModel> model;
76 int color = 0;
77 };
78
79 struct LockedBlock {
81 int x = 0;
82 int y = 0;
83 };
84
86 std::string name;
87 int score = 0;
88 };
89
91 std::array<std::array<int, boardWidth>, boardHeight> cells{};
92 std::array<Block, 4> activeBlocks{};
93 int activeX = 0;
94 int activeY = 0;
95 int activeColor = 7;
96 int score = 0;
97 int lines = 0;
98 int level = 1;
99 bool gameOver = false;
100 bool hasState = false;
101
103 for (auto &row : cells) {
104 row.fill(-1);
105 }
106 }
107 };
108
116
118 public:
119 explicit HighScores(std::filesystem::path filePath) : filePath(std::move(filePath)) { load(); }
120
121 void addScore(std::string name, int score) {
122 normalizeName(name);
123 scoreEntries.push_back({std::move(name), score});
124 sortAndTrim();
125 write();
126 }
127
128 [[nodiscard]] bool qualifies(int score) const {
129 if (scoreEntries.size() < MAX_ENTRIES) {
130 return true;
131 }
132 return score > scoreEntries.back().score;
133 }
134
135 [[nodiscard]] const std::vector<HighScoreEntry> &entries() const { return scoreEntries; }
136
137 [[nodiscard]] int bestScore() const { return scoreEntries.empty() ? 0 : scoreEntries.front().score; }
138
139 void write() const {
140 std::ofstream out(filePath, std::ios::trunc);
141 if (!out.is_open()) {
142 return;
143 }
144
145 for (const HighScoreEntry &entry : scoreEntries) {
146 out << entry.name << ':' << entry.score << '\n';
147 }
148 }
149
150 private:
151 static constexpr std::size_t MAX_ENTRIES = 10;
152 static constexpr std::size_t MAX_NAME_BYTES = 16;
153
154 std::filesystem::path filePath;
155 std::vector<HighScoreEntry> scoreEntries{};
156
157 static void normalizeName(std::string &name) {
158 if (name.empty()) {
159 name = "Anonymous";
160 }
161
162 for (char &ch : name) {
163 if (ch == '\n' || ch == '\r' || ch == ':') {
164 ch = '_';
165 }
166 }
167
168 if (name.size() > MAX_NAME_BYTES) {
169 name.resize(MAX_NAME_BYTES);
170 }
171 }
172
173 void sortAndTrim() {
174 std::sort(scoreEntries.begin(), scoreEntries.end(), [](const HighScoreEntry &a, const HighScoreEntry &b) {
175 if (a.score != b.score) {
176 return a.score > b.score;
177 }
178 return a.name < b.name;
179 });
180
181 if (scoreEntries.size() > MAX_ENTRIES) {
182 scoreEntries.resize(MAX_ENTRIES);
183 }
184 }
185
186 void load() {
187 scoreEntries.clear();
188
189 std::ifstream in(filePath);
190 if (!in.is_open()) {
191 return;
192 }
193
194 std::string line;
195 while (std::getline(in, line)) {
196 const std::size_t separator = line.find(':');
197 if (separator == std::string::npos) {
198 continue;
199 }
200
201 HighScoreEntry entry{};
202 entry.name = line.substr(0, separator);
203 entry.score = static_cast<int>(std::strtol(line.substr(separator + 1).c_str(), nullptr, 10));
204 normalizeName(entry.name);
205 scoreEntries.push_back(std::move(entry));
206 }
207
208 sortAndTrim();
209 }
210 };
211
212 [[nodiscard]] std::filesystem::path resolveScoreFilePath() {
213 if (const char *basePath = SDL_GetBasePath(); basePath != nullptr && basePath[0] != '\0') {
214 return std::filesystem::path(basePath) / "scores.dat";
215 }
216
217 std::error_code ec;
218 const std::filesystem::path cwd = std::filesystem::current_path(ec);
219 if (!ec) {
220 return cwd / "scores.dat";
221 }
222
223 return std::filesystem::path("scores.dat");
224 }
225
226 const std::array<PieceDefinition, 7> pieceDefinitions{{
227 {{{{-1, 0}, {0, 0}, {1, 0}, {2, 0}}}, 0},
228 {{{{-1, 0}, {0, 0}, {1, 0}, {-1, 1}}}, 1},
229 {{{{-1, 0}, {0, 0}, {1, 0}, {1, 1}}}, 2},
230 {{{{0, 0}, {1, 0}, {0, 1}, {1, 1}}}, 3},
231 {{{{-1, 0}, {0, 0}, {0, 1}, {1, 1}}}, 4},
232 {{{{-1, 0}, {0, 0}, {1, 0}, {0, 1}}}, 5},
233 {{{{-1, 1}, {0, 1}, {0, 0}, {1, 0}}}, 6},
234 }};
235
236 const std::array<std::string, 8> textureManifests{{
237 "manifest_cyan.txt",
238 "manifest_blue.txt",
239 "manifest_orange.txt",
240 "manifest_yellow.txt",
241 "manifest_green.txt",
242 "manifest_purple.txt",
243 "manifest_red.txt",
244 "manifest_gray.txt",
245 }};
246
247 const std::array<std::string, 8> blockTextureFiles{{
248 "block_ltblue.png",
249 "block_dblue.png",
250 "block_orange.png",
251 "block_yellow.png",
252 "block_green.png",
253 "block_purple.png",
254 "block_red.png",
255 "block_gray.png",
256 }};
257
258 const std::array<glm::vec3, 8> colorTints{{
259 {0.65f, 0.95f, 1.0f},
260 {0.45f, 0.56f, 1.0f},
261 {1.0f, 0.63f, 0.25f},
262 {1.0f, 0.92f, 0.35f},
263 {0.48f, 1.0f, 0.45f},
264 {0.82f, 0.48f, 1.0f},
265 {1.0f, 0.42f, 0.42f},
266 {0.55f, 0.58f, 0.62f},
267 }};
268
269 [[nodiscard]] std::array<Block, 4> rotatedBlocks(const ActivePiece &piece) {
270 std::array<Block, 4> rotated = piece.blocks;
271 for (Block &block : rotated) {
272 const int oldX = block.x;
273 block.x = -block.y;
274 block.y = oldX;
275 }
276 return rotated;
277 }
278
279 enum class AppScreen {
280 Intro,
281 Menu,
282 Game,
283 GameOver,
286 Credits,
287 };
288
289 class TetrisWindow final : public mxvk::VK_Window {
290 public:
291 TetrisWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync) : mxvk::VK_Window("-[ MXVK 3D Tetris ]-", width, height, fullscreen, MXVK_VALIDATION, enable_vsync), assetRoot((path.empty() || path == ".") ? std::string(tetris_ASSET_DIR) : path), dataRoot(assetRoot + "/data"), shaderRoot(dataRoot), highScores(resolveScoreFilePath()) {
292 std::random_device rd;
293 rng.seed(rd());
294 setFont(dataRoot + "/font.ttf", 22);
295 setClearColor(0.0f, 0.0f, 0.0f, 1.0f);
296 tryOpenFirstGamepad();
297#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
298 music = std::make_unique<mxvk::VK_Mixer>();
299 musicTrack = music->loadMusic(dataRoot + "/music.ogg");
300 ensureMusicPlaying();
301#endif
302 background = createSprite(dataRoot + "/psychedelic_background.png");
303 backgroundTransitionSprite = createSprite(dataRoot + "/psychedelic_background.png", shaderRoot + "/sprite.vert.spv", shaderRoot + "/tetris_background_transition.frag.spv");
304 menuBackgroundSprite = createSprite(dataRoot + "/start_screen.png", shaderRoot + "/sprite.vert.spv", shaderRoot + "/tetris_screen_fade.frag.spv");
305 highScoresBackgroundSprite = createSprite(dataRoot + "/high_scores_screen.png", shaderRoot + "/sprite.vert.spv", shaderRoot + "/tetris_screen_fade.frag.spv");
306 creditsBackgroundSprite = createSprite(dataRoot + "/credits_screen.png", shaderRoot + "/sprite.vert.spv", shaderRoot + "/tetris_screen_fade.frag.spv");
307 multiplayerBackgroundSprite = createSprite(dataRoot + "/multiplayer_screen.png", shaderRoot + "/sprite.vert.spv", shaderRoot + "/tetris_screen_fade.frag.spv");
308 previewBorderSprite = createSprite(1, 1);
309 const uint32_t whitePixel = 0xFFFFFFFFu;
310 previewBorderSprite->updateTexture(&whitePixel, 1, 1);
311 for (std::size_t i = 0; i < blockPreviewSprites.size(); ++i) {
312 blockPreviewSprites[i] = createSprite(dataRoot + "/" + blockTextureFiles[i]);
313 }
314 gameOverSprite = createSprite(dataRoot + "/gameover.png");
315 introSprite = createSprite(dataRoot + "/intro.png", shaderRoot + "/tetris_intro.vert.spv", shaderRoot + "/tetris_intro.frag.spv");
316 introStart = std::chrono::steady_clock::now();
317 initModels();
318 initCreditsModel();
319 resetGame();
320 }
321
322 ~TetrisWindow() override {
323 if (device != VK_NULL_HANDLE) {
324 vkDeviceWaitIdle(device);
325 }
326#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
327 if (music) {
328 music->stopMusic();
329 }
330#endif
331 stopNameEntry();
332 closeMultiplayerSession();
333 closeGamepad();
334 cleanupModels();
335 }
336
337 void onSwapchainRecreated() override {
338 forEachModel([this](mxvk::VKAbstractModel &model) { model.resize(this); });
339 }
340
341 void event(SDL_Event &e) override {
342 if (e.type == SDL_EVENT_QUIT) {
343 exit();
344 return;
345 }
346
347 if (e.type == SDL_EVENT_KEY_DOWN) {
348 if (e.key.repeat) {
349 return;
350 }
351 if (screen == AppScreen::GameOver && enteringName) {
352 handleNameEntryKey(e.key.key);
353 return;
354 }
355 }
356
357 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
358 if (!openGamepad(e.gdevice.which)) {
359 tryOpenFirstGamepad();
360 }
361 return;
362 }
363
364 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
365 if (gamepad != nullptr && e.gdevice.which == gamepadId) {
366 closeGamepad();
367 tryOpenFirstGamepad();
368 }
369 return;
370 }
371
372 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
373 if (screen == AppScreen::GameOver && enteringName) {
374 handleNameEntryButton(e.gbutton.button);
375 return;
376 }
377 handleGamepadButtonDown(e.gbutton.button);
378 }
379 }
380
381 void proc() override {
382 ensureMusicPlaying();
383 tryOpenFirstGamepad();
384 updateIntroState();
385 updateScreenTransition();
386 updateMultiplayerNetwork();
387 updateInput();
388 if (screen == AppScreen::Game) {
389 updateGame();
390 }
391 drawHud();
392 }
393
394 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override {
395 const VkExtent2D extent = getSwapchainExtent();
396 drawScreenBackdrop(cmd, extent);
397
398 if (screen == AppScreen::Game || screen == AppScreen::GameOver) {
399 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
400 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.2f, cameraDistance), glm::vec3(0.0f, 0.1f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
401 view = glm::rotate(view, glm::radians(gridPitch), glm::vec3(1.0f, 0.0f, 0.0f));
402 view = glm::rotate(view, glm::radians(gridYaw), glm::vec3(0.0f, 1.0f, 0.0f));
403 view = glm::rotate(view, glm::radians(gridRoll), glm::vec3(0.0f, 0.0f, 1.0f));
404
405 glm::mat4 proj = glm::perspective(glm::radians(46.0f), aspect, 0.1f, 100.0f);
406 proj[1][1] *= -1.0f;
407 const bool blinkVisible = !lineClearActive || isLineClearVisible();
408
409 for (LockedBlock &locked : lockedBlocks) {
410 if (blinkVisible || !isClearingRow(locked.y)) {
411 drawBlock(cmd, imageIndex, locked.block, locked.x, locked.y, locked.block.color, view, proj);
412 }
413 }
414
415 if (!lineClearActive) {
416 for (size_t i = 0; i < activeModels.size(); ++i) {
417 const int x = active.x + active.blocks[i].x;
418 const int y = active.y + active.blocks[i].y;
419 drawBlock(cmd, imageIndex, activeModels[i], x, y, active.color, view, proj);
420 }
421 }
422
423 drawFrame(cmd, imageIndex, view, proj);
424 drawNextPiecePreview(cmd, imageIndex, extent);
425 drawOpponentGrid(cmd, extent);
426 }
427 if (screen == AppScreen::Credits) {
428 drawCreditsModel(cmd, imageIndex, extent);
429 }
430 drawGameScreenTransitionOverlay(cmd, extent);
431 drawIntroOverlay(cmd, extent);
432 drawGameOverOverlay(cmd, extent);
433 }
434
435 private:
436 std::string assetRoot;
437 std::string dataRoot;
438 std::string shaderRoot;
439 std::array<std::array<Cell, boardWidth>, boardHeight> board{};
440 ActivePiece active{};
441 std::mt19937 rng{};
442 std::vector<LockedBlock> lockedBlocks{};
443 std::array<BlockModel, 4> activeModels{};
444 std::vector<BlockModel> frameModels{};
445 mxvk::VK_Sprite *background = nullptr;
446 mxvk::VK_Sprite *backgroundTransitionSprite = nullptr;
447 mxvk::VK_Sprite *menuBackgroundSprite = nullptr;
448 mxvk::VK_Sprite *highScoresBackgroundSprite = nullptr;
449 mxvk::VK_Sprite *creditsBackgroundSprite = nullptr;
450 mxvk::VK_Sprite *multiplayerBackgroundSprite = nullptr;
451 mxvk::VK_Sprite *previewBorderSprite = nullptr;
452 std::array<mxvk::VK_Sprite *, 8> blockPreviewSprites{};
453 mxvk::VK_Sprite *introSprite = nullptr;
454 mxvk::VK_Sprite *gameOverSprite = nullptr;
455 std::unique_ptr<mxvk::VKAbstractModel> creditsTuxModel{};
456 HighScores highScores;
457#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
458 std::unique_ptr<mxvk::VK_Mixer> music{};
459 int musicTrack = -1;
460#endif
461 SDL_Gamepad *gamepad = nullptr;
462 SDL_JoystickID gamepadId = 0;
463 std::chrono::steady_clock::time_point introStart{std::chrono::steady_clock::now()};
464 std::chrono::steady_clock::time_point lastFall{std::chrono::steady_clock::now()};
465 std::chrono::steady_clock::time_point lastInputUpdate{std::chrono::steady_clock::now()};
466 std::chrono::steady_clock::time_point lineClearStart{std::chrono::steady_clock::now()};
467 std::chrono::steady_clock::time_point backgroundTransitionStart{std::chrono::steady_clock::now()};
468 std::chrono::steady_clock::time_point screenTransitionStart{std::chrono::steady_clock::now()};
469 std::chrono::steady_clock::time_point gameOverTransitionStart{std::chrono::steady_clock::now()};
470 bool introActive = true;
471 float gridYaw = 0.0f;
472 float gridPitch = 0.0f;
473 float gridRoll = 0.0f;
474 float cameraDistance = 2.45f;
475 int score = 0;
476 int linesCleared = 0;
477 int level = 1;
478 int cursorPos = 0;
479 float fallSeconds = 0.65f;
480 float moveRepeatTimer = 0.0f;
481 float softDropRepeatTimer = 0.0f;
482 float gamepadMoveRepeatTimer = 0.0f;
483 float gamepadSoftDropRepeatTimer = 0.0f;
484 bool gameOver = false;
485 bool enteringName = false;
486 bool highScoresAfterSave = false;
487 std::size_t nameCharIndex = 0;
488 std::string playerName{};
489 bool hardDropHeld = false;
490 bool rotateHeld = false;
491 bool resetHeld = false;
492 bool enterHeld = false;
493 bool escapeHeld = false;
494 bool backspaceHeld = false;
495 bool menuUpHeld = false;
496 bool menuDownHeld = false;
497 bool menuEnterHeld = false;
498 bool introSkipHeld = false;
499 bool introFadeStarted = false;
500 bool screenTransitionActive = false;
501 bool gameOverTransitionActive = false;
502 bool lineClearActive = false;
503 bool backgroundTransitionActive = false;
504 std::array<bool, boardHeight> clearingRows{};
505 PieceQueueEntry nextPiece{};
507 AppScreen transitionFromScreen = AppScreen::Intro;
508 mxnetwork::MXNetworkInit networkInit{};
509 mxnetwork::Socket listenSocket{};
510 mxnetwork::Socket peerSocket{};
511 MultiplayerSnapshot opponentSnapshot{};
512 MultiplayerMode multiplayerMode = MultiplayerMode::Idle;
513 std::string multiplayerHost{"127.0.0.1"};
514 std::string multiplayerStatus{"Enter host IP, then press J. Press H to host."};
515 std::string multiplayerResult{};
516 std::string networkReceiveBuffer{};
517 std::chrono::steady_clock::time_point lastNetworkSend{std::chrono::steady_clock::now()};
518 std::uint32_t networkSequence = 0;
519 int multiplayerCursor = 0;
520 bool multiplayerActive = false;
521 bool multiplayerHostSide = false;
522 bool hostHeld = false;
523 bool joinHeld = false;
524 static constexpr float introHoldSeconds = 5.0f;
525 static constexpr float introFadeSeconds = 1.0f;
526 static constexpr float screenTransitionSeconds = 0.36f;
527 static constexpr float gameOverTransitionSeconds = 0.5f;
528 static constexpr float backgroundTransitionSeconds = 1.25f;
529 static constexpr Sint16 gamepadDeadzone = 10000;
530 static constexpr float gamepadMoveInitialDelaySeconds = 0.22f;
531 static constexpr float gamepadMoveRepeatSeconds = 0.12f;
532 static constexpr float gamepadSoftDropInitialDelaySeconds = 0.18f;
533 static constexpr float gamepadSoftDropRepeatSeconds = 0.08f;
534 static constexpr float gamepadStickRotateSpeed = 120.0f;
535 static constexpr float gamepadStickPitchSpeed = 100.0f;
536 static constexpr float gamepadStickScale = 1.0f / 32768.0f;
537 int gamepadMoveDirection = 0;
538 float gamepadMoveHeldSeconds = 0.0f;
539 bool gamepadSoftDropHeld = false;
540
541 void initModels() {
542 frameModels.reserve(boardWidth + (boardHeight * 2) + 2);
543 for (int i = 0; i < boardWidth + (boardHeight * 2) + 2; ++i) {
544 frameModels.push_back(loadBlockModel(7));
545 }
546 }
547
548 [[nodiscard]] BlockModel loadBlockModel(int color) {
549 BlockModel block{};
550 block.color = color;
551 block.model = std::make_unique<mxvk::VKAbstractModel>();
552 block.model->load(this, dataRoot + "/cube.mxmod.z", dataRoot + "/" + textureManifests[color], dataRoot, 1.0f);
553 block.model->setShaders(this, shaderRoot + "/tetris_piece.vert.spv", shaderRoot + "/tetris_piece.frag.spv");
554 return block;
555 }
556
557 void cleanupModels() {
558 forEachModel([this](mxvk::VKAbstractModel &model) { model.cleanup(this); });
559 lockedBlocks.clear();
560 }
561
562 template <typename Fn> void forEachModel(Fn fn) {
563 for (LockedBlock &locked : lockedBlocks) {
564 if (locked.block.model) {
565 fn(*locked.block.model);
566 }
567 }
568 for (BlockModel &block : activeModels) {
569 if (block.model) {
570 fn(*block.model);
571 }
572 }
573 for (BlockModel &block : frameModels) {
574 if (block.model) {
575 fn(*block.model);
576 }
577 }
578 if (creditsTuxModel) {
579 fn(*creditsTuxModel);
580 }
581 }
582
583 void initCreditsModel() {
584 creditsTuxModel = std::make_unique<mxvk::VKAbstractModel>();
585 creditsTuxModel->load(this, dataRoot + "/tux/tux.obj", dataRoot + "/tux/tux.mtl", dataRoot + "/tux", 0.35f);
586 creditsTuxModel->setShaders(this, shaderRoot + "/tetris_model.vert.spv", shaderRoot + "/tetris_model.frag.spv");
587 }
588
589 void resetGame() {
590 stopNameEntry();
591 clearLockedBlocks();
592 for (auto &row : board) {
593 for (Cell &cell : row) {
594 cell.color = -1;
595 }
596 }
597 gameOver = false;
598 gameOverTransitionActive = false;
599 multiplayerResult.clear();
600 lineClearActive = false;
601 backgroundTransitionActive = false;
602 clearingRows.fill(false);
603 score = 0;
604 linesCleared = 0;
605 updateDifficulty();
606 lastFall = std::chrono::steady_clock::now();
607 nextPiece = randomPiece();
608 spawnPiece();
609 }
610
611 void resetOpponentSnapshot() {
612 opponentSnapshot = MultiplayerSnapshot{};
613 networkReceiveBuffer.clear();
614 }
615
616 void closeMultiplayerSession(std::string status = "Enter host IP, then press J. Press H to host.") {
617 if (peerSocket.valid()) {
618 peerSocket.close();
619 }
620 if (listenSocket.valid()) {
621 listenSocket.close();
622 }
623 multiplayerActive = false;
624 multiplayerHostSide = false;
625 multiplayerMode = MultiplayerMode::Idle;
626 multiplayerStatus = std::move(status);
627 resetOpponentSnapshot();
628 }
629
630 [[nodiscard]] std::string multiplayerOutcomeText() const {
631 if (gameOver && !opponentSnapshot.gameOver) {
632 return "You lost. Opponent won.";
633 }
634 if (!gameOver && opponentSnapshot.gameOver) {
635 return "You won. Opponent lost.";
636 }
637 if (score > opponentSnapshot.score) {
638 return "You won by score.";
639 }
640 if (score < opponentSnapshot.score) {
641 return "You lost by score.";
642 }
643 return "Draw.";
644 }
645
646 void enterMultiplayerGameOver(std::string reason) {
647 const std::string outcome = multiplayerOutcomeText();
648 closeMultiplayerSession(std::move(reason));
649 multiplayerResult = outcome;
650 gameOver = true;
651 screen = AppScreen::GameOver;
652 gameOverTransitionActive = true;
653 gameOverTransitionStart = std::chrono::steady_clock::now();
654 enteringName = false;
655 highScoresAfterSave = false;
656 resetMenuLatchState();
657 }
658
659 void finishLocalMultiplayerGameOver() {
660 multiplayerResult = "You lost. Opponent won.";
661 sendMultiplayerSnapshot(true);
662 closeMultiplayerSession("Game over.");
663 enteringName = false;
664 highScoresAfterSave = false;
665 }
666
667 void beginHosting() {
668 closeMultiplayerSession();
669 listenSocket = mxnetwork::Socket(mxnetwork::SocketType::TYPE_INET);
670 if (!listenSocket.listen(multiplayerPort, 1)) {
671 multiplayerMode = MultiplayerMode::Error;
672 multiplayerStatus = std::format("Could not host on port {}", multiplayerPort);
673 return;
674 }
675 listenSocket.setblocking(false);
676 multiplayerMode = MultiplayerMode::Hosting;
677 multiplayerHostSide = true;
678 multiplayerStatus = std::format("Hosting on port {}. Waiting for peer...", multiplayerPort);
679 }
680
681 void joinHost() {
682 closeMultiplayerSession();
683 if (multiplayerHost.empty()) {
684 multiplayerMode = MultiplayerMode::Error;
685 multiplayerStatus = "Enter an IP address before joining.";
686 return;
687 }
688 peerSocket = mxnetwork::Socket(mxnetwork::SocketType::TYPE_INET);
689 multiplayerMode = MultiplayerMode::Joining;
690 multiplayerStatus = std::format("Connecting to {}:{}...", multiplayerHost, multiplayerPort);
691 if (!peerSocket.connect(multiplayerHost, multiplayerPort)) {
692 peerSocket.close();
693 multiplayerMode = MultiplayerMode::Error;
694 multiplayerStatus = std::format("Could not connect to {}:{}", multiplayerHost, multiplayerPort);
695 return;
696 }
697 peerSocket.setblocking(false);
698 multiplayerHostSide = false;
699 finishMultiplayerConnection("Connected as guest.");
700 }
701
702 void finishMultiplayerConnection(const std::string &status) {
703 if (listenSocket.valid()) {
704 listenSocket.close();
705 }
706 multiplayerMode = MultiplayerMode::Connected;
707 multiplayerStatus = status;
708 multiplayerActive = true;
709 resetOpponentSnapshot();
710 resetGame();
711 introActive = false;
712 screen = AppScreen::Game;
713 screenTransitionActive = false;
714 transitionFromScreen = AppScreen::Game;
715 lastInputUpdate = std::chrono::steady_clock::now();
716 lastNetworkSend = std::chrono::steady_clock::now();
717 resetMenuLatchState();
718 sendMultiplayerSnapshot(true);
719 }
720
721 void updateMultiplayerNetwork() {
722 if (multiplayerMode == MultiplayerMode::Hosting && listenSocket.valid()) {
723 try {
724 std::optional<mxnetwork::Socket> accepted = listenSocket.accept();
725 if (accepted) {
726 peerSocket = std::move(*accepted);
727 peerSocket.setblocking(false);
728 finishMultiplayerConnection("Peer connected. Multiplayer started.");
729 }
730 } catch (const mxnetwork::Exception &ex) {
731 multiplayerMode = MultiplayerMode::Error;
732 multiplayerStatus = std::format("Accept failed: {}", ex.text());
733 }
734 }
735
736 if (multiplayerMode != MultiplayerMode::Connected || !peerSocket.valid()) {
737 return;
738 }
739
740 receiveMultiplayerData();
741
742 const auto now = std::chrono::steady_clock::now();
743 const float elapsed = std::chrono::duration<float>(now - lastNetworkSend).count();
744 if (elapsed >= 0.05f) {
745 sendMultiplayerSnapshot(false);
746 lastNetworkSend = now;
747 }
748 }
749
750 std::string makeMultiplayerSnapshotLine() {
751 std::ostringstream out;
752 out << "S " << networkSequence++ << ' ' << score << ' ' << linesCleared << ' ' << level << ' ' << (gameOver ? 1 : 0) << ' ';
753 for (int y = 0; y < boardHeight; ++y) {
754 for (int x = 0; x < boardWidth; ++x) {
755 const int color = board[y][x].color;
756 out << ((color >= 0 && color <= 7) ? static_cast<char>('0' + color) : '.');
757 }
758 }
759 out << ' ' << active.color << ' ' << active.x << ' ' << active.y;
760 for (const Block &block : active.blocks) {
761 out << ' ' << block.x << ' ' << block.y;
762 }
763 out << '\n';
764 return out.str();
765 }
766
767 void sendMultiplayerSnapshot(bool force) {
768 if (!force && (screen != AppScreen::Game && screen != AppScreen::GameOver)) {
769 return;
770 }
771 if (multiplayerMode != MultiplayerMode::Connected || !peerSocket.valid()) {
772 return;
773 }
774 const std::string line = makeMultiplayerSnapshotLine();
775 const ssize_t written = peerSocket.write(line.data(), line.size(), 0);
776 if (written == 0) {
777 enterMultiplayerGameOver("Peer disconnected.");
778 }
779 }
780
781 void receiveMultiplayerData() {
782 std::array<char, 2048> buffer{};
783 while (peerSocket.valid()) {
784 const ssize_t received = peerSocket.read(buffer.data(), buffer.size(), 0);
785 if (received > 0) {
786 networkReceiveBuffer.append(buffer.data(), static_cast<std::size_t>(received));
787 consumeMultiplayerLines();
788 continue;
789 }
790 if (received == 0) {
791 enterMultiplayerGameOver("Peer disconnected.");
792 }
793 break;
794 }
795 }
796
797 void consumeMultiplayerLines() {
798 std::size_t newline = networkReceiveBuffer.find('\n');
799 while (newline != std::string::npos) {
800 const std::string line = networkReceiveBuffer.substr(0, newline);
801 networkReceiveBuffer.erase(0, newline + 1);
802 parseMultiplayerSnapshot(line);
803 newline = networkReceiveBuffer.find('\n');
804 }
805 constexpr std::size_t maxBufferedBytes = 8192;
806 if (networkReceiveBuffer.size() > maxBufferedBytes) {
807 networkReceiveBuffer.clear();
808 }
809 }
810
811 void parseMultiplayerSnapshot(const std::string &line) {
812 std::istringstream in(line);
813 char type = 0;
814 [[maybe_unused]] std::uint32_t sequence = 0;
815 std::string cells;
816 int remoteGameOver = 0;
817 MultiplayerSnapshot snapshot{};
818
819 in >> type >> sequence >> snapshot.score >> snapshot.lines >> snapshot.level >> remoteGameOver >> cells;
820 if (type != 'S' || cells.size() != static_cast<std::size_t>(boardWidth * boardHeight)) {
821 return;
822 }
823
824 snapshot.gameOver = remoteGameOver != 0;
825 for (int y = 0; y < boardHeight; ++y) {
826 for (int x = 0; x < boardWidth; ++x) {
827 const char ch = cells[static_cast<std::size_t>(y * boardWidth + x)];
828 snapshot.cells[y][x] = (ch >= '0' && ch <= '7') ? ch - '0' : -1;
829 }
830 }
831
832 in >> snapshot.activeColor >> snapshot.activeX >> snapshot.activeY;
833 for (Block &block : snapshot.activeBlocks) {
834 in >> block.x >> block.y;
835 }
836 if (!in) {
837 return;
838 }
839 snapshot.activeColor = std::clamp(snapshot.activeColor, 0, 7);
840 snapshot.hasState = true;
841 opponentSnapshot = snapshot;
842 }
843
844 void clearLockedBlocks() {
845 for (LockedBlock &locked : lockedBlocks) {
846 if (locked.block.model) {
847 locked.block.model->cleanup(this);
848 }
849 }
850 lockedBlocks.clear();
851 }
852
853 void spawnPiece() {
854 active.blocks = nextPiece.blocks;
855 active.x = boardWidth / 2;
856 active.y = boardHeight - 2;
857 active.color = nextPiece.color;
858 reloadActiveModels(active.color);
859 gameOver = collides(active.x, active.y, active.blocks);
860 if (gameOver) {
861 enterGameOverState();
862 }
863 nextPiece = randomPiece();
864 }
865
866 void reloadActiveModels(int color) {
867 for (BlockModel &block : activeModels) {
868 if (block.model) {
869 block.model->cleanup(this);
870 }
871 block = loadBlockModel(color);
872 }
873 }
874
875 [[nodiscard]] PieceQueueEntry randomPiece() {
876 std::uniform_int_distribution<int> dist(0, static_cast<int>(pieceDefinitions.size()) - 1);
877 const PieceDefinition &definition = pieceDefinitions[dist(rng)];
878 return PieceQueueEntry{definition.blocks, definition.color};
879 }
880
881 void updateDifficulty() {
882 const int previousLevel = level;
883 level = (linesCleared / 8) + 1;
884 static constexpr std::array<float, 16> arcadeFallSeconds{
885 0.72f,
886 0.66f,
887 0.60f,
888 0.54f,
889 0.48f,
890 0.43f,
891 0.38f,
892 0.34f,
893 0.30f,
894 0.26f,
895 0.23f,
896 0.20f,
897 0.18f,
898 0.16f,
899 0.14f,
900 0.12f,
901 };
902
903 const std::size_t index = std::min<std::size_t>(arcadeFallSeconds.size() - 1, static_cast<std::size_t>(std::max(level - 1, 0)));
904 fallSeconds = arcadeFallSeconds[index];
905 if (level > previousLevel) {
906 triggerBackgroundTransition();
907 }
908 }
909
910#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
911 void ensureMusicPlaying() {
912 if (!music) {
913 return;
914 }
915 if (musicTrack < 0) {
916 return;
917 }
918 if (!music->isMusicPlaying(musicTrack)) {
919 if (music->playMusic(musicTrack, -1) != 0) {
920 throw mxvk::Exception("Could not start Tetris background music");
921 }
922 }
923 }
924#else
925 void ensureMusicPlaying() {}
926#endif
927
928 void updateInput() {
929 const auto now = std::chrono::steady_clock::now();
930 float deltaSeconds = std::chrono::duration<float>(now - lastInputUpdate).count();
931 lastInputUpdate = now;
932 deltaSeconds = std::clamp(deltaSeconds, 0.0f, 0.05f);
933
934 const bool *keys = SDL_GetKeyboardState(nullptr);
935 if (keys == nullptr) {
936 return;
937 }
938
939 if (introActive) {
940 const bool enterDown = keys[SDL_SCANCODE_RETURN];
941 const bool spaceDown = keys[SDL_SCANCODE_SPACE];
942 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
943 const bool skipDown = enterDown || spaceDown;
944 if (skipDown && !introSkipHeld) {
945 const auto now = std::chrono::steady_clock::now();
946 introStart = now - std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float>(introHoldSeconds));
947 introFadeStarted = false;
948 requestScreen(AppScreen::Menu);
949 }
950 if (escapeDown && !escapeHeld) {
951 exit();
952 }
953 introSkipHeld = skipDown;
954 escapeHeld = escapeDown;
955 return;
956 }
957
958 if (screenTransitionActive) {
959 return;
960 }
961
962 if (screen == AppScreen::GameOver) {
963 if (enteringName) {
964 return;
965 }
966 handleGameOverKeys(keys);
967 return;
968 }
969
970 if (screen == AppScreen::NetworkMultiplayer) {
971 handleMultiplayerKeys(keys);
972 return;
973 }
974
975 if (screen != AppScreen::Game) {
976 handleMenuKeys(keys);
977 return;
978 }
979
980 handleHeldViewRotation(keys, deltaSeconds);
981 handleHeldPieceMovement(keys, deltaSeconds);
982 handleOneShotKeys(keys);
983 handleGamepadInput(deltaSeconds);
984 }
985
986 void handleMenuKeys(const bool *keys) {
987 if (screenTransitionActive) {
988 menuUpHeld = keys[SDL_SCANCODE_UP];
989 menuDownHeld = keys[SDL_SCANCODE_DOWN];
990 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
991 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
992 return;
993 }
994
995 const bool upDown = keys[SDL_SCANCODE_UP];
996 const bool downDown = keys[SDL_SCANCODE_DOWN];
997 const bool enterDown = keys[SDL_SCANCODE_RETURN];
998 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
999
1000 if (screen == AppScreen::Menu) {
1001 if (upDown && !menuUpHeld) {
1002 cursorPos = (cursorPos + 3) % 4;
1003 }
1004 if (downDown && !menuDownHeld) {
1005 cursorPos = (cursorPos + 1) % 4;
1006 }
1007 }
1008 if (enterDown && !menuEnterHeld) {
1009 if (screen == AppScreen::Menu) {
1010 if (cursorPos == 0) {
1011 startGame();
1012 } else if (cursorPos == 1) {
1013 goToNetworkMultiplayer();
1014 } else if (cursorPos == 2) {
1015 goToHighScores();
1016 } else if (cursorPos == 3) {
1017 goToCredits();
1018 }
1019 } else if (screen == AppScreen::HighScores && highScoresAfterSave) {
1020 highScoresAfterSave = false;
1021 restartIntroSequence();
1022 } else {
1023 goToMenu();
1024 }
1025 }
1026 if (escapeDown && !escapeHeld) {
1027 if (screen == AppScreen::Menu) {
1028 exit();
1029 } else {
1030 highScoresAfterSave = false;
1031 goToMenu();
1032 }
1033 }
1034
1035 menuUpHeld = upDown;
1036 menuDownHeld = downDown;
1037 menuEnterHeld = enterDown;
1038 escapeHeld = escapeDown;
1039 }
1040
1041 void handleMultiplayerKeys(const bool *keys) {
1042 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1043 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1044 const bool hostDown = keys[SDL_SCANCODE_H];
1045 const bool joinDown = keys[SDL_SCANCODE_J];
1046 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1047
1048 if (escapeDown && !escapeHeld) {
1049 closeMultiplayerSession();
1050 goToMenu();
1051 }
1052 if (hostDown && !hostHeld) {
1053 beginHosting();
1054 }
1055 if (joinDown && !joinHeld) {
1056 joinHost();
1057 }
1058 if (enterDown && !menuEnterHeld) {
1059 joinHost();
1060 }
1061 if (backspaceDown && !backspaceHeld && !multiplayerHost.empty()) {
1062 multiplayerHost.pop_back();
1063 }
1064
1065 appendMultiplayerAddressCharacters(keys);
1066
1067 escapeHeld = escapeDown;
1068 menuEnterHeld = enterDown;
1069 hostHeld = hostDown;
1070 joinHeld = joinDown;
1071 backspaceHeld = backspaceDown;
1072 }
1073
1074 void appendMultiplayerAddressCharacters(const bool *keys) {
1075 static constexpr std::array<SDL_Scancode, 10> digitKeys{{
1076 SDL_SCANCODE_0,
1077 SDL_SCANCODE_1,
1078 SDL_SCANCODE_2,
1079 SDL_SCANCODE_3,
1080 SDL_SCANCODE_4,
1081 SDL_SCANCODE_5,
1082 SDL_SCANCODE_6,
1083 SDL_SCANCODE_7,
1084 SDL_SCANCODE_8,
1085 SDL_SCANCODE_9,
1086 }};
1087 static constexpr std::array<SDL_Scancode, 10> keypadDigitKeys{{
1088 SDL_SCANCODE_KP_0,
1089 SDL_SCANCODE_KP_1,
1090 SDL_SCANCODE_KP_2,
1091 SDL_SCANCODE_KP_3,
1092 SDL_SCANCODE_KP_4,
1093 SDL_SCANCODE_KP_5,
1094 SDL_SCANCODE_KP_6,
1095 SDL_SCANCODE_KP_7,
1096 SDL_SCANCODE_KP_8,
1097 SDL_SCANCODE_KP_9,
1098 }};
1099
1100 auto appendChar = [this](char ch) {
1101 constexpr std::size_t maxAddressLength = 64;
1102 if (multiplayerHost.size() < maxAddressLength) {
1103 multiplayerHost.push_back(ch);
1104 }
1105 };
1106
1107 for (int i = 0; i < 10; ++i) {
1108 if ((keys[digitKeys[static_cast<std::size_t>(i)]] || keys[keypadDigitKeys[static_cast<std::size_t>(i)]]) && multiplayerCursor != (i + 1)) {
1109 appendChar(static_cast<char>('0' + i));
1110 multiplayerCursor = i + 1;
1111 return;
1112 }
1113 }
1114 if ((keys[SDL_SCANCODE_PERIOD] || keys[SDL_SCANCODE_KP_PERIOD]) && multiplayerCursor != 11) {
1115 appendChar('.');
1116 multiplayerCursor = 11;
1117 return;
1118 }
1119 if (keys[SDL_SCANCODE_MINUS] && multiplayerCursor != 12) {
1120 appendChar('-');
1121 multiplayerCursor = 12;
1122 return;
1123 }
1124 if (!keys[SDL_SCANCODE_PERIOD] && !keys[SDL_SCANCODE_KP_PERIOD] && !keys[SDL_SCANCODE_MINUS]) {
1125 bool anyDigitDown = false;
1126 for (int i = 0; i < 10; ++i) {
1127 anyDigitDown = anyDigitDown || keys[digitKeys[static_cast<std::size_t>(i)]] || keys[keypadDigitKeys[static_cast<std::size_t>(i)]];
1128 }
1129 if (!anyDigitDown) {
1130 multiplayerCursor = 0;
1131 }
1132 }
1133 }
1134
1135 void handleGamepadInput(float deltaSeconds) {
1136 if (gamepad == nullptr || introActive || (screen != AppScreen::Game && screen != AppScreen::GameOver) || gameOver || lineClearActive) {
1137 gamepadMoveDirection = 0;
1138 gamepadMoveHeldSeconds = 0.0f;
1139 gamepadSoftDropHeld = false;
1140 gamepadMoveRepeatTimer = 0.0f;
1141 gamepadSoftDropRepeatTimer = 0.0f;
1142 return;
1143 }
1144
1145 const Sint16 leftX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1146 const Sint16 leftY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1147 const Sint16 rightX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
1148 const Sint16 rightY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
1149
1150 const int moveDirection = (leftX < -gamepadDeadzone) ? -1 : (leftX > gamepadDeadzone) ? 1 : 0;
1151 if (moveDirection == 0) {
1152 gamepadMoveDirection = 0;
1153 gamepadMoveHeldSeconds = 0.0f;
1154 gamepadMoveRepeatTimer = 0.0f;
1155 } else {
1156 if (moveDirection != gamepadMoveDirection) {
1157 gamepadMoveDirection = moveDirection;
1158 gamepadMoveHeldSeconds = 0.0f;
1159 gamepadMoveRepeatTimer = 0.0f;
1160 movePiece(gamepadMoveDirection, 0);
1161 } else {
1162 gamepadMoveHeldSeconds += deltaSeconds;
1163 const float threshold = (gamepadMoveHeldSeconds < gamepadMoveInitialDelaySeconds) ? gamepadMoveInitialDelaySeconds : gamepadMoveRepeatSeconds;
1164 gamepadMoveRepeatTimer += deltaSeconds;
1165 if (gamepadMoveRepeatTimer >= threshold) {
1166 movePiece(gamepadMoveDirection, 0);
1167 gamepadMoveRepeatTimer = 0.0f;
1168 }
1169 }
1170 }
1171
1172 const bool softDropDown = leftY > gamepadDeadzone;
1173 if (!softDropDown) {
1174 gamepadSoftDropHeld = false;
1175 gamepadSoftDropRepeatTimer = 0.0f;
1176 } else {
1177 const float threshold = gamepadSoftDropHeld ? gamepadSoftDropRepeatSeconds : gamepadSoftDropInitialDelaySeconds;
1178 gamepadSoftDropRepeatTimer += deltaSeconds;
1179 if (gamepadSoftDropRepeatTimer >= threshold) {
1180 softDrop();
1181 gamepadSoftDropRepeatTimer = 0.0f;
1182 lastFall = std::chrono::steady_clock::now();
1183 gamepadSoftDropHeld = true;
1184 }
1185 }
1186
1187 if (std::abs(rightX) > gamepadDeadzone) {
1188 gridYaw += static_cast<float>(rightX) * gamepadStickScale * gamepadStickRotateSpeed * deltaSeconds;
1189 }
1190 if (std::abs(rightY) > gamepadDeadzone) {
1191 gridPitch = std::clamp(gridPitch - static_cast<float>(rightY) * gamepadStickScale * gamepadStickPitchSpeed * deltaSeconds, -70.0f, 70.0f);
1192 }
1193
1194 constexpr float gamepadZoomSpeed = 3.2f;
1195 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
1196 cameraDistance = std::min(9.0f, cameraDistance + gamepadZoomSpeed * deltaSeconds);
1197 }
1198 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
1199 cameraDistance = std::max(1.65f, cameraDistance - gamepadZoomSpeed * deltaSeconds);
1200 }
1201 }
1202
1203 void handleGamepadButtonDown(Uint8 button) {
1204 if (gamepad == nullptr) {
1205 return;
1206 }
1207 if (screenTransitionActive) {
1208 return;
1209 }
1210
1211 if (introActive) {
1212 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1213 introActive = false;
1214 requestScreen(AppScreen::Menu);
1215 }
1216 return;
1217 }
1218
1219 switch (screen) {
1220 case AppScreen::Menu:
1221 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1222 cursorPos = (cursorPos + 3) % 4;
1223 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1224 cursorPos = (cursorPos + 1) % 4;
1225 } else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1226 if (cursorPos == 0) {
1227 startGame();
1228 } else if (cursorPos == 1) {
1229 goToNetworkMultiplayer();
1230 } else if (cursorPos == 2) {
1231 goToHighScores();
1232 } else if (cursorPos == 3) {
1233 goToCredits();
1234 }
1235 } else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1236 exit();
1237 }
1238 break;
1239 case AppScreen::Game:
1240 if (gameOver) {
1241 if (button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_SOUTH) {
1242 restartIntroSequence();
1243 } else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1244 goToMenu();
1245 }
1246 return;
1247 }
1248 if (lineClearActive) {
1249 return;
1250 }
1251 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1252 rotatePiece();
1253 } else if (button == SDL_GAMEPAD_BUTTON_EAST) {
1254 hardDrop();
1255 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
1256 goToMenu();
1257 }
1258 break;
1260 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1261 if (enteringName) {
1262 commitScoreEntry();
1263 } else {
1264 restartIntroSequence();
1265 }
1266 } else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1267 highScoresAfterSave = false;
1268 goToMenu();
1269 }
1270 break;
1273 case AppScreen::Credits:
1274 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1275 highScoresAfterSave = false;
1276 goToMenu();
1277 }
1278 break;
1279 case AppScreen::Intro:
1280 break;
1281 }
1282 }
1283
1284 bool openGamepad(SDL_JoystickID id) {
1285 if (gamepad != nullptr && gamepadId == id) {
1286 return true;
1287 }
1288 closeGamepad();
1289 gamepad = SDL_OpenGamepad(id);
1290 if (gamepad == nullptr) {
1291 return false;
1292 }
1293 gamepadId = id;
1294 return true;
1295 }
1296
1297 void closeGamepad() {
1298 if (gamepad != nullptr) {
1299 SDL_CloseGamepad(gamepad);
1300 gamepad = nullptr;
1301 gamepadId = 0;
1302 }
1303 }
1304
1305 void tryOpenFirstGamepad() {
1306 if (gamepad != nullptr) {
1307 return;
1308 }
1309 int count = 0;
1310 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1311 if (ids == nullptr || count <= 0) {
1312 if (ids != nullptr) {
1313 SDL_free(ids);
1314 }
1315 return;
1316 }
1317 openGamepad(ids[0]);
1318 SDL_free(ids);
1319 }
1320
1321 void updateIntroState() {
1322 if (!introActive) {
1323 return;
1324 }
1325
1326 const auto now = std::chrono::steady_clock::now();
1327 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1328 if (!introFadeStarted && elapsed >= introHoldSeconds) {
1329 introFadeStarted = true;
1330 requestScreen(AppScreen::Menu);
1331 }
1332 if (elapsed >= (introHoldSeconds + introFadeSeconds)) {
1333 introActive = false;
1334 introFadeStarted = false;
1335 lastFall = now;
1336 }
1337 }
1338
1339 void startGame() {
1340 const AppScreen previousScreen = screen;
1341 closeMultiplayerSession();
1342 resetGame();
1343 introActive = false;
1344 screen = AppScreen::Game;
1345 screenTransitionActive = previousScreen != AppScreen::Game;
1346 transitionFromScreen = previousScreen;
1347 screenTransitionStart = std::chrono::steady_clock::now();
1348 lastInputUpdate = std::chrono::steady_clock::now();
1349 resetMenuLatchState();
1350 }
1351
1352 void restartIntroSequence() {
1353 resetGame();
1354 highScoresAfterSave = false;
1355 introActive = true;
1356 introFadeStarted = false;
1357 introStart = std::chrono::steady_clock::now();
1358 screen = AppScreen::Intro;
1359 screenTransitionActive = false;
1360 transitionFromScreen = AppScreen::Intro;
1361 lastInputUpdate = std::chrono::steady_clock::now();
1362 resetMenuLatchState();
1363 introSkipHeld = true;
1364 }
1365
1366 void enterGameOverState() {
1367 screen = AppScreen::GameOver;
1368 gameOverTransitionActive = true;
1369 gameOverTransitionStart = std::chrono::steady_clock::now();
1370 nameCharIndex = 0;
1371 playerName.clear();
1372 if (multiplayerActive) {
1373 finishLocalMultiplayerGameOver();
1374 resetMenuLatchState();
1375 return;
1376 }
1377 enteringName = highScores.qualifies(score);
1378 highScoresAfterSave = false;
1379 resetMenuLatchState();
1380 }
1381
1382 void stopNameEntry() {
1383 enteringName = false;
1384 nameCharIndex = 0;
1385 playerName.clear();
1386 }
1387
1388 void commitScoreEntry() {
1389 highScores.addScore(playerName, score);
1390 stopNameEntry();
1391 highScoresAfterSave = true;
1392 menuEnterHeld = true;
1393 goToHighScores();
1394 }
1395
1396 [[nodiscard]] char currentNameCharacter() const { return nameCharacters[nameCharIndex % nameCharacterCount]; }
1397
1398 void cycleNameCharacter(int delta) {
1399 if (!enteringName) {
1400 return;
1401 }
1402 const int count = static_cast<int>(nameCharacterCount);
1403 const int next = (static_cast<int>(nameCharIndex) + delta + count) % count;
1404 nameCharIndex = static_cast<std::size_t>(next);
1405 }
1406
1407 void appendCurrentNameCharacter() {
1408 if (!enteringName || playerName.size() >= nameMaxLength) {
1409 return;
1410 }
1411 playerName += currentNameCharacter();
1412 }
1413
1414 void deleteNameCharacter() {
1415 if (!enteringName || playerName.empty()) {
1416 return;
1417 }
1418 playerName.pop_back();
1419 }
1420
1421 void handleNameEntryKey(SDL_Keycode key) {
1422 if (key == SDLK_UP) {
1423 cycleNameCharacter(-1);
1424 } else if (key == SDLK_DOWN) {
1425 cycleNameCharacter(1);
1426 } else if (key == SDLK_SPACE) {
1427 appendCurrentNameCharacter();
1428 } else if (key == SDLK_BACKSPACE) {
1429 deleteNameCharacter();
1430 } else if (key == SDLK_RETURN) {
1431 commitScoreEntry();
1432 } else if (key == SDLK_ESCAPE) {
1433 highScoresAfterSave = false;
1434 goToMenu();
1435 }
1436 }
1437
1438 void handleNameEntryButton(Uint8 button) {
1439 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1440 cycleNameCharacter(-1);
1441 } else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1442 cycleNameCharacter(1);
1443 } else if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1444 appendCurrentNameCharacter();
1445 } else if (button == SDL_GAMEPAD_BUTTON_WEST) {
1446 commitScoreEntry();
1447 } else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
1448 deleteNameCharacter();
1449 }
1450 }
1451
1452 void goToMenu() {
1453 if (screen == AppScreen::Game || screen == AppScreen::GameOver) {
1454 gameOver = false;
1455 }
1456 if (multiplayerActive || screen == AppScreen::NetworkMultiplayer) {
1457 closeMultiplayerSession();
1458 }
1459 gameOverTransitionActive = false;
1460 highScoresAfterSave = false;
1461 stopNameEntry();
1462 cursorPos = 0;
1463 requestScreen(AppScreen::Menu);
1464 }
1465
1466 void goToHighScores() {
1467 gameOver = false;
1468 stopNameEntry();
1469 requestScreen(AppScreen::HighScores);
1470 }
1471
1472 void goToCredits() { requestScreen(AppScreen::Credits); }
1473
1474 void goToNetworkMultiplayer() { requestScreen(AppScreen::NetworkMultiplayer); }
1475
1476 void requestScreen(AppScreen nextScreen) {
1477 if (screen == nextScreen && !screenTransitionActive) {
1478 return;
1479 }
1480 if (nextScreen == AppScreen::Game) {
1481 screenTransitionActive = false;
1482 screen = nextScreen;
1483 return;
1484 }
1485 if (screen == AppScreen::Game || screen == AppScreen::GameOver) {
1486 screenTransitionActive = false;
1487 screen = nextScreen;
1488 return;
1489 }
1490 transitionFromScreen = screen;
1491 screen = nextScreen;
1492 screenTransitionActive = true;
1493 screenTransitionStart = std::chrono::steady_clock::now();
1494 resetMenuLatchState();
1495 }
1496
1497 void updateScreenTransition() {
1498 if (!screenTransitionActive) {
1499 return;
1500 }
1501 const auto now = std::chrono::steady_clock::now();
1502 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1503 if (elapsed >= screenTransitionSeconds) {
1504 screenTransitionActive = false;
1505 transitionFromScreen = screen;
1506 resetMenuLatchState();
1507 }
1508 }
1509
1510 float screenFadeAlpha() const {
1511 if (!screenTransitionActive) {
1512 return 1.0f;
1513 }
1514 const auto now = std::chrono::steady_clock::now();
1515 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1516 return std::clamp(elapsed / screenTransitionSeconds, 0.0f, 1.0f);
1517 }
1518
1519 float gameOverFadeAlpha() const {
1520 if (!gameOverTransitionActive) {
1521 return 1.0f;
1522 }
1523 const auto now = std::chrono::steady_clock::now();
1524 const float elapsed = std::chrono::duration<float>(now - gameOverTransitionStart).count();
1525 return std::clamp(elapsed / gameOverTransitionSeconds, 0.0f, 1.0f);
1526 }
1527
1528 void resetMenuLatchState() {
1529 menuUpHeld = false;
1530 menuDownHeld = false;
1531 menuEnterHeld = false;
1532 introSkipHeld = false;
1533 backspaceHeld = false;
1534 hostHeld = false;
1535 joinHeld = false;
1536 multiplayerCursor = 0;
1537 }
1538
1539 void updateMenuInputLatchState() {
1540 const bool *keys = SDL_GetKeyboardState(nullptr);
1541 if (keys == nullptr) {
1542 return;
1543 }
1544 menuUpHeld = keys[SDL_SCANCODE_UP];
1545 menuDownHeld = keys[SDL_SCANCODE_DOWN];
1546 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
1547 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
1548 }
1549
1550 void handleHeldViewRotation(const bool *keys, float deltaSeconds) {
1551 constexpr float yawSpeed = 115.0f;
1552 constexpr float pitchSpeed = 90.0f;
1553 constexpr float rollSpeed = 100.0f;
1554 constexpr float zoomSpeed = 3.2f;
1555
1556 if (keys[SDL_SCANCODE_A]) {
1557 gridYaw -= yawSpeed * deltaSeconds;
1558 }
1559 if (keys[SDL_SCANCODE_D]) {
1560 gridYaw += yawSpeed * deltaSeconds;
1561 }
1562 if (keys[SDL_SCANCODE_W]) {
1563 gridPitch = std::clamp(gridPitch + pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1564 }
1565 if (keys[SDL_SCANCODE_S]) {
1566 gridPitch = std::clamp(gridPitch - pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1567 }
1568 if (keys[SDL_SCANCODE_Q]) {
1569 gridRoll -= rollSpeed * deltaSeconds;
1570 }
1571 if (keys[SDL_SCANCODE_E]) {
1572 gridRoll += rollSpeed * deltaSeconds;
1573 }
1574 if (keys[SDL_SCANCODE_PAGEUP]) {
1575 cameraDistance = std::max(1.65f, cameraDistance - zoomSpeed * deltaSeconds);
1576 }
1577 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1578 cameraDistance = std::min(9.0f, cameraDistance + zoomSpeed * deltaSeconds);
1579 }
1580 }
1581
1582 void handleHeldPieceMovement(const bool *keys, float deltaSeconds) {
1583 if (gameOver || lineClearActive) {
1584 moveRepeatTimer = 0.0f;
1585 softDropRepeatTimer = 0.0f;
1586 return;
1587 }
1588
1589 constexpr float horizontalRepeatSeconds = 0.14f;
1590 constexpr float softDropRepeatSeconds = 0.075f;
1591
1592 moveRepeatTimer += deltaSeconds;
1593 softDropRepeatTimer += deltaSeconds;
1594
1595 int horizontalDirection = 0;
1596 if (keys[SDL_SCANCODE_LEFT] && !keys[SDL_SCANCODE_RIGHT]) {
1597 horizontalDirection = -1;
1598 } else if (keys[SDL_SCANCODE_RIGHT] && !keys[SDL_SCANCODE_LEFT]) {
1599 horizontalDirection = 1;
1600 }
1601
1602 if (horizontalDirection != 0) {
1603 if (moveRepeatTimer >= horizontalRepeatSeconds) {
1604 movePiece(horizontalDirection, 0);
1605 moveRepeatTimer = 0.0f;
1606 }
1607 } else {
1608 moveRepeatTimer = horizontalRepeatSeconds;
1609 }
1610
1611 if (keys[SDL_SCANCODE_DOWN]) {
1612 if (softDropRepeatTimer >= softDropRepeatSeconds) {
1613 softDrop();
1614 softDropRepeatTimer = 0.0f;
1615 lastFall = std::chrono::steady_clock::now();
1616 }
1617 } else {
1618 softDropRepeatTimer = softDropRepeatSeconds;
1619 }
1620 }
1621
1622 void handleOneShotKeys(const bool *keys) {
1623 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1624 const bool hardDropDown = keys[SDL_SCANCODE_Z];
1625 const bool rotateDown = keys[SDL_SCANCODE_UP];
1626 const bool resetDown = keys[SDL_SCANCODE_R];
1627 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1628
1629 if (escapeDown && !escapeHeld) {
1630 goToMenu();
1631 }
1632 if (gameOver && enterDown && !enterHeld) {
1633 restartIntroSequence();
1634 escapeHeld = escapeDown;
1635 hardDropHeld = hardDropDown;
1636 rotateHeld = rotateDown;
1637 resetHeld = resetDown;
1638 enterHeld = enterDown;
1639 return;
1640 }
1641 if (hardDropDown && !hardDropHeld) {
1642 hardDrop();
1643 }
1644 if (rotateDown && !rotateHeld) {
1645 rotatePiece();
1646 }
1647 if (resetDown && !resetHeld) {
1648 resetGame();
1649 }
1650
1651 escapeHeld = escapeDown;
1652 hardDropHeld = hardDropDown;
1653 rotateHeld = rotateDown;
1654 resetHeld = resetDown;
1655 enterHeld = enterDown;
1656 }
1657
1658 void handleGameOverKeys(const bool *keys) {
1659 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1660 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1661 const bool resetDown = keys[SDL_SCANCODE_R];
1662 const bool highScoresDown = keys[SDL_SCANCODE_H];
1663 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1664
1665 if (enteringName) {
1666 if (backspaceDown && !backspaceHeld && !playerName.empty()) {
1667 playerName.pop_back();
1668 }
1669 if (enterDown && !enterHeld) {
1670 commitScoreEntry();
1671 backspaceHeld = backspaceDown;
1672 enterHeld = enterDown;
1673 escapeHeld = escapeDown;
1674 resetHeld = resetDown;
1675 menuEnterHeld = highScoresDown;
1676 return;
1677 }
1678
1679 backspaceHeld = backspaceDown;
1680 enterHeld = enterDown;
1681 escapeHeld = escapeDown;
1682 resetHeld = resetDown;
1683 menuEnterHeld = highScoresDown;
1684 return;
1685 } else {
1686 if (enterDown && !enterHeld) {
1687 restartIntroSequence();
1688 backspaceHeld = backspaceDown;
1689 enterHeld = enterDown;
1690 escapeHeld = escapeDown;
1691 resetHeld = resetDown;
1692 menuEnterHeld = highScoresDown;
1693 return;
1694 }
1695 }
1696
1697 if (resetDown && !resetHeld) {
1698 restartIntroSequence();
1699 }
1700 if (highScoresDown && !menuEnterHeld) {
1701 stopNameEntry();
1702 goToHighScores();
1703 }
1704 if (escapeDown && !escapeHeld) {
1705 goToMenu();
1706 }
1707
1708 backspaceHeld = backspaceDown;
1709 enterHeld = enterDown;
1710 escapeHeld = escapeDown;
1711 resetHeld = resetDown;
1712 menuEnterHeld = highScoresDown;
1713 }
1714
1715 [[nodiscard]] bool collides(int pieceX, int pieceY, const std::array<Block, 4> &blocks) const {
1716 for (const Block &block : blocks) {
1717 const int x = pieceX + block.x;
1718 const int y = pieceY + block.y;
1719 if (x < 0 || x >= boardWidth || y < 0) {
1720 return true;
1721 }
1722 if (y >= boardHeight) {
1723 continue;
1724 }
1725 if (board[y][x].color >= 0) {
1726 return true;
1727 }
1728 }
1729 return false;
1730 }
1731
1732 void movePiece(int dx, int dy) {
1733 if (!gameOver && !lineClearActive && !collides(active.x + dx, active.y + dy, active.blocks)) {
1734 active.x += dx;
1735 active.y += dy;
1736 }
1737 }
1738
1739 void softDrop() {
1740 if (gameOver || lineClearActive) {
1741 return;
1742 }
1743 if (collides(active.x, active.y - 1, active.blocks)) {
1744 lockPiece();
1745 } else {
1746 --active.y;
1747 }
1748 }
1749
1750 void hardDrop() {
1751 if (gameOver || lineClearActive) {
1752 return;
1753 }
1754 while (!collides(active.x, active.y - 1, active.blocks)) {
1755 --active.y;
1756 }
1757 lockPiece();
1758 }
1759
1760 void rotatePiece() {
1761 if (gameOver || lineClearActive) {
1762 return;
1763 }
1764 const auto rotated = rotatedBlocks(active);
1765 if (!collides(active.x, active.y, rotated)) {
1766 active.blocks = rotated;
1767 return;
1768 }
1769 if (!collides(active.x - 1, active.y, rotated)) {
1770 --active.x;
1771 active.blocks = rotated;
1772 return;
1773 }
1774 if (!collides(active.x + 1, active.y, rotated)) {
1775 ++active.x;
1776 active.blocks = rotated;
1777 }
1778 }
1779
1780 void updateGame() {
1781 if (screen != AppScreen::Game || introActive) {
1782 return;
1783 }
1784 if (gameOver) {
1785 return;
1786 }
1787
1788 if (lineClearActive) {
1789 const auto now = std::chrono::steady_clock::now();
1790 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1791 if (elapsed >= 1.0f) {
1792 finalizeLineClear();
1793 }
1794 return;
1795 }
1796
1797 const auto now = std::chrono::steady_clock::now();
1798 const float elapsed = std::chrono::duration<float>(now - lastFall).count();
1799 if (elapsed >= fallSeconds) {
1800 softDrop();
1801 lastFall = now;
1802 }
1803 }
1804
1805 void lockPiece() {
1806 for (const Block &block : active.blocks) {
1807 const int x = active.x + block.x;
1808 const int y = active.y + block.y;
1809 if (x >= 0 && x < boardWidth && y >= 0 && y < boardHeight) {
1810 board[y][x].color = active.color;
1811 LockedBlock locked{};
1812 locked.block = loadBlockModel(active.color);
1813 locked.x = x;
1814 locked.y = y;
1815 lockedBlocks.push_back(std::move(locked));
1816 }
1817 }
1818 const auto fullRows = findFullRows();
1819 if (std::any_of(fullRows.begin(), fullRows.end(), [](bool value) { return value; })) {
1820 startLineClear(fullRows);
1821 } else {
1822 spawnPiece();
1823 }
1824 lastFall = std::chrono::steady_clock::now();
1825 }
1826
1827 [[nodiscard]] std::array<bool, boardHeight> findFullRows() const {
1828 std::array<bool, boardHeight> fullRows{};
1829 for (int y = 0; y < boardHeight; ++y) {
1830 bool full = true;
1831 for (int x = 0; x < boardWidth; ++x) {
1832 full = full && board[y][x].color >= 0;
1833 }
1834 fullRows[y] = full;
1835 }
1836 return fullRows;
1837 }
1838
1839 void startLineClear(const std::array<bool, boardHeight> &fullRows) {
1840 clearingRows = fullRows;
1841 lineClearActive = true;
1842 lineClearStart = std::chrono::steady_clock::now();
1843 }
1844
1845 void finalizeLineClear() {
1846 int cleared = 0;
1847 for (bool row : clearingRows) {
1848 if (row) {
1849 ++cleared;
1850 }
1851 }
1852
1853 int writeY = 0;
1854 for (int readY = 0; readY < boardHeight; ++readY) {
1855 if (clearingRows[readY]) {
1856 continue;
1857 }
1858 if (writeY != readY) {
1859 board[writeY] = board[readY];
1860 }
1861 ++writeY;
1862 }
1863 for (; writeY < boardHeight; ++writeY) {
1864 for (Cell &cell : board[writeY]) {
1865 cell.color = -1;
1866 }
1867 }
1868
1869 eraseClearedModels(clearingRows);
1870 clearingRows.fill(false);
1871 lineClearActive = false;
1872 if (cleared > 0) {
1873 score += cleared * 10;
1874 if (cleared > 1) {
1875 score += 5;
1876 }
1877 linesCleared += cleared;
1878 updateDifficulty();
1879 }
1880 spawnPiece();
1881 lastFall = std::chrono::steady_clock::now();
1882 }
1883
1884 void eraseClearedModels(const std::array<bool, boardHeight> &fullRows) {
1885 std::vector<LockedBlock> remaining{};
1886 remaining.reserve(lockedBlocks.size());
1887 for (LockedBlock &locked : lockedBlocks) {
1888 if (locked.y >= 0 && locked.y < boardHeight && fullRows[locked.y]) {
1889 if (locked.block.model) {
1890 locked.block.model->cleanup(this);
1891 }
1892 continue;
1893 }
1894
1895 int rowsBelow = 0;
1896 for (int y = 0; y < locked.y; ++y) {
1897 if (fullRows[y]) {
1898 ++rowsBelow;
1899 }
1900 }
1901 locked.y -= rowsBelow;
1902 remaining.push_back(std::move(locked));
1903 }
1904 lockedBlocks = std::move(remaining);
1905 }
1906
1907 [[nodiscard]] bool isClearingRow(int y) const { return lineClearActive && y >= 0 && y < boardHeight && clearingRows[y]; }
1908
1909 [[nodiscard]] bool isLineClearVisible() const {
1910 const auto now = std::chrono::steady_clock::now();
1911 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1912 constexpr float blinkIntervalSeconds = 0.12f;
1913 return (static_cast<int>(elapsed / blinkIntervalSeconds) % 2) == 0;
1914 }
1915
1916 void drawIntroOverlay(VkCommandBuffer cmd, const VkExtent2D &extent) {
1917 if (!introActive || introSprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1918 return;
1919 }
1920
1921 const auto now = std::chrono::steady_clock::now();
1922 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1923 const float fadeProgress = std::clamp((elapsed - introHoldSeconds) / introFadeSeconds, 0.0f, 1.0f);
1924 const float alpha = 1.0f - fadeProgress;
1925
1926 introSprite->setShaderParams(elapsed, 0.0f, 0.0f, alpha);
1927 introSprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1928 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1929 introSprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1930 introSprite->clearQueue();
1931 }
1932
1933 void drawGameOverOverlay(VkCommandBuffer cmd, const VkExtent2D &extent) {
1934 const bool shouldShow = (screen == AppScreen::Game && gameOver) || screen == AppScreen::GameOver;
1935 if (!shouldShow || gameOverSprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1936 return;
1937 }
1938
1939 const float alpha = gameOverFadeAlpha();
1940 gameOverSprite->setShaderParams(0.0f, 0.0f, 0.0f, alpha);
1941 gameOverSprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1942 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1943 gameOverSprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1944 gameOverSprite->clearQueue();
1945 }
1946
1947 [[nodiscard]] glm::vec3 blockPosition(int x, int y) const {
1948 const float worldX = (static_cast<float>(x) - (static_cast<float>(boardWidth) - 1.0f) * 0.5f) * cubeSpacing;
1949 const float worldY = (static_cast<float>(y) - (static_cast<float>(boardHeight) - 1.0f) * 0.5f) * cubeSpacing;
1950 return glm::vec3(worldX, worldY, 0.0f);
1951 }
1952
1953 [[nodiscard]] glm::mat4 blockMatrix(int x, int y, float scale = cubeScale) const {
1954 glm::mat4 model(1.0f);
1955 model = glm::translate(model, blockPosition(x, y));
1956 model = glm::scale(model, glm::vec3(scale));
1957 return model;
1958 }
1959
1960 void drawBlock(VkCommandBuffer cmd, uint32_t imageIndex, BlockModel &block, int x, int y, int color, const glm::mat4 &view, const glm::mat4 &proj) {
1961 mxvk::UniformBufferObject ubo{};
1962 ubo.model = blockMatrix(x, y);
1963 ubo.view = view;
1964 ubo.proj = proj;
1965 ubo.fx = glm::vec4(colorTints[color], 1.0f);
1966 block.model->updateUBO(imageIndex, ubo);
1967 block.model->render(cmd, imageIndex, false);
1968 }
1969
1970 void drawFrame(VkCommandBuffer cmd, uint32_t imageIndex, const glm::mat4 &view, const glm::mat4 &proj) {
1971 size_t index = 0;
1972 for (int y = 0; y < boardHeight; ++y) {
1973 drawFrameBlock(cmd, imageIndex, frameModels[index++], -1, y, view, proj);
1974 drawFrameBlock(cmd, imageIndex, frameModels[index++], boardWidth, y, view, proj);
1975 }
1976 for (int x = -1; x <= boardWidth; ++x) {
1977 drawFrameBlock(cmd, imageIndex, frameModels[index++], x, -1, view, proj);
1978 }
1979 }
1980
1981 void drawFrameBlock(VkCommandBuffer cmd, uint32_t imageIndex, BlockModel &block, int x, int y, const glm::mat4 &view, const glm::mat4 &proj) {
1982 mxvk::UniformBufferObject ubo{};
1983 ubo.model = blockMatrix(x, y, cubeScale * 0.82f);
1984 ubo.view = view;
1985 ubo.proj = proj;
1986 ubo.fx = glm::vec4(colorTints[7], 1.0f);
1987 block.model->updateUBO(imageIndex, ubo);
1988 block.model->render(cmd, imageIndex, false);
1989 }
1990
1991 void drawSpriteRect(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd, const VkExtent2D &extent, int x, int y, int w, int h) {
1992 if (sprite == nullptr) {
1993 return;
1994 }
1995 sprite->drawSpriteRect(x, y, w, h);
1996 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1997 sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1998 sprite->clearQueue();
1999 }
2000
2001 void drawNextPiecePreview(VkCommandBuffer cmd, [[maybe_unused]] uint32_t imageIndex, const VkExtent2D &extent) {
2002 if (screen != AppScreen::Game || introActive || previewBorderSprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
2003 return;
2004 }
2005
2006 const int panelSize = std::min({220, static_cast<int>(static_cast<float>(extent.width) * 0.28f), static_cast<int>(static_cast<float>(extent.height) * 0.34f)});
2007 const int panelW = panelSize;
2008 const int panelH = panelSize;
2009 const int margin = 24;
2010 const int panelX = static_cast<int>(extent.width) - panelW - margin;
2011 const int panelY = 88;
2012 const int border = 4;
2013
2014 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, panelW, border);
2015 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY + panelH - border, panelW, border);
2016 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, border, panelH);
2017 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + panelW - border, panelY, border, panelH);
2018
2019 printText("Next", panelX + 12, panelY - 28, SDL_Color{255, 255, 255, 255});
2020
2021 int minX = nextPiece.blocks[0].x;
2022 int maxX = nextPiece.blocks[0].x;
2023 int minY = nextPiece.blocks[0].y;
2024 int maxY = nextPiece.blocks[0].y;
2025 for (const Block &block : nextPiece.blocks) {
2026 minX = std::min(minX, block.x);
2027 maxX = std::max(maxX, block.x);
2028 minY = std::min(minY, block.y);
2029 maxY = std::max(maxY, block.y);
2030 }
2031
2032 const float innerPadding = 28.0f;
2033 const float innerSize = static_cast<float>(panelSize) - innerPadding * 2.0f;
2034 const int blockSize = static_cast<int>(std::min(34.0f, innerSize / static_cast<float>(std::max(maxX - minX + 1, maxY - minY + 1))));
2035 const float pieceW = static_cast<float>(maxX - minX + 1) * blockSize;
2036 const float pieceH = static_cast<float>(maxY - minY + 1) * blockSize;
2037 const float centerX = static_cast<float>(panelX) + static_cast<float>(panelW) * 0.5f;
2038 const float centerY = static_cast<float>(panelY) + static_cast<float>(panelH) * 0.5f;
2039 const float originX = centerX - pieceW * 0.5f;
2040 const float originY = centerY - pieceH * 0.5f;
2041 mxvk::VK_Sprite *blockSprite = blockPreviewSprites[static_cast<std::size_t>(nextPiece.color)];
2042
2043 for (std::size_t i = 0; i < nextPiece.blocks.size(); ++i) {
2044 const Block &pieceBlock = nextPiece.blocks[i];
2045 const float x = originX + static_cast<float>(pieceBlock.x - minX) * blockSize;
2046 const float y = originY + static_cast<float>(pieceBlock.y - minY) * blockSize;
2047 drawSpriteRect(blockSprite, cmd, extent, static_cast<int>(x), static_cast<int>(y), blockSize, blockSize);
2048 }
2049 }
2050
2051 void drawOpponentGrid(VkCommandBuffer cmd, const VkExtent2D &extent) {
2052 if (!multiplayerActive || screen != AppScreen::Game || introActive || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE || previewBorderSprite == nullptr) {
2053 return;
2054 }
2055
2056 const int margin = 24;
2057 const int previewSize = std::min({220, static_cast<int>(static_cast<float>(extent.width) * 0.28f), static_cast<int>(static_cast<float>(extent.height) * 0.34f)});
2058 const int previewBottom = 88 + previewSize;
2059 const int panelTop = previewBottom + 56;
2060 const int textAreaHeight = 78;
2061 const int panelBottom = static_cast<int>(extent.height) - margin - textAreaHeight;
2062 const int availableHeight = panelBottom - panelTop;
2063 const int availableWidth = std::min(260, static_cast<int>(static_cast<float>(extent.width) * 0.24f));
2064 if (availableWidth < 70 || availableHeight < 120) {
2065 return;
2066 }
2067 const int blockSize = std::max(4, std::min(availableWidth / boardWidth, availableHeight / boardHeight));
2068 const int gridW = blockSize * boardWidth;
2069 const int gridH = blockSize * boardHeight;
2070 const int panelX = static_cast<int>(extent.width) - gridW - margin;
2071 const int panelY = panelTop + std::max(0, (availableHeight - gridH) / 2);
2072 const int border = std::max(2, blockSize / 5);
2073 const auto textXInsideRightEdge = [this, &extent, margin, panelX](const std::initializer_list<std::string> &lines) {
2074 int maxWidth = 0;
2075 for (const std::string &line : lines) {
2076 int width = 0;
2077 int height = 0;
2078 if (getTextDimensions(line, width, height)) {
2079 maxWidth = std::max(maxWidth, width);
2080 }
2081 }
2082 return std::max(margin, std::min(panelX, static_cast<int>(extent.width) - margin - maxWidth));
2083 };
2084
2085 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, gridW + border * 2, border);
2086 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY + gridH, gridW + border * 2, border);
2087 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, border, gridH + border * 2);
2088 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + gridW, panelY - border, border, gridH + border * 2);
2089
2090 printText("Opponent", panelX, panelY - 30, SDL_Color{255, 255, 255, 255});
2091 if (!opponentSnapshot.hasState) {
2092 const std::string waitingText = "Waiting...";
2093 printText(waitingText, textXInsideRightEdge({waitingText}), panelY + gridH + 12, SDL_Color{255, 220, 120, 255});
2094 return;
2095 }
2096
2097 for (int y = 0; y < boardHeight; ++y) {
2098 for (int x = 0; x < boardWidth; ++x) {
2099 const int color = opponentSnapshot.cells[y][x];
2100 if (color < 0 || color >= static_cast<int>(blockPreviewSprites.size())) {
2101 continue;
2102 }
2103 const int screenX = panelX + x * blockSize;
2104 const int screenY = panelY + (boardHeight - 1 - y) * blockSize;
2105 drawSpriteRect(blockPreviewSprites[static_cast<std::size_t>(color)], cmd, extent, screenX, screenY, blockSize, blockSize);
2106 }
2107 }
2108
2109 if (!opponentSnapshot.gameOver) {
2110 for (const Block &block : opponentSnapshot.activeBlocks) {
2111 const int x = opponentSnapshot.activeX + block.x;
2112 const int y = opponentSnapshot.activeY + block.y;
2113 if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight) {
2114 continue;
2115 }
2116 const int screenX = panelX + x * blockSize;
2117 const int screenY = panelY + (boardHeight - 1 - y) * blockSize;
2118 drawSpriteRect(blockPreviewSprites[static_cast<std::size_t>(opponentSnapshot.activeColor)], cmd, extent, screenX, screenY, blockSize, blockSize);
2119 }
2120 }
2121
2122 const std::string scoreText = std::format("Score {}", opponentSnapshot.score);
2123 const std::string linesText = std::format("Lines {} Lv {}", opponentSnapshot.lines, opponentSnapshot.level);
2124 const std::string gameOverText = "Game Over";
2125 const int statsX = textXInsideRightEdge({scoreText, linesText, gameOverText});
2126 printText(scoreText, statsX, panelY + gridH + 12, SDL_Color{255, 255, 255, 255});
2127 printText(linesText, statsX, panelY + gridH + 38, SDL_Color{180, 220, 255, 255});
2128 if (opponentSnapshot.gameOver) {
2129 printText(gameOverText, statsX, panelY + gridH + 64, SDL_Color{255, 120, 120, 255});
2130 }
2131 }
2132
2133 void drawCreditsModel(VkCommandBuffer cmd, uint32_t imageIndex, const VkExtent2D &extent) {
2134 if (creditsTuxModel == nullptr) {
2135 return;
2136 }
2137
2138 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
2139 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.25f, 2.8f), glm::vec3(0.0f, 0.05f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
2140 view = glm::rotate(view, glm::radians(-10.0f), glm::vec3(1.0f, 0.0f, 0.0f));
2141 glm::mat4 proj = glm::perspective(glm::radians(40.0f), aspect, 0.1f, 100.0f);
2142 proj[1][1] *= -1.0f;
2143
2144 const float elapsed = static_cast<float>(SDL_GetTicks()) / 1000.0f;
2145 glm::mat4 model(1.0f);
2146 model = glm::translate(model, glm::vec3(0.0f, -0.34f, 0.0f));
2147 model = glm::rotate(model, elapsed * 0.75f, glm::vec3(0.0f, 1.0f, 0.0f));
2148 model = glm::rotate(model, std::sin(elapsed * 0.6f) * 0.08f, glm::vec3(1.0f, 0.0f, 0.0f));
2149 model = glm::scale(model, glm::vec3(0.228f));
2150
2151 mxvk::UniformBufferObject ubo{};
2152 ubo.model = model;
2153 ubo.view = view;
2154 ubo.proj = proj;
2155 ubo.fx = glm::vec4(1.0f, 1.0f, 1.0f, elapsed);
2156 creditsTuxModel->updateUBO(imageIndex, ubo);
2157 creditsTuxModel->render(cmd, imageIndex, false);
2158 }
2159
2160 void drawHud() {
2161 if (introActive) {
2162 return;
2163 }
2164 const VkExtent2D extent = getSwapchainExtent();
2165 const int centerX = static_cast<int>(extent.width) / 2;
2166 const float alpha = screenFadeAlpha();
2167 const auto withAlpha = [alpha](SDL_Color color) {
2168 color.a = static_cast<Uint8>(static_cast<float>(color.a) * alpha);
2169 return color;
2170 };
2171
2172 switch (screen) {
2173 case AppScreen::Menu: {
2174 const int titleY = static_cast<int>(static_cast<float>(extent.height) * 0.18f);
2175 const int menuY = static_cast<int>(static_cast<float>(extent.height) * 0.40f);
2176 const int spacing = static_cast<int>(std::max(34.0f, static_cast<float>(extent.height) * 0.07f));
2177 printCenteredText("MXVK 3D Tetris", centerX, titleY, withAlpha(SDL_Color{255, 255, 0, 255}));
2178 printCenteredText("Choose a mode", centerX, titleY + 42, withAlpha(SDL_Color{255, 255, 255, 255}));
2179
2180 const char *items[] = {"New Game", "Network Multiplayer", "High Scores", "Credits"};
2181 for (int i = 0; i < 4; ++i) {
2182 const SDL_Color color = (i == cursorPos) ? SDL_Color{255, 255, 0, 255} : SDL_Color{255, 255, 255, 255};
2183 printCenteredText(items[i], centerX, menuY + i * spacing, withAlpha(color));
2184 }
2185 printCenteredText("Use arrows and Enter", centerX, static_cast<int>(static_cast<float>(extent.height) * 0.86f), withAlpha(SDL_Color{180, 180, 180, 255}));
2186 break;
2187 }
2188 case AppScreen::HighScores: {
2189 const int baseY = static_cast<int>(static_cast<float>(extent.height) * 0.16f);
2190 printCenteredText("High Scores", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2191 const auto &scores = highScores.entries();
2192 const int listLeftX = static_cast<int>(static_cast<float>(extent.width) * 0.34f);
2193 const int listRightX = static_cast<int>(static_cast<float>(extent.width) * 0.70f);
2194 const int lineHeight = 30;
2195 for (std::size_t i = 0; i < 10U; ++i) {
2196 const int rowY = baseY + 54 + static_cast<int>(i) * lineHeight;
2197 const bool hasScore = i < scores.size();
2198 const std::string nameText = hasScore ? scores[i].name : "---";
2199 const std::string scoreText = hasScore ? std::format("{}", scores[i].score) : "---";
2200 printText(std::format("{:>2}. {}", i + 1U, nameText), listLeftX, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2201 int scoreWidth = 0;
2202 int scoreHeight = 0;
2203 if (getTextDimensions(scoreText, scoreWidth, scoreHeight)) {
2204 printText(scoreText, listRightX - scoreWidth, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2205 } else {
2206 printText(scoreText, listRightX, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2207 }
2208 }
2209 printCenteredText("Press Enter or Escape to return", centerX, static_cast<int>(static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2210 break;
2211 }
2212 case AppScreen::Credits: {
2213 const int baseY = static_cast<int>(static_cast<float>(extent.height) * 0.2f);
2214 printCenteredText("Credits", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2215 printCenteredText("MXVK 3D Tetris", centerX, baseY + 48, withAlpha(SDL_Color{255, 255, 255, 255}));
2216 printCenteredText("Built with Vulkan and SDL3", centerX, baseY + 84, withAlpha(SDL_Color{255, 220, 120, 255}));
2217 printCenteredText("Press Enter or Escape to return", centerX, static_cast<int>(static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2218 break;
2219 }
2221 const int baseY = static_cast<int>(static_cast<float>(extent.height) * 0.16f);
2222 printCenteredText("Network Multiplayer", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2223 printCenteredText(std::format("Port: {}", multiplayerPort), centerX, baseY + 42, withAlpha(SDL_Color{255, 220, 120, 255}));
2224 printCenteredText(std::format("Peer IP: {}", multiplayerHost.empty() ? "_" : multiplayerHost), centerX, baseY + 86, withAlpha(SDL_Color{255, 255, 255, 255}));
2225 printCenteredText(multiplayerStatus, centerX, baseY + 126, withAlpha(SDL_Color{180, 220, 255, 255}));
2226 printCenteredText("H host J/Enter join Backspace edit", centerX, baseY + 178, withAlpha(SDL_Color{255, 255, 255, 255}));
2227 printCenteredText("Run one copy as host, then connect from the other machine by IP.", centerX, baseY + 216, withAlpha(SDL_Color{200, 200, 200, 255}));
2228 printCenteredText("Escape returns to menu", centerX, static_cast<int>(static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2229 break;
2230 }
2231 case AppScreen::GameOver: {
2232 const int baseY = static_cast<int>(static_cast<float>(extent.height) * 0.20f);
2233 const float gameOverAlpha = gameOverFadeAlpha();
2234 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2235 color.a = static_cast<Uint8>(static_cast<float>(color.a) * gameOverAlpha);
2236 return color;
2237 };
2238
2239 printCenteredText("Game Over", centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 0, 255}));
2240 printCenteredText(std::format("Final Score: {}", score), centerX, baseY + 42, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2241 if (!multiplayerResult.empty()) {
2242 printCenteredText(multiplayerResult, centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2243 printCenteredText("Enter to restart", centerX, baseY + 124, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2244 printCenteredText("Escape for menu", centerX, baseY + 158, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2245 break;
2246 }
2247 if (enteringName) {
2248 printCenteredText("New high score", centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2249 printCenteredText(std::format("Name: {}", playerName.empty() ? "_" : playerName), centerX, baseY + 118, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2250 printCenteredText(std::format("Pick: [{}]", currentNameCharacter()), centerX, baseY + 154, withGameOverAlpha(SDL_Color{120, 255, 255, 255}));
2251 printCenteredText("Up/Down choose, A/Space add", centerX, baseY + 190, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2252 printCenteredText("X/Enter save, B/Backspace delete", centerX, baseY + 224, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2253 } else {
2254 printCenteredText("R to restart from intro", centerX, baseY + 92, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2255 printCenteredText("H for high scores", centerX, baseY + 126, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2256 printCenteredText("Enter to restart", centerX, baseY + 160, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2257 }
2258 break;
2259 }
2260 case AppScreen::Game:
2261 printText(std::format("Score: {}", score), 15, 15, withAlpha(SDL_Color{255, 255, 255, 255}));
2262 printText(std::format("Lines Cleared: {}", linesCleared), 15, 45, withAlpha(SDL_Color{255, 220, 120, 255}));
2263 printText(std::format("Level: {}", level), 15, 75, withAlpha(SDL_Color{120, 220, 255, 255}));
2264 if (multiplayerActive) {
2265 printText(multiplayerHostSide ? "Multiplayer: Host" : "Multiplayer: Guest", 15, 105, withAlpha(SDL_Color{180, 255, 180, 255}));
2266 }
2267 if (gameOver) {
2268 const int baseY = static_cast<int>(static_cast<float>(extent.height) * 0.58f);
2269 const float gameOverAlpha = gameOverFadeAlpha();
2270 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2271 color.a = static_cast<Uint8>(static_cast<float>(color.a) * gameOverAlpha);
2272 return color;
2273 };
2274 printCenteredText(std::format("Final Score: {}", score), centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2275 printCenteredText(multiplayerResult.empty() ? "Game over" : multiplayerResult, centerX, baseY + 40, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2276 }
2277 break;
2278 case AppScreen::Intro:
2279 break;
2280 }
2281 }
2282
2283 void printCenteredText(const std::string &text, int centerX, int y, const SDL_Color &color) {
2284 int textWidth = 0;
2285 int textHeight = 0;
2286 if (getTextDimensions(text, textWidth, textHeight)) {
2287 printText(text, centerX - textWidth / 2, y, color);
2288 return;
2289 }
2290 printText(text, centerX, y, color);
2291 }
2292
2293 void triggerBackgroundTransition() {
2294 backgroundTransitionActive = true;
2295 backgroundTransitionStart = std::chrono::steady_clock::now();
2296 }
2297
2298 void updateBackgroundTransitionState() {
2299 if (!backgroundTransitionActive) {
2300 return;
2301 }
2302 const auto now = std::chrono::steady_clock::now();
2303 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2304 if (elapsed >= backgroundTransitionSeconds) {
2305 backgroundTransitionActive = false;
2306 }
2307 }
2308
2309 [[nodiscard]] mxvk::VK_Sprite *screenSpriteFor(AppScreen screen) const {
2310 switch (screen) {
2311 case AppScreen::Menu:
2312 return menuBackgroundSprite;
2314 return highScoresBackgroundSprite;
2315 case AppScreen::Credits:
2316 return creditsBackgroundSprite;
2318 return multiplayerBackgroundSprite;
2319 case AppScreen::Intro:
2320 case AppScreen::Game:
2322 return nullptr;
2323 }
2324 return nullptr;
2325 }
2326
2327 void drawFadedSprite(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd, const VkExtent2D &extent, float alpha) {
2328 if (sprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
2329 return;
2330 }
2331 sprite->setShaderParams(0.0f, 0.0f, 0.0f, alpha);
2332 sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
2333 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
2334 sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
2335 sprite->clearQueue();
2336 }
2337
2338 void drawGameScreenTransitionOverlay(VkCommandBuffer cmd, const VkExtent2D &extent) {
2339 if (screen != AppScreen::Game || !screenTransitionActive) {
2340 return;
2341 }
2342 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - screenFadeAlpha());
2343 }
2344
2345 void drawScreenBackdrop(VkCommandBuffer cmd, const VkExtent2D &extent) {
2346 if (screen == AppScreen::Game) {
2347 if (sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
2348 return;
2349 }
2350 updateBackgroundTransitionState();
2351
2352 mxvk::VK_Sprite *sprite = background;
2353 if (backgroundTransitionActive && backgroundTransitionSprite != nullptr) {
2354 sprite = backgroundTransitionSprite;
2355 }
2356 if (sprite == nullptr) {
2357 return;
2358 }
2359
2360 if (sprite == backgroundTransitionSprite) {
2361 const auto now = std::chrono::steady_clock::now();
2362 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2363 sprite->setShaderParams(elapsed, 0.0f, 0.0f, 0.0f);
2364 }
2365
2366 sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
2367 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
2368 sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
2369 sprite->clearQueue();
2370 return;
2371 }
2372
2373 const float alpha = screenFadeAlpha();
2374 if (screenTransitionActive) {
2375 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - alpha);
2376 }
2377 drawFadedSprite(screenSpriteFor(screen), cmd, extent, alpha);
2378 }
2379 };
2380
2381} // namespace
2382
2383int main(int argc, char **argv) {
2384 try {
2385 const Arguments args = proc_args(argc, argv);
2386 TetrisWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
2387 window.loop();
2388 } catch (const mxnetwork::Exception &e) {
2389 std::cerr << std::format("mxnetwork: Exception: {}\n", e.text());
2390 return EXIT_FAILURE;
2391 } catch (mxvk::Exception &e) {
2392 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
2393 return EXIT_FAILURE;
2394 } catch (ArgException<std::string> &e) {
2395 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
2396 return EXIT_FAILURE;
2397 }
2398
2399 return EXIT_SUCCESS;
2400}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
HighScores(std::filesystem::path filePath)
Definition tetris.cpp:119
void addScore(std::string name, int score)
Definition tetris.cpp:121
const std::vector< HighScoreEntry > & entries() const
Definition tetris.cpp:135
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
Definition tetris.cpp:337
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t imageIndex) override
Optional hook for derived classes to record extra draw commands.
Definition tetris.cpp:394
void event(SDL_Event &e) override
Handle one SDL event.
Definition tetris.cpp:341
TetrisWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
Definition tetris.cpp:291
void proc() override
Execute one processing/update step.
Definition tetris.cpp:381
Lightweight exception wrapper for MXNetwork failures.
Definition exception.hpp:11
std::string text() const
Return the stored error text.
Definition exception.cpp:5
Convenience wrapper that owns mesh, textures, descriptors, and pipeline state.
void cleanup(VK_Window *window)
Destroy all owned Vulkan resources.
void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued draw commands into the given command buffer.
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void clearQueue()
Discard all pending draw commands without rendering.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkPipelineLayout sprite_pipeline_layout
Definition mxvk.hpp:680
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:222
VkDevice device
Definition mxvk.hpp:606
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:4455
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
VkPipeline sprite_pipeline
Definition mxvk.hpp:681
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
#define MXVK_VALIDATION
Definition mxvk.hpp:28
High-level model wrapper integrated with MXVK dynamic rendering.
SDL3_mixer audio subsystem wrapper.
constexpr std::string_view multiplayerPort
Definition tetris.cpp:46
std::array< Block, 4 > rotatedBlocks(const ActivePiece &piece)
Definition tetris.cpp:269
constexpr char nameCharacters[]
Definition tetris.cpp:44
constexpr std::size_t nameCharacterCount
Definition tetris.cpp:45
std::filesystem::path resolveScoreFilePath()
Definition tetris.cpp:212
const std::array< std::string, 8 > blockTextureFiles
Definition tetris.cpp:247
const std::array< glm::vec3, 8 > colorTints
Definition tetris.cpp:258
const std::array< std::string, 8 > textureManifests
Definition tetris.cpp:236
constexpr float cubeSpacing
Definition tetris.cpp:42
constexpr std::size_t nameMaxLength
Definition tetris.cpp:43
const std::array< PieceDefinition, 7 > pieceDefinitions
Definition tetris.cpp:226
int main()
Definition main.py:165
Definition model.py:1
@ TYPE_INET
IPv4 stream socket.
Definition socket.hpp:41
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
std::atomic< bool > active
Definition relay.cpp:12
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
std::array< std::array< int, boardWidth >, boardHeight > cells
Definition tetris.cpp:91
#define tetris_ASSET_DIR
Definition tetris.cpp:34