R-Type
Distributed multiplayer game engine in C++
Loading...
Searching...
No Matches
GameLoop.cpp
Go to the documentation of this file.
1/*
2** EPITECH PROJECT, 2025
3** Created by samuelBleau on 26/11/2025.
4** File description:
5** GameLoop.cpp
6*/
7
8#include "GameLoop.hpp"
9#include <cmath>
10#include <unordered_set>
11#include "../ClientGameRules.hpp"
12#include "GameruleKeys.hpp"
13#include "Input/KeyBindings.hpp"
14
15GameLoop::GameLoop(EventBus &eventBus, Replicator &replicator, const std::string &playerName)
16 : _eventBus(&eventBus), _replicator(&replicator), _playerName(playerName) {}
17
21
23 if (_initialized) {
24 return true;
25 }
26
27 LOG_INFO("Initializing subsystems...");
28
29 // 1. InputBuffer
30 _inputBuffer = std::make_unique<InputBuffer>();
31 LOG_INFO("InputBuffer initialized");
32
33 // 2. Rendering
34 _rendering = std::make_unique<Rendering>(*_eventBus);
35 LOG_INFO("Rendering initialized");
36
37 // 3. Subscribe to network events for entity updates
38 _eventBus->subscribe<NetworkEvent>([this](const NetworkEvent &event) { handleNetworkMessage(event); });
39 LOG_INFO("Subscribed to NetworkEvent");
40
41 // 4. Subscribe to UI events
42 _eventBus->subscribe<UIEvent>([this](const UIEvent &event) { handleUIEvent(event); });
43 LOG_INFO("Subscribed to UIEvent");
44
45 _initialized = true;
46 LOG_INFO("All subsystems initialized successfully!");
47
48 return true;
49}
50
51void GameLoop::handleUIEvent(const UIEvent &event) {
52 if (event.getType() == UIEventType::JOIN_GAME) {
53 LOG_INFO("[GameLoop] Joining game requested by UI");
54 if (_replicator) {
55 // Get room ID from event data, fallback to "default" if empty
56 std::string roomId = event.getData();
57 if (roomId.empty()) {
58 roomId = "default";
59 LOG_WARNING("[GameLoop] No room ID provided, using default room");
60 }
61
62 LOG_INFO("[GameLoop] Joining room: ", roomId);
64
65 // Wait for S2C_ROOM_STATE from server with player list
66 // No need for mock data anymore
67 }
68 } else if (event.getType() == UIEventType::CREATE_ROOM) {
69 LOG_INFO("[GameLoop] Create room requested by UI");
70 if (_replicator) {
71 // Parse room data (format: "roomName|maxPlayers|isPrivate|gameSpeedMultiplier")
72 const std::string &data = event.getData();
73 size_t pos1 = data.find('|');
74 size_t pos2 = data.find('|', pos1 + 1);
75 size_t pos3 = data.find('|', pos2 + 1);
76
77 if (pos1 != std::string::npos && pos2 != std::string::npos) {
78 std::string roomName = data.substr(0, pos1);
79 uint32_t maxPlayers = std::stoi(data.substr(pos1 + 1, pos2 - pos1 - 1));
80 bool isPrivate = (data.substr(pos2 + 1, pos3 - pos2 - 1) == "1");
81 float gameSpeedMultiplier = 1.0f;
82 if (pos3 != std::string::npos) {
83 gameSpeedMultiplier = std::stof(data.substr(pos3 + 1));
84 }
85
86 LOG_INFO("[GameLoop] Creating room: ", roomName, " (Max: ", maxPlayers,
87 ", Private: ", isPrivate, ", Speed: ", static_cast<int>(gameSpeedMultiplier * 100),
88 "%)");
89 _replicator->sendCreateRoom(roomName, maxPlayers, isPrivate, gameSpeedMultiplier);
90
91 // Mark that we just created a room, so we know we're the host when RoomState comes back
92 _justCreatedRoom = true;
93
94 // Request room list after creation (with small delay)
95 std::this_thread::sleep_for(std::chrono::milliseconds(100));
96 // TODO: Request room list
97 }
98 }
99 } else if (event.getType() == UIEventType::REQUEST_ROOM_LIST) {
100 LOG_INFO("[GameLoop] Room list requested by UI");
101 if (_replicator) {
103 }
105 // Update auto-matchmaking preference on server (from settings menu)
106 LOG_INFO("[GameLoop] Auto-matchmaking preference update");
107 const std::string &data = event.getData();
108 if (_replicator) {
109 bool enabled = (data == "enable");
111 LOG_INFO("[GameLoop] Preference updated (will apply when player clicks Play)");
112 }
113 } else if (event.getType() == UIEventType::AUTO_MATCHMAKING) {
114 // Trigger auto-matchmaking (from Play button)
115 LOG_INFO("[GameLoop] Auto-matchmaking request (triggering matchmaking now)");
116 if (_replicator) {
118 }
119 } else if (event.getType() == UIEventType::START_GAME_REQUEST) {
120 LOG_INFO("[GameLoop] Host requesting game start");
121 if (_replicator) {
123 }
124 } else if (event.getType() == UIEventType::LEAVE_ROOM) {
125 LOG_INFO("[GameLoop] Player leaving room");
126 _justCreatedRoom = false; // Reset host flag when leaving
127 if (_replicator) {
129 }
130 } else if (event.getType() == UIEventType::QUIT_GAME) {
131 stop();
132 } else if (event.getType() == UIEventType::REGISTER_ACCOUNT) {
133 LOG_INFO("[GameLoop] Register account requested by UI");
134 if (_replicator) {
135 // Parse credentials (format: "username:password")
136 const std::string &credentials = event.getData();
137 size_t colonPos = credentials.find(':');
138 if (colonPos != std::string::npos) {
139 std::string username = credentials.substr(0, colonPos);
140 std::string password = credentials.substr(colonPos + 1);
141 LOG_INFO("[GameLoop] Registering account: ", username);
142 _replicator->sendRegisterAccount(username, password);
143 }
144 }
145 } else if (event.getType() == UIEventType::LOGIN_ACCOUNT) {
146 LOG_INFO("[GameLoop] Login account requested by UI");
147 if (_replicator) {
148 // Parse credentials (format: "username:password")
149 const std::string &credentials = event.getData();
150 size_t colonPos = credentials.find(':');
151 if (colonPos != std::string::npos) {
152 std::string username = credentials.substr(0, colonPos);
153 std::string password = credentials.substr(colonPos + 1);
154 LOG_INFO("[GameLoop] Logging in with account: ", username);
155 _replicator->sendLoginAccount(username, password);
156 }
157 }
158 }
159}
160
162 if (!_initialized) {
163 LOG_ERROR("Cannot run, not initialized!");
164 return;
165 }
166
167 LOG_INFO("Starting main loop...");
168 LOG_INFO("Architecture:");
169 LOG_INFO(" - THREAD 1 (Network): Replicator receiving packets");
170 LOG_INFO(" - THREAD 2 (Main): Game logic + Rendering");
171
172 _rendering->Initialize(800, 600, "R-Type Client");
173
174 // Load sprite sheets
175 LOG_INFO("Loading sprite sheets...");
176 if (!_rendering->LoadTexture("PlayerShips.gif", "assets/sprites/PlayerShips.gif")) {
177 LOG_WARNING("Failed to load PlayerShips.gif");
178 } else {
179 LOG_INFO("✓ Loaded PlayerShips.gif (player ship)");
180 }
181 if (!_rendering->LoadTexture("Projectiles", "assets/sprites/Projectiles.gif")) {
182 LOG_WARNING("Failed to load Projectiles.gif");
183 } else {
184 LOG_INFO("✓ Loaded Projectiles.gif (projectiles)");
185 }
186 if (!_rendering->LoadTexture("Wall.png", "assets/sprites/Wall.png")) {
187 LOG_WARNING("Failed to load Wall.png");
188 } else {
189 LOG_INFO("✓ Loaded Wall.png (obstacles)");
190 }
191 if (!_rendering->LoadTexture("OrbitalModule", "assets/sprites/Module.gif")) {
192 LOG_WARNING("Failed to load Module.gif");
193 } else {
194 LOG_INFO("✓ Loaded Module.gif (orbital modules)");
195 }
196
197 // Apply stored entity ID if GameStart was received before run()
198 if (_myEntityId.has_value()) {
199 LOG_INFO("Applying stored local player entity ID: ", _myEntityId.value());
200 _rendering->SetMyEntityId(_myEntityId.value());
201 }
202
203 _running = true;
204
205 // Setup chat message callback NOW that Rendering is fully initialized
206 LOG_INFO("[GameLoop] Setting up chat message callback...");
207 if (_rendering) {
208 _rendering->SetOnChatMessageSent([this](const std::string &message) {
209 LOG_INFO("[GameLoop] Chat callback triggered with message: '", message, "'");
210 if (_replicator) {
211 LOG_INFO("[GameLoop] Calling replicator->sendChatMessage()");
212 bool sent = _replicator->sendChatMessage(message);
213 LOG_INFO("[GameLoop] Message send result: ", (sent ? "SUCCESS" : "FAILED"));
214 } else {
215 LOG_ERROR("[GameLoop] Replicator is NULL!");
216 }
217 });
218 LOG_INFO("[GameLoop] ✓ Chat message callback configured");
219 } else {
220 LOG_ERROR("[GameLoop] Rendering is NULL!");
221 }
222
223 while (_running) {
224 // Get player ID from replicator (once authenticated)
225 if (_myPlayerId == 0 && _replicator) {
227 }
228
229 // Calculate delta time
230 float deltaTime = calculateDeltaTime();
231 _accumulator += deltaTime;
232
233 // 1. Process network messages from network thread
234 if (_replicator) {
235 _replicator->processMessages(); // ← Reads from network thread queue
236 }
237
238 // 2. Fixed timestep updates (physics, ECS, input sending)
239 while (_accumulator >= _fixedTimestep) {
240 // Process and send inputs at fixed rate (60 Hz)
241 processInput();
242
246 }
247
248 // 3. Variable timestep update (interpolation, etc.)
249 update(deltaTime);
250
251 // 4. Render
252 render();
253 }
254
255 LOG_INFO("Main loop stopped.");
256}
257
259 if (!_initialized) {
260 return;
261 }
262
263 LOG_INFO("Shutting down subsystems...");
264
265 _rendering.reset();
266 LOG_INFO("Rendering stopped");
267
268 _inputBuffer.reset();
269 LOG_INFO("InputBuffer stopped");
270
271 // Note: EventBus and Replicator are owned by Client, don't delete them
272 LOG_INFO("GameLoop subsystems stopped");
273
274 _initialized = false;
275 _running = false;
276 LOG_INFO("Shutdown complete.");
277}
278
280 LOG_INFO("Stop requested...");
281 _running = false;
282}
283
285 if (_rendering) {
286 _rendering->SetReconciliationThreshold(threshold);
287 LOG_INFO("Reconciliation threshold set to: ", threshold, " pixels");
288 }
289}
290
292 if (_rendering) {
293 return _rendering->GetReconciliationThreshold();
294 }
295 return 5.0f; // Default fallback value
296}
297
298void GameLoop::update(float deltaTime) {
299 // Variable timestep logic
300 // - Interpolation between fixed steps
301 // - Camera updates
302 // - Animations
303
304 // Update entity interpolation for smooth rendering
305 // Only update if rendering system is fully initialized
306 if (_rendering && _rendering->IsWindowOpen()) {
307 _rendering->UpdateInterpolation(deltaTime);
308
309 // Update ping display and adaptive reconciliation
310 if (_replicator != nullptr) {
311 const uint32_t currentPing = _replicator->getLatency();
312 _rendering->SetPing(currentPing);
313
314 // ADAPTIVE RECONCILIATION THRESHOLD
315
316 // Base threshold: 5.0px (was 3.0px) - Gives more breathing room for local prediction
317
318 // Penalty: +0.25px per ms of ping (was 0.2px)
319
320 // Max: 30.0px (was 20.0px) - Allow significant drift at high ping (100ms+)
321
322 float adaptiveThreshold = 5.0f + (static_cast<float>(currentPing) * _playerSpeed * 0.0025f);
323
324 if (adaptiveThreshold > 30.0f)
325 adaptiveThreshold = 30.0f;
326
327 _rendering->SetReconciliationThreshold(adaptiveThreshold);
328 }
329
330 // Update ping display timer (throttles updates to 1 second)
331 _rendering->UpdatePingTimer(deltaTime);
332 }
333
334 // Note: Don't check WindowShouldClose here - it's checked in render()
335 // The window state is managed properly in the rendering system
336}
337
338void GameLoop::fixedUpdate(float fixedDeltaTime) {
339 // TODO: Fixed timestep logic (deterministic)
340 // - Physics simulation
341 // - ECS systems update
342 // - Collision detection
343 // - Game state prediction
344 (void)fixedDeltaTime;
345}
346
348 if (!_rendering) {
349 return;
350 }
351
352 // Rendering::Render() already clears the window.
353 _rendering->Render();
354
355 // Check if shutdown was requested during render (e.g. Quit button)
356 if (!_rendering->IsWindowOpen()) {
357 stop();
358 shutdown();
359 }
360}
361
363 if (!_rendering || !_replicator) {
364 return;
365 }
366
367 // Get key bindings
368 auto &bindings = Input::KeyBindings::getInstance();
369
370 // Collect all currently pressed actions
371 std::vector<RType::Messages::Shared::Action> actions;
372
373 // Calculate movement delta (distance per frame at fixed 60Hz, scaled by game speed)
375
376 // Collect input directions first
377 int dx = 0, dy = 0;
378
379 // Helper lambda to check if a binding (keyboard or gamepad) is pressed
380 auto isBindingDown = [this](int binding) {
381 if (binding == KEY_NULL) {
382 return false;
383 }
384 if (Input::IsGamepadBinding(binding)) {
385 int button = Input::BindingToGamepadButton(binding);
386 // Check all connected gamepads (typically just gamepad 0)
387 for (int gp = 0; gp < 4; ++gp) {
388 if (_rendering->IsGamepadAvailable(gp) && _rendering->IsGamepadButtonDown(gp, button)) {
389 return true;
390 }
391 }
392 return false;
393 }
394 return _rendering->IsKeyDown(binding);
395 };
396
397 // Helper lambda to check if an action's key or gamepad button is pressed
398 auto isActionDown = [&bindings, &isBindingDown](Input::GameAction action) {
399 int primary = bindings.GetPrimaryKey(action);
400 int secondary = bindings.GetSecondaryKey(action);
401 return isBindingDown(primary) || isBindingDown(secondary);
402 };
403
404 // Movement using configurable bindings
405 if (isActionDown(Input::GameAction::MOVE_UP)) {
407 dy = -1;
408 }
409 if (isActionDown(Input::GameAction::MOVE_DOWN)) {
411 dy = 1;
412 }
413 if (isActionDown(Input::GameAction::MOVE_LEFT)) {
415 dx = -1;
416 }
417 if (isActionDown(Input::GameAction::MOVE_RIGHT)) {
419 dx = 1;
420 }
421 if (isActionDown(Input::GameAction::SHOOT)) {
423 }
424
425 // Update movement state based on input
426 _isMoving = (dx != 0 || dy != 0);
427
428 // Inform rendering system about movement state (for reconciliation logic)
429 if (_rendering) {
430 _rendering->SetLocalPlayerMoving(_isMoving);
431 }
432
433 // CLIENT-SIDE PREDICTION: Apply movement with diagonal normalization (MUST MATCH SERVER!)
434 // Only predict if entity is initialized (prevents moving before spawn)
436 float moveX = static_cast<float>(dx);
437 float moveY = static_cast<float>(dy);
438
439 // Normalize diagonal movement to prevent going faster diagonally
440 if (dx != 0 && dy != 0) {
441 float length = std::sqrt(moveX * moveX + moveY * moveY); // √2 ≈ 1.414
442 moveX /= length; // 1/√2 ≈ 0.707
443 moveY /= length; // 1/√2 ≈ 0.707
444 }
445
446 // Apply normalized movement
447 _rendering->MoveEntityLocally(_myEntityId.value(), moveX * moveDelta, moveY * moveDelta);
448 }
449
450 // Don't send inputs if in spectator mode (avoid unnecessary network traffic)
451 if (_replicator->isSpectator()) {
452 return;
453 }
454
455 // Create current snapshot
457 currentSnapshot.sequenceId = _inputSequenceId++;
458 currentSnapshot.actions = actions;
459
460 // Add to history
461 _inputHistory.push_front(currentSnapshot);
462 if (_inputHistory.size() > INPUT_HISTORY_SIZE) {
463 _inputHistory.pop_back();
464 }
465
466 // Create packet with full history (redundancy)
467 // Convert deque to vector for the message constructor
468 std::vector<RType::Messages::C2S::PlayerInput::InputSnapshot> historyVector(_inputHistory.begin(),
469 _inputHistory.end());
470
471 RType::Messages::C2S::PlayerInput inputPacket(historyVector);
472
473 // Serialize and wrap in network message
474 std::vector<uint8_t> payload = inputPacket.serialize();
475 std::vector<uint8_t> packet =
477
478 // Send to server (packet already contains type, so pass empty type)
479 _replicator->sendPacket(static_cast<NetworkMessageType>(0), packet);
480}
481
483 static auto lastTime = std::chrono::high_resolution_clock::now();
484 auto currentTime = std::chrono::high_resolution_clock::now();
485
486 std::chrono::duration<float> delta = currentTime - lastTime;
487 lastTime = currentTime;
488
489 return delta.count();
490}
491
493 auto messageType = NetworkMessages::getMessageType(event.getData());
494 auto payload = NetworkMessages::getPayload(event.getData());
495
496 switch (messageType) {
498 handleGameStart(payload);
499 break;
501 handleGameState(payload);
502 break;
504 handleGameruleUpdate(payload);
505 break;
507 handleRoomList(payload);
508 break;
510 handleRoomState(payload);
511 break;
513 handleEntityDestroyed(payload);
514 break;
516 handleChatMessage(payload);
517 break;
519 handleLeftRoom(payload);
520 break;
522 handleGameOver(payload);
523 break;
524 default:
525 break;
526 }
527}
528
529void GameLoop::handleGameStart(const std::vector<uint8_t> &payload) {
530 LOG_INFO("GameStart message received");
531
532 // Hide waiting room and start game
533 if (_rendering) {
534 _rendering->StartGame();
535 }
536
537 try {
538 auto gameStart = RType::Messages::S2C::GameStart::deserialize(payload);
539 LOG_INFO("GameStart received: yourEntityId=", gameStart.yourEntityId);
540
541 // Set up parallax background using map config from server
542 if (_rendering) {
543 const auto &mapConfig = gameStart.mapConfig;
544 LOG_INFO("Map config: bg='", mapConfig.background, "', parallax='", mapConfig.parallaxBackground,
545 "', speed=", mapConfig.scrollSpeed, ", parallaxFactor=", mapConfig.parallaxSpeedFactor);
546 _rendering->SetBackground(mapConfig.background, mapConfig.parallaxBackground,
547 mapConfig.scrollSpeed, mapConfig.parallaxSpeedFactor);
548 }
549
550 for (const auto &entity : gameStart.initialState.entities) {
551 if (entity.entityId == gameStart.yourEntityId) {
552 _myEntityId = entity.entityId;
553 _entityInitialized = true;
554 LOG_INFO("✓ Stored local player entity ID: ", entity.entityId);
555
556 // Set the entity ID in the rendering system
557 if (_rendering) {
558 _rendering->SetMyEntityId(entity.entityId);
559 LOG_INFO("✓ SetMyEntityId called with ID: ", entity.entityId);
560 }
561 }
562
563 if (_rendering) {
564 _rendering->UpdateEntity(entity.entityId, entity.type, entity.position.x, entity.position.y,
565 entity.health.value_or(-1), entity.currentAnimation, entity.spriteX,
566 entity.spriteY, entity.spriteW, entity.spriteH);
567 }
568 }
569 LOG_INFO("Loaded ", gameStart.initialState.entities.size(), " entities from GameStart");
570 } catch (const std::exception &e) {
571 LOG_ERROR("Failed to parse GamerulePacket: ", e.what());
572 }
573}
574
575void GameLoop::handleRoomList(const std::vector<uint8_t> &payload) {
576 try {
577 auto roomList = RType::Messages::S2C::RoomList::deserialize(payload);
578
579 LOG_INFO("✓ RoomList received with ", roomList.rooms.size(), " rooms");
580
581 // Convert to RoomData format for Rendering
582 std::vector<RoomData> rooms;
583 for (const auto &room : roomList.rooms) {
584 RoomData roomData;
585 roomData.roomId = room.roomId;
586 roomData.roomName = room.roomName;
587 roomData.playerCount = room.playerCount;
588 roomData.maxPlayers = room.maxPlayers;
589 roomData.isPrivate = room.isPrivate;
590 roomData.state = room.state;
591 rooms.push_back(roomData);
592
593 LOG_INFO(" - Room: ", room.roomName, " [", room.playerCount, "/", room.maxPlayers, "]");
594 }
595
596 // Update rendering with room list
597 if (_rendering) {
598 _rendering->UpdateRoomList(rooms);
599 }
600
601 } catch (const std::exception &e) {
602 LOG_ERROR("Failed to parse RoomList: ", e.what());
603 }
604}
605
606void GameLoop::handleRoomState(const std::vector<uint8_t> &payload) {
607 try {
608 auto roomState = RType::Messages::S2C::RoomState::deserialize(payload);
609
610 LOG_INFO("✓ RoomState received: ", roomState.roomName, " with ", roomState.players.size(),
611 " players");
612
613 // Convert to PlayerInfo format for Rendering
614 std::vector<Game::PlayerInfo> players;
615 bool isHost = false;
616 bool isSpectator = false; // Check if we are a spectator
617
618 // Determine if we are the host by checking if our playerId matches a player with isHost=true
619 for (const auto &playerData : roomState.players) {
620 Game::PlayerInfo playerInfo(playerData.playerId, playerData.playerName, playerData.isHost,
621 playerData.isSpectator);
622 players.push_back(playerInfo);
623
624 LOG_INFO(" - Player: '", playerData.playerName, "' (ID:", playerData.playerId,
625 ") | isHost=", playerData.isHost, " | isSpectator=", playerData.isSpectator);
626
627 // Check if this is us
628 if (playerData.playerId == _myPlayerId) {
629 isHost = playerData.isHost;
630 isSpectator = playerData.isSpectator;
631
632 if (playerData.isHost) {
633 LOG_INFO(" -> MATCH! This is ME and I'm the HOST");
634 } else if (playerData.isSpectator) {
635 LOG_INFO(" -> MATCH! This is ME and I'm a SPECTATOR");
636 } else {
637 LOG_INFO(" -> This is ME (regular player)");
638 }
639 }
640 }
641
642 LOG_INFO(" Final isHost value: ", isHost, ", isSpectator: ", isSpectator);
643
644 // Update waiting room with player list
645 if (_rendering) {
646 _rendering->UpdateWaitingRoom(players, roomState.roomName, isHost, isSpectator);
647 }
648
649 } catch (const std::exception &e) {
650 LOG_ERROR("Failed to parse RoomState: ", e.what());
651 }
652}
653
654void GameLoop::handleEntityDestroyed(const std::vector<uint8_t> &payload) {
655 try {
656 auto entityDestroyed = RType::Messages::S2C::EntityDestroyed::deserialize(payload);
657
658 LOG_INFO("✓ EntityDestroyed received: entityId=", entityDestroyed.entityId,
659 " reason=", static_cast<int>(entityDestroyed.reason));
660
661 // Remove the entity from rendering immediately
662 if (_rendering) {
663 _rendering->RemoveEntity(entityDestroyed.entityId);
664 }
665
666 // If this was our entity, handle game over scenario
667 if (_myEntityId.has_value() && entityDestroyed.entityId == _myEntityId.value()) {
668 LOG_WARNING("Our entity was destroyed!");
669 _myEntityId = std::nullopt;
670 _entityInitialized = false;
671 }
672
673 } catch (const std::exception &e) {
674 LOG_ERROR("Failed to parse EntityDestroyed: ", e.what());
675 }
676}
677
678void GameLoop::handleGameState(const std::vector<uint8_t> &payload) {
679 try {
680 auto gameState = RType::Messages::S2C::GameState::deserialize(payload);
681
682 // Track which entities are in this GameState
683 std::unordered_set<uint32_t> currentEntityIds;
684
685 for (const auto &entity : gameState.entities) {
686 currentEntityIds.insert(entity.entityId);
687
688 if (entity.entityId == _myEntityId.value_or(0) && _clientSidePredictionEnabled) {
690 } else {
691 _rendering->UpdateEntity(entity.entityId, entity.type, entity.position.x, entity.position.y,
692 entity.health.value_or(-1), entity.currentAnimation, entity.spriteX,
693 entity.spriteY, entity.spriteW, entity.spriteH);
694 }
695 }
696
697 // Remove entities that no longer exist in the GameState
698 // (e.g., collectibles that were picked up)
699 if (_rendering) {
700 std::vector<uint32_t> entitiesToRemove;
701
702 // Check which previously seen entities are now missing
703 for (uint32_t id : _knownEntityIds) {
704 if (currentEntityIds.find(id) == currentEntityIds.end()) {
705 entitiesToRemove.push_back(id);
706 }
707 }
708
709 // Remove obsolete entities
710 for (uint32_t id : entitiesToRemove) {
711 _rendering->RemoveEntity(id);
712 LOG_DEBUG("[CLEANUP] Removed entity ", id, " (no longer in GameState)");
713 }
714
715 // Update our known entity list
716 _knownEntityIds = currentEntityIds;
717 }
718 } catch (const std::exception &e) {
719 LOG_ERROR("Failed to parse GameState: ", e.what());
720 }
721}
722
724 // 1. Prune history: Remove inputs already processed by server
725 while (!_inputHistory.empty() && _inputHistory.back().sequenceId <= entity.lastProcessedInput) {
726 _inputHistory.pop_back();
727 }
728
729 // 2. Re-simulate: Start from Server Position
730 float predictedX = entity.position.x;
731 float predictedY = entity.position.y;
732
733 simulateInputHistory(predictedX, predictedY);
734
735 if (_rendering) {
736 _rendering->UpdateEntity(entity.entityId, entity.type, predictedX, predictedY,
737 entity.health.value_or(-1), entity.currentAnimation, entity.spriteX,
738 entity.spriteY, entity.spriteW, entity.spriteH);
739 }
740}
741
742void GameLoop::simulateInputHistory(float &x, float &y) {
743 for (auto it = _inputHistory.rbegin(); it != _inputHistory.rend(); ++it) {
744 const auto &snapshot = *it;
745 int dx = 0, dy = 0;
746
747 for (auto act : snapshot.actions) {
749 dy = -1;
751 dy = 1;
753 dx = -1;
755 dx = 1;
756 }
757
758 if (dx != 0 || dy != 0) {
759 float moveX = static_cast<float>(dx);
760 float moveY = static_cast<float>(dy);
761
762 // Normalize diagonal
763 if (dx != 0 && dy != 0) {
764 float length = std::sqrt(moveX * moveX + moveY * moveY);
765 moveX /= length;
766 moveY /= length;
767 }
768
769 // Scale by game speed multiplier to match server's slowed game time
770 float frameDelta = _playerSpeed * (1.0f / 60.0f) * _gameSpeedMultiplier;
771 x += moveX * frameDelta;
772 y += moveY * frameDelta;
773 }
774 }
775}
776
777void GameLoop::handleGameruleUpdate(const std::vector<uint8_t> &payload) {
778 try {
779 auto gamerulePacket = RType::Messages::S2C::GamerulePacket::deserialize(payload);
780 auto &clientRules = client::ClientGameRules::getInstance();
781 clientRules.updateMultiple(gamerulePacket.getGamerules());
782
783 LOG_INFO("✓ Gamerule update received: ", gamerulePacket.size(), " rules updated");
784
785 // Apply player speed from gamerules
786 float speed = clientRules.get(GameruleKey::PLAYER_SPEED, _playerSpeed);
787 if (speed != _playerSpeed) {
788 _playerSpeed = speed;
789 LOG_INFO(" - Player speed updated to: ", _playerSpeed);
790 }
791
792 // Apply game speed multiplier from gamerules
793 float gameSpeed = clientRules.get(GameruleKey::GAME_SPEED_MULTIPLIER, _gameSpeedMultiplier);
794 if (gameSpeed != _gameSpeedMultiplier) {
795 _gameSpeedMultiplier = gameSpeed;
796 // Client keeps 60Hz loop rate but scales deltaTime for prediction
797 // to match server's slowed game time
798 LOG_INFO(" - Game speed multiplier updated to: ", _gameSpeedMultiplier,
799 " (prediction will use scaled time)");
800 }
801 } catch (const std::exception &e) {
802 LOG_ERROR("Failed to parse GamerulePacket: ", e.what());
803 }
804}
805
806void GameLoop::handleChatMessage(const std::vector<uint8_t> &payload) {
807 try {
809
810 LOG_INFO("✓ ChatMessage from ", chatMsg.playerName, ": ", chatMsg.message);
811
812 // Forward to rendering for display
813 if (_rendering) {
814 _rendering->AddChatMessage(chatMsg.playerId, chatMsg.playerName, chatMsg.message,
815 chatMsg.timestamp);
816 }
817 } catch (const std::exception &e) {
818 LOG_ERROR("Failed to parse ChatMessage: ", e.what());
819 }
820}
821
822void GameLoop::handleLeftRoom(const std::vector<uint8_t> &payload) {
823 using namespace RType::Messages;
824
825 try {
826 auto leftRoomMsg = S2C::LeftRoom::deserialize(payload);
827
828 LOG_INFO("✓ LeftRoom received - playerId: ", leftRoomMsg.playerId,
829 ", reason: ", static_cast<int>(leftRoomMsg.reason), ", message: ", leftRoomMsg.message);
830
831 // If the player who left is ME, return to room list
832 // Note: We need to compare with local player ID which we should track
833 // For now, we'll assume if we receive this message, it's for us
834 // (Server should only send it to the player who left)
835
836 if (leftRoomMsg.reason == S2C::LeftRoomReason::KICKED && _myPlayerId == leftRoomMsg.playerId) {
837 LOG_INFO("You were kicked from the room: ", leftRoomMsg.message);
838
839 // Show the message to the user via chat if available
840 if (_rendering) {
841 _rendering->AddChatMessage(0, "SYSTEM", leftRoomMsg.message, 0);
842 }
843 }
844
845 if (_myPlayerId == leftRoomMsg.playerId) {
846 LOG_INFO("✓ You have left the room, returning to room list");
848 }
849
850 } catch (const std::exception &e) {
851 LOG_ERROR("Failed to parse LeftRoom: ", e.what());
852 }
853}
854
855void GameLoop::handleGameOver(const std::vector<uint8_t> &payload) {
856 try {
857 auto gameOver = RType::Messages::S2C::GameOver::deserialize(payload);
858 LOG_INFO("Game Over - ", gameOver.reason);
859
860 if (_rendering) {
861 _rendering->ShowGameOver(gameOver.reason);
862 }
863 } catch (const std::exception &e) {
864 LOG_ERROR("Failed to parse GameOver: ", e.what());
865 }
866}
@ GAME_SPEED_MULTIPLIER
Game speed multiplier (0.25 to 1.0, accessibility feature)
#define LOG_INFO(...)
Definition Logger.hpp:181
#define LOG_DEBUG(...)
Definition Logger.hpp:180
#define LOG_ERROR(...)
Definition Logger.hpp:183
#define LOG_WARNING(...)
Definition Logger.hpp:182
@ UPDATE_AUTO_MATCHMAKING_PREF
Type-safe event publication/subscription system.
Definition EventBus.hpp:44
void publish(const T &event)
Publish an event to all subscribers.
Definition EventBus.hpp:116
size_t subscribe(EventCallback< T > callback)
Subscribe to a specific event type.
Definition EventBus.hpp:108
std::unique_ptr< Rendering > _rendering
Definition GameLoop.hpp:250
EventBus * _eventBus
Definition GameLoop.hpp:247
float _playerSpeed
Definition GameLoop.hpp:274
void processInput()
Process player inputs.
Definition GameLoop.cpp:362
static constexpr size_t INPUT_HISTORY_SIZE
Definition GameLoop.hpp:263
void handleEntityDestroyed(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:654
uint32_t _currentFrame
Definition GameLoop.hpp:257
void handleLeftRoom(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:822
void handleNetworkMessage(const NetworkEvent &event)
Handle incoming network messages.
Definition GameLoop.cpp:492
void handleGameruleUpdate(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:777
bool _clientSidePredictionEnabled
Definition GameLoop.hpp:275
GameLoop(EventBus &eventBus, Replicator &replicator, const std::string &playerName)
Constructor with shared EventBus and Replicator.
Definition GameLoop.cpp:15
uint32_t _inputSequenceId
Definition GameLoop.hpp:260
std::unique_ptr< InputBuffer > _inputBuffer
Definition GameLoop.hpp:248
float _gameSpeedMultiplier
Definition GameLoop.hpp:276
void update(float deltaTime)
Update game logic (variable timestep)
Definition GameLoop.cpp:298
bool _entityInitialized
Definition GameLoop.hpp:269
void handleGameState(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:678
void handleGameOver(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:855
bool _initialized
Definition GameLoop.hpp:253
float _fixedTimestep
Definition GameLoop.hpp:255
Replicator * _replicator
Definition GameLoop.hpp:249
std::deque< RType::Messages::C2S::PlayerInput::InputSnapshot > _inputHistory
Definition GameLoop.hpp:264
void run()
Start the main game loop.
Definition GameLoop.cpp:161
void setReconciliationThreshold(float threshold)
Set the reconciliation threshold for client-side prediction.
Definition GameLoop.cpp:284
bool _isMoving
Definition GameLoop.hpp:273
void handleRoomState(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:606
float getReconciliationThreshold() const
Get the current reconciliation threshold.
Definition GameLoop.cpp:291
void simulateInputHistory(float &x, float &y)
Definition GameLoop.cpp:742
bool _justCreatedRoom
Definition GameLoop.hpp:270
void handleRoomList(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:575
void processServerReconciliation(const RType::Messages::S2C::EntityState &entity)
Definition GameLoop.cpp:723
bool initialize()
Initialize all game subsystems.
Definition GameLoop.cpp:22
float _accumulator
Definition GameLoop.hpp:256
void shutdown()
Stop and clean up all subsystems.
Definition GameLoop.cpp:258
bool _running
Definition GameLoop.hpp:252
void handleGameStart(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:529
void handleUIEvent(const UIEvent &event)
Definition GameLoop.cpp:51
void render()
Perform rendering of current frame.
Definition GameLoop.cpp:347
std::optional< uint32_t > _myEntityId
Definition GameLoop.hpp:267
std::unordered_set< uint32_t > _knownEntityIds
Definition GameLoop.hpp:279
float calculateDeltaTime()
Calculate time elapsed since last frame.
Definition GameLoop.cpp:482
uint32_t _myPlayerId
Definition GameLoop.hpp:268
~GameLoop()
Destructor.
Definition GameLoop.cpp:18
void stop()
Stop the game loop.
Definition GameLoop.cpp:279
void fixedUpdate(float fixedDeltaTime)
Update physics simulation (fixed timestep)
Definition GameLoop.cpp:338
void handleChatMessage(const std::vector< uint8_t > &payload)
Definition GameLoop.cpp:806
static KeyBindings & getInstance()
Get the singleton instance.
Event representing a network message.
const std::vector< uint8_t > & getData() const
Get the message data.
Player input message sent from client to server (with redundancy)
std::vector< uint8_t > serialize() const
Serialize to byte vector.
static EntityDestroyed deserialize(const std::vector< uint8_t > &data)
State of a single entity.
std::optional< int32_t > health
static GameOver deserialize(const std::vector< uint8_t > &data)
Definition GameOver.hpp:42
static GameStart deserialize(const std::vector< uint8_t > &data)
Definition GameStart.hpp:80
static GameState deserialize(const std::vector< uint8_t > &data)
Definition GameState.hpp:54
static GamerulePacket deserialize(const std::vector< uint8_t > &data)
Deserialize a packet from bytes using Cap'n Proto.
static RoomList deserialize(const std::vector< uint8_t > &data)
Definition RoomList.hpp:53
static RoomState deserialize(const std::vector< uint8_t > &data)
Definition RoomState.hpp:63
static S2CChatMessage deserialize(const std::vector< uint8_t > &data)
Deserialize from byte vector.
Client-server network replication manager with dedicated network thread.
bool sendStartGame()
Send start game request to server.
bool sendLoginAccount(const std::string &username, const std::string &password)
Send login request to server.
bool sendAutoMatchmaking()
Send auto-matchmaking request to server.
void processMessages()
Process incoming network messages.
bool sendRequestRoomList()
Request the list of available rooms from server.
bool isSpectator() const
Check if in spectator mode.
uint32_t getMyPlayerId() const
Get the player ID assigned by server.
void sendPacket(NetworkMessageType type, const std::vector< uint8_t > &data)
Send a packet to the server.
bool sendRegisterAccount(const std::string &username, const std::string &password)
Send register account request to server.
bool sendLeaveRoom()
Send request to leave current room.
bool updateAutoMatchmakingPreference(bool enabled)
Update auto-matchmaking preference on server.
uint32_t getLatency() const
Get current latency in milliseconds.
bool sendCreateRoom(const std::string &roomName, uint32_t maxPlayers, bool isPrivate, float gameSpeedMultiplier=1.0f)
Send create room request to server.
bool sendJoinRoom(const std::string &roomId)
Send join room request to server.
bool sendChatMessage(const std::string &message)
Send chat message to server.
UIEventType getType() const
Definition UIEvent.hpp:55
static ClientGameRules & getInstance()
Get the singleton instance.
NetworkMessageType
Types of network messages exchanged between client and server.
GameAction
Enumeration of all bindable game actions.
int BindingToGamepadButton(int binding)
Extract gamepad button from a binding value.
bool IsGamepadBinding(int binding)
Check if a binding value represents a gamepad button.
MessageType getMessageType(const std::vector< uint8_t > &packet)
Get message type from packet.
std::vector< uint8_t > createMessage(MessageType type, const std::vector< uint8_t > &payload)
Create a message with type and payload.
std::vector< uint8_t > getPayload(const std::vector< uint8_t > &packet)
Get payload from packet (without header)
All game messages for R-Type network protocol.
Definition Server.hpp:30
Player information in waiting room.
uint32_t playerCount
Definition UIEvent.hpp:18
std::string roomName
Definition UIEvent.hpp:17
bool isPrivate
Definition UIEvent.hpp:20
std::string roomId
Definition UIEvent.hpp:16
uint32_t maxPlayers
Definition UIEvent.hpp:19
uint8_t state
Definition UIEvent.hpp:21