Air-Trap 1.0.0
A multiplayer R-Type clone game engine built with C++23 and ECS architecture
Loading...
Searching...
No Matches
NetworkSyncSystem.cpp
Go to the documentation of this file.
1
8#include "Systems/NetworkSyncSystem.hpp"
9#include "RType/Logger.hpp"
23#include "Utils/DebugFlags.hpp"
24
25#include <chrono>
26#include <cmath>
27
28namespace rtp::client {
30 // Public API
32
34 : _network(network), _registry(registry), _builder(builder) {}
35
37 {
40 if (_ammoReloadRemaining < 0.0f)
42 }
43 _pingTimer += dt;
45 _pingTimer = 0.0f;
46 const auto now = std::chrono::steady_clock::now();
47 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
48 net::PingPayload payload{ static_cast<uint64_t>(ms) };
50 packet << payload;
52 }
53 while (auto eventOpt = _network.pollEvent()) {
54 handleEvent(*eventOpt);
55 }
56 }
57
58 void NetworkSyncSystem::tryLogin(const std::string& username, const std::string& password, uint8_t weaponKind) const
59 {
60 net::LoginPayload payload;
61 std::strncpy(payload.username, username.c_str(), sizeof(payload.username) - 1);
62 std::strncpy(payload.password, password.c_str(), sizeof(payload.password) - 1);
63 payload.weaponKind = weaponKind;
64
66 packet << payload;
67
69 }
70
71 void NetworkSyncSystem::tryRegister(const std::string& username, const std::string& password) const
72 {
74 std::strncpy(payload.username, username.c_str(), sizeof(payload.username) - 1);
75 std::strncpy(payload.password, password.c_str(), sizeof(payload.password) - 1);
76
78 packet << payload;
79
81 }
82
84 {
85 log::info("Requesting list of available rooms from server...");
86 if (_availableRooms.size() > 0)
87 _availableRooms.clear();
90 }
91
92 void NetworkSyncSystem::tryCreateRoom(const std::string& roomName, uint32_t maxPlayers, float difficulty, float speed, uint32_t duration, uint32_t seed, uint32_t levelId, uint8_t roomType) {
93 log::info("Attempting to create room '{}' on server...", roomName);
95 _currentLevelId = levelId;
97 std::strncpy(payload.roomName, roomName.c_str(), sizeof(payload.roomName) - 1);
98 payload.maxPlayers = maxPlayers;
99 payload.difficulty = difficulty;
100 payload.speed = speed;
101 payload.duration = duration;
102 payload.seed = seed;
103 payload.levelId = levelId;
104 payload.roomType = roomType; // Set room type
106 packet << payload;
107
109 }
110
111 void NetworkSyncSystem::tryJoinRoom(uint32_t roomId, bool asSpectator)
112 {
114 // Try to infer the level id from the latest room list
115 for (const auto& r : _availableRooms) {
116 if (r.roomId == roomId) {
117 _currentLevelId = r.levelId;
118 break;
119 }
120 }
122 net::JoinRoomPayload payload{};
123 payload.roomId = roomId;
124 payload.isSpectator = static_cast<uint8_t>(asSpectator ? 1 : 0);
125 packet << payload;
126
128 }
129
137
139 {
141 packet << static_cast<uint8_t>(isReady ? 1 : 0);
142
144 }
145
147 _isStartingSolo = true;
148 tryCreateRoom("Solo Game", 1, 0.5f, 1.0f, 10, 0, 1, static_cast<uint8_t>(net::roomType::Private));
149 }
150
151 void NetworkSyncSystem::trySendMessage(const std::string& message) const
152 {
154 net::RoomChatPayload payload{};
155 std::strncpy(payload.message, message.c_str(), sizeof(payload.message) - 1);
156 payload.message[sizeof(payload.message) - 1] = '\0';
157 packet << payload;
158
160 }
161
162 void NetworkSyncSystem::sendSelectedWeapon(uint8_t weaponKind) const
163 {
164 log::info("Sending selected weapon {} to server", static_cast<int>(weaponKind));
166 packet << weaponKind;
168 }
169
171 // Getters
173
175 {
177 }
178
180 {
181 return _isReady;
182 }
183
185 {
186 return _network.isUdpReady();
187 }
188
190 {
191 return _isLoggedIn;
192 }
193
194 std::string NetworkSyncSystem::getUsername(void) const
195 {
196 return _username;
197 }
198
199 std::list<net::RoomInfo> NetworkSyncSystem::getAvailableRooms(void) const
200 {
201 return _availableRooms;
202 }
203
205 {
207 }
208
213
215 {
216 return _ammoCurrent;
217 }
218
219 uint16_t NetworkSyncSystem::getAmmoMax(void) const
220 {
221 return _ammoMax;
222 }
223
225 {
226 return _ammoReloading;
227 }
228
233
234 uint32_t NetworkSyncSystem::getPingMs(void) const
235 {
236 return _pingMs;
237 }
238
240 {
241 const bool kicked = _kicked;
242 _kicked = false;
243 return kicked;
244 }
245
247 {
248 return _currentLevelId;
249 }
250
252 {
253 return _lastChatMessage;
254 }
255
256 const std::deque<std::string>& NetworkSyncSystem::getChatHistory(void) const
257 {
258 return _chatHistory;
259 }
260
262 {
263 _gameOverSummary = summary;
264 }
265
270
272 {
273 if (!_gameOverPending) {
274 return false;
275 }
276 _gameOverPending = false;
277 return true;
278 }
279
281 {
282 return _score;
283 }
284
286 {
287 return _healthCurrent;
288 }
289
291 {
292 return _healthMax;
293 }
294
296 // Private Methods
298
300 {
301 switch (event.packet.header.opCode)
302 {
303 // case net::OpCode::EntitySpawn: { /* EntitySpawn */
304 // net::EntitySpawnPayload payload;
305 // event.packet >> payload;
306 // spawnEntityFromServer(payload);
307 // break;
308 // }
310 onLoginResponse(event.packet);
311 break;
312 }
315 break;
316 }
318 onRoomResponse(event.packet);
319 break;
320 }
323 break;
324 }
327 break;
328 }
331 break;
332 }
334 log::info("Received StartGame notification from server.");
336 break;
337 }
340 break;
341 }
343 onEntityDeath(event.packet);
344 break;
345 }
347 onRoomUpdate(event.packet);
348 break;
351 break;
352 }
354 onAmmoUpdate(event.packet);
355 break;
356 }
358 onBeamState(event.packet);
359 break;
360 }
361 case net::OpCode::Pong: {
362 onPong(event.packet);
363 break;
364 }
367 break;
368 }
370 net::ScoreUpdatePayload payload{};
371 event.packet >> payload;
372 _score = payload.score;
373 break;
374 }
376 net::HealthUpdatePayload payload{};
377 event.packet >> payload;
378 _healthCurrent = payload.current;
379 _healthMax = payload.max;
380 break;
381 }
383 net::GameOverPayload payload{};
384 event.packet >> payload;
385 GameOverSummary summary;
386 summary.bestPlayer = std::string(payload.bestPlayer);
387 summary.bestScore = payload.bestScore;
388 summary.playerScore = payload.playerScore;
389 _gameOverSummary = summary;
390 _gameOverPending = true;
391 break;
392 }
393 case net::OpCode::Kicked: {
394 _kicked = true;
396 _lastChatMessage.clear();
397 _chatHistory.clear();
398 break;
399 }
400 default:
401 log::warning("Unhandled OpCode received: {}", static_cast<uint8_t>(event.packet.header.opCode));
402 break;
403 }
404 }
405
407 {
409 packet >> payload;
410
411 if (payload.success) {
412 log::info("Login successful for user '{}'", std::string(payload.username));
413 _isLoggedIn = true;
414 _username = std::string(payload.username);
416 } else {
417 log::warning("Login failed for user '{}'", std::string(payload.username));
418 _isLoggedIn = false;
419 _username = "";
421 }
422 }
423
425 {
427 packet >> payload;
428
429 if (payload.success) {
430 log::info("Registration successful for user '{}'", std::string(payload.username));
431 _isLoggedIn = true;
432 _username = std::string(payload.username);
434 } else {
435 log::warning("Registration failed for user '{}'", std::string(payload.username));
436 _isLoggedIn = false;
437 _username = "";
439 }
440 }
441
443 {
444 _availableRooms.clear();
445
446 uint32_t roomCount = 0;
447 packet >> roomCount;
448
449 log::info("Received {} available rooms from server.", roomCount);
450
451 if (roomCount <= 0) {
452 log::info("No available rooms received from server.");
453 return;
454 }
455
456 for (uint32_t i = 0; i < roomCount; ++i) {
457 net::RoomInfo roomInfo{};
458 packet >> roomInfo;
459 _availableRooms.push_back(roomInfo);
460
461 log::info(
462 "Received Room: ID={} Name='{}' Players={}/{} InGame={} Difficulty={} Speed={} Duration={} Seed={} LevelID={}",
463 roomInfo.roomId,
464 std::string(roomInfo.roomName),
465 roomInfo.currentPlayers,
466 roomInfo.maxPlayers,
467 (int)roomInfo.inGame,
468 roomInfo.difficulty,
469 roomInfo.speed,
470 roomInfo.duration,
471 roomInfo.seed,
472 roomInfo.levelId
473 );
474 }
475 }
476
477
479 {
480 uint8_t success = 0;
481 packet >> success;
482
483 if (success) {
484 log::info("Successfully joined the room.");
486 } else {
487 log::warning("Failed to join the room.");
488 }
489 }
490
492 {
493 uint8_t success = 0;
494 packet >> success;
495 if (success) {
496 log::info("Room created successfully.");
498 if (_isStartingSolo) {
499 trySetReady(true);
500 _isStartingSolo = false;
501 }
502 } else {
503 log::warning("Failed to create the room.");
504 _isStartingSolo = false;
505 }
506 }
507
509 {
510 uint8_t success = 0;
511 packet >> success;
512
513 if (success) {
514 log::info("Successfully left the room.");
516 _lastChatMessage.clear();
517 _chatHistory.clear();
518 } else {
519 log::warning("Failed to leave the room.");
520 }
521 }
522
524 {
526 packet >> payload;
527
528 log::info("Spawning entity from server: NetID={}, Type={}, Position=({}, {})",
529 payload.netId, static_cast<int>(payload.type), payload.posX, payload.posY);
530
531 const Vec2f pos{payload.posX, payload.posY};
532
534 switch (static_cast<net::EntityType>(payload.type)) {
537 break;
540 break;
543 break;
546 break;
549 break;
550
553 break;
554
557 if (payload.weaponKind == static_cast<uint8_t>(ecs::components::WeaponKind::Boomerang)) {
558 t = EntityTemplate::shot_1(pos);
559 t.tag = "boomerang_bullet";
560 }
561 break;
564 if (payload.weaponKind == static_cast<uint8_t>(ecs::components::WeaponKind::Boomerang)) {
565 t = EntityTemplate::shot_1(pos);
566 t.tag = "boomerang_bullet";
567 }
568 if (payload.sizeX > 0.0f) {
569 float scale = 1.0f;
570 if (payload.sizeX <= 6.0f) {
571 scale = 0.5f;
572 } else if (payload.sizeX < 12.0f) {
573 scale = 1.0f;
574 } else if (payload.sizeX < 16.0f) {
575 scale = 1.5f;
576 } else {
577 scale = 2.0f;
578 }
579 t.scale = {scale, scale};
580 }
581 break;
584 break;
585
588 break;
591 break;
593 log::debug("Creating PowerupHeal template at ({}, {})", pos.x, pos.y);
595 break;
598 break;
600 log::debug("Creating PowerupDoubleFire template at ({}, {})", pos.x, pos.y);
602 break;
604 log::debug("Creating PowerupShield template at ({}, {})", pos.x, pos.y);
606 break;
608 log::debug("Creating BossShield template at ({}, {})", pos.x, pos.y);
610 break;
611
612 default:
613 log::warning("Unknown entity type {}, fallback template", (int)payload.type);
615 break;
616 }
617
618 log::debug("About to spawn entity template, tag='{}'", t.tag);
619 auto res = _builder.spawn(t);
620 if (!res) {
621 log::error("Failed to spawn entity from template: {}", res.error().message());
622 return;
623 }
624 log::debug("Successfully spawned entity from template");
625
626 auto e = res.value();
627
630 );
631
632 const auto entityType = static_cast<net::EntityType>(payload.type);
634 e, ecs::components::EntityType{entityType}
635 );
636
637 if (entityType == net::EntityType::Player) {
639 wcfg.kind = static_cast<ecs::components::WeaponKind>(payload.weaponKind);
640
642 wcfg = rtp::config::getWeaponDef(wcfg.kind);
643 wcfg.kind = static_cast<ecs::components::WeaponKind>(payload.weaponKind);
644 } else {
645 switch (wcfg.kind) {
647 wcfg.fireRate = 6.0f; wcfg.damage = 10; wcfg.ammo = -1; wcfg.maxAmmo = -1; break;
649 wcfg.fireRate = 0.0f;
650 wcfg.damage = 4;
651 wcfg.beamDuration = 5.0f;
652 wcfg.beamCooldown = 3.0f;
653 wcfg.maxAmmo = 1;
654 break;
656 wcfg.fireRate = 2.0f; wcfg.damage = 6; wcfg.homing = true; wcfg.ammo = 50; wcfg.maxAmmo = 50; break;
658 wcfg.fireRate = 0.5f; wcfg.damage = 18; wcfg.isBoomerang = true; wcfg.ammo = -1; wcfg.maxAmmo = -1; break;
659 default:
660 break;
661 }
662 }
663
664 if (auto weaponsOpt = _registry.get<ecs::components::SimpleWeapon>()) {
665 auto &weapons = weaponsOpt.value().get();
666 if (weapons.has(e)) {
667 weapons[e] = wcfg;
668 } else {
670 }
671 } else {
673 }
674
675 if (auto ammoOpt = _registry.get<ecs::components::Ammo>()) {
676 auto &ammos = ammoOpt.value().get();
677 if (ammos.has(e)) {
678 if (wcfg.maxAmmo >= 0) {
679 ammos[e].max = static_cast<uint16_t>(wcfg.maxAmmo);
680 if (wcfg.ammo > 0)
681 ammos[e].current = static_cast<uint16_t>(wcfg.ammo);
682 }
683 } else {
684 uint16_t max = (wcfg.maxAmmo >= 0) ? static_cast<uint16_t>(wcfg.maxAmmo) : 100;
685 uint16_t cur = (wcfg.ammo >= 0) ? static_cast<uint16_t>( (wcfg.ammo > 0) ? wcfg.ammo : max ) : max;
686 _registry.add<ecs::components::Ammo>(e, ecs::components::Ammo{cur, max, 2.0f, 0.0f, false, true});
687 }
688 } else {
689 uint16_t max = (wcfg.maxAmmo >= 0) ? static_cast<uint16_t>(wcfg.maxAmmo) : 100;
690 uint16_t cur = (wcfg.ammo >= 0) ? static_cast<uint16_t>( (wcfg.ammo > 0) ? wcfg.ammo : max ) : max;
691 _registry.add<ecs::components::Ammo>(e, ecs::components::Ammo{cur, max, 2.0f, 0.0f, false, true});
692 }
693
694 }
695
696 if (payload.sizeX > 0.0f && payload.sizeY > 0.0f) {
698 e, ecs::components::BoundingBox{payload.sizeX, payload.sizeY}
699 );
700 } else if (entityType == net::EntityType::Obstacle ||
701 entityType == net::EntityType::ObstacleSolid) {
703 e, ecs::components::BoundingBox{32.0f, 32.0f}
704 );
705 }
706
707 if (entityType == net::EntityType::Bullet ||
708 entityType == net::EntityType::ChargedBullet) {
709 if (auto transformsOpt = _registry.get<ecs::components::Transform>(); transformsOpt) {
710 auto &transforms = transformsOpt.value().get();
711 if (transforms.has(e)) {
712 transforms[e].rotation = 180.0f;
713 }
714 }
715 } else if (entityType == net::EntityType::EnemyBullet) {
716 if (auto transformsOpt = _registry.get<ecs::components::Transform>(); transformsOpt) {
717 auto &transforms = transformsOpt.value().get();
718 if (transforms.has(e)) {
719 transforms[e].rotation = 0.0f;
720 }
721 }
722 }
723
724 _netIdToEntity[payload.netId] = e;
725 }
726
728 {
729 net::EntityDeathPayload payload{};
730 packet >> payload;
731
732 auto it = _netIdToEntity.find(payload.netId);
733 if (it == _netIdToEntity.end()) {
734 return;
735 }
736
737 const ecs::Entity entity = it->second;
738
739 // Vérifier si c'est un power-up Shield qui a été collecté
740 auto entityTypesOpt = _registry.get<ecs::components::EntityType>();
741 if (entityTypesOpt) {
742 auto& entityTypes = entityTypesOpt.value().get();
743 if (entityTypes.has(entity)) {
744 const auto entityType = entityTypes[entity].type;
745 log::debug("EntityDeath: netId={}, entityType={}", payload.netId, static_cast<int>(entityType));
746
747 if (entityType == net::EntityType::PowerupShield) {
748 log::info("🛡️ PowerupShield collected!");
749 auto playerTypesOpt = _registry.get<ecs::components::EntityType>();
750 auto shieldVisualsOpt = _registry.get<rtp::ecs::components::ShieldVisual>();
751
752 if (playerTypesOpt && shieldVisualsOpt) {
753 auto& playerTypes = playerTypesOpt.value().get();
754 auto& shieldVisuals = shieldVisualsOpt.value().get();
755
756 int playerCount = 0;
757 for (auto playerEntity : playerTypes.entities()) {
758 if (!playerTypes.has(playerEntity)) continue;
759 if (playerTypes[playerEntity].type != net::EntityType::Player) continue;
760
761 playerCount++;
762
763 if (!shieldVisuals.has(playerEntity)) {
765 } else {
766 shieldVisuals[playerEntity].animationTime = 0.0f;
767 shieldVisuals[playerEntity].alpha = 255.0f;
768 }
769 }
770 } else {
771 log::warning("Could not get playerTypes or shieldVisuals!");
772 }
773 }
774 else if (entityType == net::EntityType::EnemyBullet) {
775 auto playerTypesOpt = _registry.get<ecs::components::EntityType>();
776 auto shieldVisualsOpt = _registry.get<rtp::ecs::components::ShieldVisual>();
777 auto transformsOpt = _registry.get<ecs::components::Transform>();
778
779 if (playerTypesOpt && shieldVisualsOpt && transformsOpt) {
780 auto& playerTypes = playerTypesOpt.value().get();
781 auto& shieldVisuals = shieldVisualsOpt.value().get();
782 auto& transforms = transformsOpt.value().get();
783
784 for (auto playerEntity : shieldVisuals.entities()) {
785 if (!shieldVisuals.has(playerEntity)) continue;
786 if (!playerTypes.has(playerEntity)) continue;
787 if (playerTypes[playerEntity].type != net::EntityType::Player) continue;
788
789 if (transforms.has(playerEntity) && transforms.has(entity)) {
790 const auto& playerPos = transforms[playerEntity].position;
791 const auto& bulletPos = transforms[entity].position;
792
793 const float dx = bulletPos.x - playerPos.x;
794 const float dy = bulletPos.y - playerPos.y;
795 const float distance = std::sqrt(dx * dx + dy * dy);
796
797 if (distance < 150.0f) {
799 log::info("🛡️ Shield absorbed bullet!");
800 break;
801 }
802 }
803 }
804 }
805 }
806 }
807 }
808
809 _registry.kill(entity);
810 _netIdToEntity.erase(it);
811 }
812
814 {
816 std::vector<net::EntitySnapshotPayload> snapshots;
817
818 packet >> header >> snapshots;
819
820 for (const auto& snap : snapshots) {
821 auto it = _netIdToEntity.find(snap.netId);
822 if (it == _netIdToEntity.end()) {
823 continue;
824 }
825
826 const ecs::Entity e = it->second;
827
828 auto transformsOpt = _registry.get<ecs::components::Transform>();
829 if (!transformsOpt)
830 continue;
831
832 auto &transforms = transformsOpt.value().get();
833 if (!transforms.has(e))
834 continue;
835
836 transforms[e].position.x = snap.position.x;
837 transforms[e].position.y = snap.position.y;
838 transforms[e].rotation = snap.rotation;
839 auto beamIt = _beamEntities.find(snap.netId);
840 if (beamIt != _beamEntities.end()) {
841 auto &vec = beamIt->second; // vector<pair<Entity, offsetY>>
842 if (auto tOpt2 = _registry.get<ecs::components::Transform>(); tOpt2) {
843 auto &transforms2 = tOpt2.value().get();
844 const float spriteW = 81.0f;
845
846 // Compute frontOffset from player's sprite width
847 float frontOffset = 20.0f;
848 ecs::Entity ownerEntity = e;
849 if (auto sOpt = _registry.get<ecs::components::Sprite>(); sOpt) {
850 auto &sprites = sOpt.value().get();
851 if (sprites.has(ownerEntity)) {
852 const float playerSpriteW = static_cast<float>(sprites[ownerEntity].rectWidth);
853 if (auto t3Opt = _registry.get<ecs::components::Transform>(); t3Opt) {
854 auto &transforms3 = t3Opt.value().get();
855 if (transforms3.has(ownerEntity)) {
856 const float playerScaleX = std::abs(transforms3[ownerEntity].scale.x);
857 frontOffset = (playerSpriteW * playerScaleX) * 0.5f + 4.0f;
858 }
859 }
860 }
861 }
862
863 auto lenIt = _beamLengths.find(snap.netId);
864 for (size_t i = 0; i < vec.size(); ++i) {
865 ecs::Entity beamEntity = vec[i].first;
866 float offsetY = vec[i].second;
867 float length = 0.0f;
868 if (lenIt != _beamLengths.end() && i < lenIt->second.size()) length = lenIt->second[i];
869
870 if (!transforms2.has(beamEntity)) continue;
871 float scaleMag = (length > 0.0f) ? (length / spriteW) : std::abs(transforms2[beamEntity].scale.x);
872 transforms2[beamEntity].rotation = 0.0f;
873 transforms2[beamEntity].scale.x = -std::abs(scaleMag);
874 const float scaledWidth = spriteW * std::abs(transforms2[beamEntity].scale.x);
875 transforms2[beamEntity].position.x = snap.position.x + frontOffset + (scaledWidth * 0.5f);
876 transforms2[beamEntity].position.y = snap.position.y + offsetY;
877 }
878 }
879 }
880 }
881 }
882
884 {
886 packet >> payload;
887
888 std::string username(payload.username);
889 std::string message(payload.message);
890 const std::string line = username.empty()
891 ? message
892 : (username + ": " + message);
893
894 pushChatMessage(line);
895 }
896
897 void NetworkSyncSystem::pushChatMessage(const std::string& message)
898 {
899 _lastChatMessage = message;
900 if (message.empty())
901 return;
902 _chatHistory.push_back(message);
903 while (_chatHistory.size() > _chatHistoryLimit) {
904 _chatHistory.pop_front();
905 }
906 }
907
909 {
910 net::AmmoUpdatePayload payload{};
911 packet >> payload;
912
913 _ammoCurrent = payload.current;
914 _ammoMax = payload.max;
915 _ammoReloading = payload.isReloading != 0;
916 _ammoReloadRemaining = payload.cooldownRemaining;
917 }
918
920 {
921 net::BeamStatePayload payload{};
922 packet >> payload;
923
924 auto it = _netIdToEntity.find(payload.ownerNetId);
925 if (it == _netIdToEntity.end()) {
926 return;
927 }
928
929
930
931 const ecs::Entity ownerEntity = it->second;
932
933 if (payload.active) {
934 Vec2f pos{0.0f, 0.0f};
935 if (auto tOpt = _registry.get<ecs::components::Transform>(); tOpt) {
936 auto &transforms = tOpt.value().get();
937 if (transforms.has(ownerEntity)) {
938 pos = transforms[ownerEntity].position;
939 }
940 }
941
943 const float spriteW = 81.0f;
944
945 float frontOffset = 20.0f;
946 if (auto sOpt = _registry.get<ecs::components::Sprite>(); sOpt) {
947 auto &sprites = sOpt.value().get();
948 if (sprites.has(ownerEntity)) {
949 const float playerSpriteW = static_cast<float>(sprites[ownerEntity].rectWidth);
950 if (auto t3Opt = _registry.get<ecs::components::Transform>(); t3Opt) {
951 auto &transforms3 = t3Opt.value().get();
952 if (transforms3.has(ownerEntity)) {
953 const float playerScaleX = std::abs(transforms3[ownerEntity].scale.x);
954 frontOffset = (playerSpriteW * playerScaleX) * 0.5f + 4.0f;
955 }
956 }
957 }
958 }
959
960 float scaleMag = (payload.length > 0.0f) ? (payload.length / spriteW) : 1.0f;
961 t.rotation = 0.0f;
962 t.scale.x = -std::abs(scaleMag);
963 t.scale.y = 1.0f;
964 t.tag = "beam_visual";
965 const float scaledWidth = spriteW * std::abs(t.scale.x);
966 t.position.x = pos.x + frontOffset + (scaledWidth * 0.5f);
967 t.position.y = pos.y + payload.offsetY;
968
969 auto res = _builder.spawn(t);
970 if (!res) {
971 log::error("Failed to spawn beam visual: {}", res.error().message());
972 return;
973 }
974 auto beamEntity = res.value();
975 _beamEntities[payload.ownerNetId].push_back({beamEntity, payload.offsetY});
976 _beamLengths[payload.ownerNetId].push_back(payload.length);
977 } else {
978 auto it2 = _beamEntities.find(payload.ownerNetId);
979 if (it2 != _beamEntities.end()) {
980 auto &vec = it2->second;
981 for (size_t i = 0; i < vec.size(); ++i) {
982 if (std::fabs(vec[i].second - payload.offsetY) < 0.1f) {
983 ecs::Entity beamEntity = vec[i].first;
984 _builder.kill(beamEntity);
985 vec.erase(vec.begin() + i);
986 auto lenIt = _beamLengths.find(payload.ownerNetId);
987 if (lenIt != _beamLengths.end() && i < lenIt->second.size()) lenIt->second.erase(lenIt->second.begin() + i);
988 break;
989 }
990 }
991 if (vec.empty()) {
992 _beamEntities.erase(it2);
993 _beamLengths.erase(payload.ownerNetId);
994 }
995 }
996 }
997
998 }
999
1001 {
1002 net::PingPayload payload{};
1003 packet >> payload;
1004 const auto now = std::chrono::steady_clock::now();
1005 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
1006 if (ms >= payload.clientTimeMs) {
1007 _pingMs = static_cast<uint32_t>(ms - payload.clientTimeMs);
1008 } else {
1009 _pingMs = 0;
1010 }
1011 }
1012
1014 {
1015 net::DebugModePayload payload{};
1016 packet >> payload;
1017 g_drawDebugBounds = (payload.enabled != 0);
1018 }
1019} // namespace rtp::client
Logger declaration with support for multiple log levels.
Network packet implementation for R-Type protocol.
void sendPacket(const net::Packet &packet, net::NetworkMode mode)
Send a packet to the server.
bool isUdpReady(void) const
Check if UDP socket is ready.
std::optional< net::NetworkEvent > pollEvent(void) override
Poll for a network event.
void kill(ecs::Entity entity)
auto spawn(const EntityTemplate &t) -> std::expected< ecs::Entity, Error >
bool _isLoggedIn
Flag indicating if the client is logged in.
void onSpawnEntityFromServer(net::Packet &packet)
std::deque< std::string > _chatHistory
Recent chat messages.
State getState(void) const
Get the current state of the client.
void tryRegister(const std::string &username, const std::string &password) const
Send a request to server attempting to register with provided credentials.
bool isInGame(void) const
Check if the client is in game.
void onEntityDeath(net::Packet &packet)
void trySetReady(bool isReady)
Send a request to set the client's ready status in the current room.
void onLeaveRoomResponse(net::Packet &packet)
void onRoomResponse(net::Packet &packet)
bool isInRoom(void) const
Check if the client is in a room.
ecs::Registry & _registry
Reference to the entity registry.
std::unordered_map< uint32_t, ecs::Entity > _netIdToEntity
Map of network IDs to entities.
void onDebugModeUpdate(net::Packet &packet)
void setGameOverSummary(const GameOverSummary &summary)
void pushChatMessage(const std::string &message)
NetworkSyncSystem(ClientNetwork &network, ecs::Registry &registry, EntityBuilder builder)
Constructor for NetworkSyncSystem.
float _ammoReloadRemaining
Reload time remaining.
bool _isReady
Flag indicating if the client is ready.
bool isReady(void) const
Check if the client is ready.
void onBeamState(net::Packet &packet)
std::list< net::RoomInfo > _availableRooms
List of available rooms from the server.
uint32_t _currentLevelId
Current level id used by client visuals.
float _pingTimer
Ping timer accumulator.
void onPong(net::Packet &packet)
void onRegisterResponse(net::Packet &packet)
void tryLogin(const std::string &username, const std::string &password, uint8_t weaponKind) const
Send a request to server attempting to log in with provided credentials.
bool isLoggedIn(void) const
Check if the client is logged in.
float _pingInterval
Ping interval in seconds.
void tryJoinRoom(uint32_t roomId, bool asSpectator=false)
Send a request to join an existing room on the server.
void tryLeaveRoom(void)
Send a request to leave the current room on the server.
void onAmmoUpdate(net::Packet &packet)
void tryStartSolo(void)
Start a solo game by creating a private room and auto-joining/readying.
std::unordered_map< uint32_t, std::vector< std::pair< ecs::Entity, float > > > _beamEntities
std::unordered_map< uint32_t, std::vector< float > > _beamLengths
bool _gameOverPending
Game over pending flag.
void onJoinRoomResponse(net::Packet &packet)
EntityBuilder _builder
Entity builder for spawning entities.
void handleEvent(net::NetworkEvent &event)
Handle a network event.
std::size_t _chatHistoryLimit
Max chat messages to keep.
void onCreateRoomResponse(net::Packet &packet)
uint32_t getCurrentLevelId(void) const
Get the current level ID for client-side visuals (parallax)
bool isUdpReady(void) const
Check if UDP is ready.
void onRoomChatReceived(net::Packet &packet)
void tryCreateRoom(const std::string &roomName, uint32_t maxPlayers, float difficulty, float speed, uint32_t duration, uint32_t seed, uint32_t levelId, uint8_t roomType)
Send a request to create a new room on the server.
void trySendMessage(const std::string &message) const
Send a chat message to the server.
void requestListRooms(void)
Send a request to have the list of available rooms from the server.
std::string _username
The username of the client.
void update(float dt) override
Update system logic for one frame.
ClientNetwork & _network
Reference to the client network manager.
bool _isStartingSolo
Check if the client is starting a solo game.
GameOverSummary _gameOverSummary
Cached game over summary.
std::string getLastChatMessage(void) const
Get the last received room chat message.
std::string getUsername(void) const
Get the username of the client.
void onLoginResponse(net::Packet &packet)
std::string _lastChatMessage
Last received chat message.
const std::deque< std::string > & getChatHistory(void) const
Get chat history buffer (most recent last)
void onRoomUpdate(net::Packet &packet)
void sendSelectedWeapon(uint8_t weaponKind) const
Send currently selected weapon to server to apply immediately.
GameOverSummary getGameOverSummary(void) const
State _currentState
Current state of the client.
std::list< net::RoomInfo > getAvailableRooms(void) const
Get a list of available rooms from the server.
uint32_t _pingMs
Latest ping in ms.
Represents an entity in the ECS (Entity-Component-System) architecture.
Definition Entity.hpp:63
void kill(Entity entity)
Definition Registry.cpp:73
auto add(Entity entity, Args &&...args) -> std::expected< std::reference_wrapper< T >, rtp::Error >
auto get(this const Self &self) -> std::expected< std::reference_wrapper< ConstLike< Self, SparseArray< T > > >, rtp::Error >
void remove(Entity entity) noexcept
Network packet with header and serializable body.
Definition Packet.hpp:471
Header header
Packet header.
Definition Packet.hpp:473
R-Type client namespace.
bool g_drawDebugBounds
bool hasWeaponConfigs()
const rtp::ecs::components::SimpleWeapon & getWeaponDef(rtp::ecs::components::WeaponKind kind)
WeaponKind
Types of player weapons.
@ Tracker
Weak auto-homing shots.
@ Boomerang
Single projectile that returns.
@ Classic
Default spam/charge laser.
@ Beam
Continuous beam 5s active, 5s cooldown.
void error(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an error message.
void warning(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log a warning message.
void info(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log an informational message.
void debug(LogFmt< std::type_identity_t< Args >... > fmt, Args &&...args) noexcept
Log a debug message.
@ Private
Private room.
EntityType
Types of entities in the game.
Definition Packet.hpp:144
@ DebugModeUpdate
Debug mode toggle.
@ SetReady
Set player readiness status.
@ LoginRequest
Client login request.
@ ListRooms
Request for room list.
@ BeamState
Beam start/stop notification.
@ RoomChatReceived
Chat message received in room.
@ LeaveRoom
Request to leave a room.
@ Pong
Ping response.
@ RegisterResponse
Incorrect password notification.
@ LoginResponse
Successful connection notification.
@ UpdateSelectedWeapon
Client selected weapon changed.
@ JoinRoom
Request to join a room.
@ RoomList
Response with room list.
@ RegisterRequest
Server login response.
@ AmmoUpdate
Ammo update notification.
@ RoomChatSended
Chat message in room.
@ StartGame
Notification to start the game.
@ GameOver
Game over summary.
@ CreateRoom
Request to create a room.
@ HealthUpdate
Player health update.
@ RoomUpdate
Notification of room update.
@ EntitySpawn
Entity spawn notification.
@ ScoreUpdate
Player score update.
@ Kicked
Player kicked notification.
@ Ping
Ping request.
@ EntityDeath
Entity death notification.
static EntityTemplate createBoss2Bullet(const Vec2f &initialPos)
static EntityTemplate createBoss2Kraken(const Vec2f &initialPos)
Vec2f position
Spawn position.
static EntityTemplate player_ship(const Vec2f &initialPos)
static EntityTemplate createBossShip(const Vec2f &initialPos)
std::string tag
Template tag for entity differentiation.
static EntityTemplate createBossShield(const Vec2f &initialPos)
static EntityTemplate effect_4(const Vec2f &initialPos)
static EntityTemplate effect_1(const Vec2f &initialPos)
static EntityTemplate createBulletEnemy(const Vec2f &initialPos)
float rotation
Spawn rotation.
static EntityTemplate createPowerUpDoubleFire(const Vec2f &initialPos)
static EntityTemplate createBulletPlayer(const Vec2f &initialPos)
static EntityTemplate shot_5(const Vec2f &initialPos)
static EntityTemplate enemy_2(const Vec2f &initialPos)
static EntityTemplate enemy_1(const Vec2f &initialPos)
static EntityTemplate createPowerUpShield(const Vec2f &initialPos)
static EntityTemplate createPowerUpHeal(const Vec2f &initialPos)
static EntityTemplate shot_1(const Vec2f &initialPos)
Ammo tracking for weapons.
Definition Ammo.hpp:18
Component representing a network identifier for an entity.
Definition NetworkId.hpp:22
Component for visual shield effect displayed around the player.
float animationTime
Time accumulator for pulse animation.
Component representing a weapon configuration.
float beamCooldown
Beam cooldown time (Beam)
int maxAmmo
Max ammo (-1 = infinite)
bool isBoomerang
Projectile returns (Boomerang)
int ammo
Current ammo (-1 = infinite)
float beamDuration
Beam active time (Beam)
float fireRate
Shots per second (or cooldown)
bool homing
Shots home to enemies (Tracker)
Component representing a sprite.
Definition Sprite.hpp:21
Component representing position, rotation, and scale of an entity.
Definition Transform.hpp:23
Vec2f position
X and Y coordinates.
Definition Transform.hpp:24
Ammo update notification data Server OpCode.
Definition Packet.hpp:374
Notify clients that an entity's beam started or stopped.
Definition Packet.hpp:385
Data for creating a new room Client OpCode.
Definition Packet.hpp:264
uint32_t seed
Seed for random generation.
Definition Packet.hpp:270
uint32_t duration
Duration of the game session.
Definition Packet.hpp:271
float difficulty
Difficulty level.
Definition Packet.hpp:267
uint32_t maxPlayers
Maximum number of players.
Definition Packet.hpp:266
uint8_t roomType
Room type (public/private)
Definition Packet.hpp:272
char roomName[64]
Desired room name.
Definition Packet.hpp:265
float speed
Speed multiplier.
Definition Packet.hpp:268
uint32_t levelId
Level identifier.
Definition Packet.hpp:269
Debug mode toggle data Server OpCode.
Definition Packet.hpp:436
Entity death notification data.
Definition Packet.hpp:362
Entity spawn notification data.
Definition Packet.hpp:348
uint8_t type
Entity type from EntityType.
Definition Packet.hpp:350
uint32_t netId
Network entity identifier.
Definition Packet.hpp:349
uint8_t weaponKind
Optional weapon kind for player entities.
Definition Packet.hpp:355
float posX
Spawn X position.
Definition Packet.hpp:351
float posY
Spawn Y position.
Definition Packet.hpp:352
float sizeY
Optional height for static entities.
Definition Packet.hpp:354
float sizeX
Optional width for static entities.
Definition Packet.hpp:353
Game over summary data.
Definition Packet.hpp:405
OpCode opCode
Operation code.
Definition Packet.hpp:135
Player health update data.
Definition Packet.hpp:415
int32_t current
Current health.
Definition Packet.hpp:416
Data for joining an existing room Client OpCode.
Definition Packet.hpp:281
uint32_t roomId
Room identifier to join.
Definition Packet.hpp:282
char password[32]
Player password.
Definition Packet.hpp:195
uint8_t weaponKind
Selected weapon kind.
Definition Packet.hpp:196
char username[32]
Player username.
Definition Packet.hpp:194
Login response data sent by server for LoginResponse Server OpCode.
Definition Packet.hpp:216
uint8_t success
Login success flag.
Definition Packet.hpp:217
char username[32]
Player username.
Definition Packet.hpp:218
Represents a network event containing session ID and packet data.
Definition INetwork.hpp:34
Ping request/response payload Client/Server /Pong OpCodes.
Definition Packet.hpp:426
Player registration data sended by client Client OpCode.
Definition Packet.hpp:205
char password[32]
Player password.
Definition Packet.hpp:207
char username[32]
Player username.
Definition Packet.hpp:206
Registration response data sent by server for RegisterResponse Server OpCode.
Definition Packet.hpp:227
uint8_t success
Registration success flag.
Definition Packet.hpp:228
char username[32]
Player username.
Definition Packet.hpp:229
Data for sending chat messages in a room Client OpCode.
Definition Packet.hpp:316
Data for receiving chat messages in a room Server OpCode.
Definition Packet.hpp:326
Information about a game room Server OpCode.
Definition Packet.hpp:244
Player score update notification data.
Definition Packet.hpp:397
int32_t score
Player score.
Definition Packet.hpp:398