10#include <unordered_set>
11#include "../ClientGameRules.hpp"
16 : _eventBus(&eventBus), _replicator(&replicator), _playerName(playerName) {}
27 LOG_INFO(
"Initializing subsystems...");
39 LOG_INFO(
"Subscribed to NetworkEvent");
46 LOG_INFO(
"All subsystems initialized successfully!");
53 LOG_INFO(
"[GameLoop] Joining game requested by UI");
56 std::string roomId =
event.getData();
59 LOG_WARNING(
"[GameLoop] No room ID provided, using default room");
62 LOG_INFO(
"[GameLoop] Joining room: ", roomId);
69 LOG_INFO(
"[GameLoop] Create room requested by UI");
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);
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));
86 LOG_INFO(
"[GameLoop] Creating room: ", roomName,
" (Max: ", maxPlayers,
87 ", Private: ", isPrivate,
", Speed: ",
static_cast<int>(gameSpeedMultiplier * 100),
95 std::this_thread::sleep_for(std::chrono::milliseconds(100));
100 LOG_INFO(
"[GameLoop] Room list requested by UI");
106 LOG_INFO(
"[GameLoop] Auto-matchmaking preference update");
107 const std::string &data =
event.getData();
109 bool enabled = (data ==
"enable");
111 LOG_INFO(
"[GameLoop] Preference updated (will apply when player clicks Play)");
115 LOG_INFO(
"[GameLoop] Auto-matchmaking request (triggering matchmaking now)");
120 LOG_INFO(
"[GameLoop] Host requesting game start");
125 LOG_INFO(
"[GameLoop] Player leaving room");
133 LOG_INFO(
"[GameLoop] Register account requested by UI");
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);
146 LOG_INFO(
"[GameLoop] Login account requested by UI");
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);
163 LOG_ERROR(
"Cannot run, not initialized!");
169 LOG_INFO(
" - THREAD 1 (Network): Replicator receiving packets");
170 LOG_INFO(
" - THREAD 2 (Main): Game logic + Rendering");
172 _rendering->Initialize(800, 600,
"R-Type Client");
175 LOG_INFO(
"Loading sprite sheets...");
176 if (!
_rendering->LoadTexture(
"PlayerShips.gif",
"assets/sprites/PlayerShips.gif")) {
179 LOG_INFO(
"✓ Loaded PlayerShips.gif (player ship)");
181 if (!
_rendering->LoadTexture(
"Projectiles",
"assets/sprites/Projectiles.gif")) {
184 LOG_INFO(
"✓ Loaded Projectiles.gif (projectiles)");
186 if (!
_rendering->LoadTexture(
"Wall.png",
"assets/sprites/Wall.png")) {
189 LOG_INFO(
"✓ Loaded Wall.png (obstacles)");
191 if (!
_rendering->LoadTexture(
"OrbitalModule",
"assets/sprites/Module.gif")) {
194 LOG_INFO(
"✓ Loaded Module.gif (orbital modules)");
206 LOG_INFO(
"[GameLoop] Setting up chat message callback...");
208 _rendering->SetOnChatMessageSent([
this](
const std::string &message) {
209 LOG_INFO(
"[GameLoop] Chat callback triggered with message: '", message,
"'");
211 LOG_INFO(
"[GameLoop] Calling replicator->sendChatMessage()");
213 LOG_INFO(
"[GameLoop] Message send result: ", (sent ?
"SUCCESS" :
"FAILED"));
215 LOG_ERROR(
"[GameLoop] Replicator is NULL!");
218 LOG_INFO(
"[GameLoop] ✓ Chat message callback configured");
220 LOG_ERROR(
"[GameLoop] Rendering is NULL!");
263 LOG_INFO(
"Shutting down subsystems...");
272 LOG_INFO(
"GameLoop subsystems stopped");
286 _rendering->SetReconciliationThreshold(threshold);
287 LOG_INFO(
"Reconciliation threshold set to: ", threshold,
" pixels");
293 return _rendering->GetReconciliationThreshold();
322 float adaptiveThreshold = 5.0f + (
static_cast<float>(currentPing) *
_playerSpeed * 0.0025f);
324 if (adaptiveThreshold > 30.0f)
325 adaptiveThreshold = 30.0f;
327 _rendering->SetReconciliationThreshold(adaptiveThreshold);
344 (void)fixedDeltaTime;
371 std::vector<RType::Messages::Shared::Action> actions;
380 auto isBindingDown = [
this](
int binding) {
381 if (binding == KEY_NULL) {
387 for (
int gp = 0; gp < 4; ++gp) {
399 int primary = bindings.GetPrimaryKey(action);
400 int secondary = bindings.GetSecondaryKey(action);
401 return isBindingDown(primary) || isBindingDown(secondary);
436 float moveX =
static_cast<float>(dx);
437 float moveY =
static_cast<float>(dy);
440 if (dx != 0 && dy != 0) {
441 float length = std::sqrt(moveX * moveX + moveY * moveY);
458 currentSnapshot.
actions = actions;
468 std::vector<RType::Messages::C2S::PlayerInput::InputSnapshot> historyVector(
_inputHistory.begin(),
474 std::vector<uint8_t> payload = inputPacket.
serialize();
475 std::vector<uint8_t> packet =
483 static auto lastTime = std::chrono::high_resolution_clock::now();
484 auto currentTime = std::chrono::high_resolution_clock::now();
486 std::chrono::duration<float> delta = currentTime - lastTime;
487 lastTime = currentTime;
489 return delta.count();
496 switch (messageType) {
530 LOG_INFO(
"GameStart message received");
539 LOG_INFO(
"GameStart received: yourEntityId=", gameStart.yourEntityId);
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);
550 for (
const auto &entity : gameStart.initialState.entities) {
551 if (entity.entityId == gameStart.yourEntityId) {
554 LOG_INFO(
"✓ Stored local player entity ID: ", entity.entityId);
559 LOG_INFO(
"✓ SetMyEntityId called with ID: ", entity.entityId);
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);
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());
579 LOG_INFO(
"✓ RoomList received with ", roomList.rooms.size(),
" rooms");
582 std::vector<RoomData> rooms;
583 for (
const auto &room : roomList.rooms) {
585 roomData.
roomId = room.roomId;
590 roomData.
state = room.state;
591 rooms.push_back(roomData);
593 LOG_INFO(
" - Room: ", room.roomName,
" [", room.playerCount,
"/", room.maxPlayers,
"]");
601 }
catch (
const std::exception &e) {
602 LOG_ERROR(
"Failed to parse RoomList: ", e.what());
610 LOG_INFO(
"✓ RoomState received: ", roomState.roomName,
" with ", roomState.players.size(),
614 std::vector<Game::PlayerInfo> players;
616 bool isSpectator =
false;
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);
624 LOG_INFO(
" - Player: '", playerData.playerName,
"' (ID:", playerData.playerId,
625 ") | isHost=", playerData.isHost,
" | isSpectator=", playerData.isSpectator);
629 isHost = playerData.isHost;
630 isSpectator = playerData.isSpectator;
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");
637 LOG_INFO(
" -> This is ME (regular player)");
642 LOG_INFO(
" Final isHost value: ", isHost,
", isSpectator: ", isSpectator);
646 _rendering->UpdateWaitingRoom(players, roomState.roomName, isHost, isSpectator);
649 }
catch (
const std::exception &e) {
650 LOG_ERROR(
"Failed to parse RoomState: ", e.what());
658 LOG_INFO(
"✓ EntityDestroyed received: entityId=", entityDestroyed.entityId,
659 " reason=",
static_cast<int>(entityDestroyed.reason));
663 _rendering->RemoveEntity(entityDestroyed.entityId);
673 }
catch (
const std::exception &e) {
674 LOG_ERROR(
"Failed to parse EntityDestroyed: ", e.what());
683 std::unordered_set<uint32_t> currentEntityIds;
685 for (
const auto &entity : gameState.entities) {
686 currentEntityIds.insert(entity.entityId);
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);
700 std::vector<uint32_t> entitiesToRemove;
704 if (currentEntityIds.find(
id) == currentEntityIds.end()) {
705 entitiesToRemove.push_back(
id);
710 for (uint32_t
id : entitiesToRemove) {
712 LOG_DEBUG(
"[CLEANUP] Removed entity ",
id,
" (no longer in GameState)");
718 }
catch (
const std::exception &e) {
719 LOG_ERROR(
"Failed to parse GameState: ", e.what());
744 const auto &snapshot = *it;
747 for (
auto act : snapshot.actions) {
758 if (dx != 0 || dy != 0) {
759 float moveX =
static_cast<float>(dx);
760 float moveY =
static_cast<float>(dy);
763 if (dx != 0 && dy != 0) {
764 float length = std::sqrt(moveX * moveX + moveY * moveY);
771 x += moveX * frameDelta;
772 y += moveY * frameDelta;
781 clientRules.updateMultiple(gamerulePacket.getGamerules());
783 LOG_INFO(
"✓ Gamerule update received: ", gamerulePacket.size(),
" rules updated");
799 " (prediction will use scaled time)");
801 }
catch (
const std::exception &e) {
802 LOG_ERROR(
"Failed to parse GamerulePacket: ", e.what());
810 LOG_INFO(
"✓ ChatMessage from ", chatMsg.playerName,
": ", chatMsg.message);
814 _rendering->AddChatMessage(chatMsg.playerId, chatMsg.playerName, chatMsg.message,
817 }
catch (
const std::exception &e) {
818 LOG_ERROR(
"Failed to parse ChatMessage: ", e.what());
826 auto leftRoomMsg = S2C::LeftRoom::deserialize(payload);
828 LOG_INFO(
"✓ LeftRoom received - playerId: ", leftRoomMsg.playerId,
829 ", reason: ",
static_cast<int>(leftRoomMsg.reason),
", message: ", leftRoomMsg.message);
836 if (leftRoomMsg.reason == S2C::LeftRoomReason::KICKED &&
_myPlayerId == leftRoomMsg.playerId) {
837 LOG_INFO(
"You were kicked from the room: ", leftRoomMsg.message);
841 _rendering->AddChatMessage(0,
"SYSTEM", leftRoomMsg.message, 0);
846 LOG_INFO(
"✓ You have left the room, returning to room list");
850 }
catch (
const std::exception &e) {
851 LOG_ERROR(
"Failed to parse LeftRoom: ", e.what());
858 LOG_INFO(
"Game Over - ", gameOver.reason);
863 }
catch (
const std::exception &e) {
864 LOG_ERROR(
"Failed to parse GameOver: ", e.what());
@ GAME_SPEED_MULTIPLIER
Game speed multiplier (0.25 to 1.0, accessibility feature)
@ UPDATE_AUTO_MATCHMAKING_PREF
Type-safe event publication/subscription system.
void publish(const T &event)
Publish an event to all subscribers.
size_t subscribe(EventCallback< T > callback)
Subscribe to a specific event type.
std::unique_ptr< Rendering > _rendering
void processInput()
Process player inputs.
static constexpr size_t INPUT_HISTORY_SIZE
void handleEntityDestroyed(const std::vector< uint8_t > &payload)
void handleLeftRoom(const std::vector< uint8_t > &payload)
void handleNetworkMessage(const NetworkEvent &event)
Handle incoming network messages.
void handleGameruleUpdate(const std::vector< uint8_t > &payload)
bool _clientSidePredictionEnabled
GameLoop(EventBus &eventBus, Replicator &replicator, const std::string &playerName)
Constructor with shared EventBus and Replicator.
uint32_t _inputSequenceId
std::unique_ptr< InputBuffer > _inputBuffer
float _gameSpeedMultiplier
void update(float deltaTime)
Update game logic (variable timestep)
void handleGameState(const std::vector< uint8_t > &payload)
void handleGameOver(const std::vector< uint8_t > &payload)
std::deque< RType::Messages::C2S::PlayerInput::InputSnapshot > _inputHistory
void run()
Start the main game loop.
void setReconciliationThreshold(float threshold)
Set the reconciliation threshold for client-side prediction.
void handleRoomState(const std::vector< uint8_t > &payload)
float getReconciliationThreshold() const
Get the current reconciliation threshold.
void simulateInputHistory(float &x, float &y)
void handleRoomList(const std::vector< uint8_t > &payload)
void processServerReconciliation(const RType::Messages::S2C::EntityState &entity)
bool initialize()
Initialize all game subsystems.
void shutdown()
Stop and clean up all subsystems.
void handleGameStart(const std::vector< uint8_t > &payload)
void handleUIEvent(const UIEvent &event)
void render()
Perform rendering of current frame.
std::optional< uint32_t > _myEntityId
std::unordered_set< uint32_t > _knownEntityIds
float calculateDeltaTime()
Calculate time elapsed since last frame.
void stop()
Stop the game loop.
void fixedUpdate(float fixedDeltaTime)
Update physics simulation (fixed timestep)
void handleChatMessage(const std::vector< uint8_t > &payload)
Event representing a network message.
const std::vector< uint8_t > & getData() const
Get the message data.
static EntityDestroyed deserialize(const std::vector< uint8_t > &data)
State of a single entity.
uint32_t lastProcessedInput
std::string currentAnimation
std::optional< int32_t > health
static GameOver deserialize(const std::vector< uint8_t > &data)
static GameStart deserialize(const std::vector< uint8_t > &data)
static GameState deserialize(const std::vector< uint8_t > &data)
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)
static RoomState deserialize(const std::vector< uint8_t > &data)
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
static ClientGameRules & getInstance()
Get the singleton instance.
NetworkMessageType
Types of network messages exchanged between client and server.
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.
Player information in waiting room.