From 78e77a0cdc1d8322fb7c5b2b5a30940a14a51471 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 Oct 2026 11:39:58 -0400 Subject: [PATCH] second commit --- Engine.cpp | 310 ++++++++++++++++++++++++++++++++++++++++++++--------- Gui.h | 6 +- Net.cpp | 177 +++++++++++++++++++++++++++++- Net.h | 28 ++++- 4 files changed, 466 insertions(+), 55 deletions(-) diff --git a/Engine.cpp b/Engine.cpp index c44f8b4..22bcd10 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -93,6 +93,11 @@ Engine::Engine() glfwSetMouseButtonCallback(window->handle, mouse_button_callback); glfwSwapInterval(1); + // Start from the real framebuffer size (can differ from the requested size under DPI scaling) + int fbw, fbh; + glfwGetFramebufferSize(window->handle, &fbw, &fbh); + framebuffer_size_callback(window->handle, fbw, fbh); + rendersystem = new RenderSystem(); rendersystem->Initialize(); physicsengine = new Physics(); @@ -243,17 +248,6 @@ void Engine::Initiate() crosshair->Dot = true; crosshair->Apply(); - TextBox* tb = new TextBox(); - tb->font = arial; - tb->text = "ASDASD\nASDASD\nADSADSA"; - tb->fontsize = 0.5f; - tb->Color = { 0.0f, 0.0f, 0.0f }; - tb->t2d.position = { 0.5f, 0.5f, 0.0f, 0.0f }; - tb->t2d.center = { 0.0f, 0.0f }; - tb->textCenter = { 0.0f, 0.5f }; - tb->t2d.size = { 0.2f, 0.1f, 0.0f, 0.0f }; - tb->BindToRenderSystem(); - TextBox* fpsCounter = new TextBox(); fpsCounter->font = arial; fpsCounter->text = "TEST1231231123"; @@ -295,7 +289,7 @@ void Engine::Initiate() grappleText->t2d.position = { 0.5f, 1.0f, 0.0f, -20.0f }; grappleText->t2d.center = { 0.5f, 1.0f }; grappleText->textCenter = { 0.5f, 0.5f }; - grappleText->t2d.size = { 0.0f, 0.0f, 300.0f, 40.0f }; + grappleText->t2d.size = { 0.0f, 0.0f, 560.0f, 40.0f }; grappleText->BindToRenderSystem(); ImageBox* testgui = new ImageBox(); @@ -325,7 +319,7 @@ void Engine::Initiate() Mesh* debugcube = CreateCubeMesh(); debugcube->name = "debugcube"; debugcube->t.ScaleTo({ 0.8f, 0.8f, 0.8f }); - debugcube->BindToRenderSystem(); + // Punch hitbox only: intentionally not bound to the render system, so it isn't drawn mainf->AddChild(debugcube); Mesh* floor = CreateCubeMesh(); @@ -352,22 +346,6 @@ void Engine::Initiate() wallBody->friction = 0.0f; physicsengine->AddObject(wallBody); - Mesh* cube = CreateCubeMesh(); - cube->name = "cube"; - cube->meshTexture = blanktex; - cube->t.ScaleTo({ 1.0f, 1.0f, 1.0f }); - cube->t.TranslateTo({ 5.0f, 4.0f, 6.5f }); - cube->BindToRenderSystem(); - mainf->AddChild(cube); - - Mesh* cube2 = CreateCubeMesh(); - cube2->name = "cubee"; - cube2->meshTexture = blanktex; - cube2->t.ScaleTo({ 1.0f, 1.0f, 1.0f }); - cube2->t.TranslateTo({ 10.0f, 4.0f, 10.5f }); - cube2->BindToRenderSystem(); - mainf->AddChild(cube2); - // Ground check auto IsGrounded = [&]() -> bool { Ray downRay; @@ -390,16 +368,16 @@ void Engine::Initiate() // body = 50 damage (two shots). Respawns the target when it dies. // Movement tuning const float airControl = 0.5f; // fraction of ground acceleration available in the air - const float grappleRange = 4.0f; // metres from the player + const float grappleRange = 8.0f; // metres from the player const float grappleSpeed = 28.0f; // launch speed toward the hit point const float grappleCooldownTime = 2.5f; // seconds float grappleCooldown = 0.0f; - // Slide (key C): locked direction, committed for slideDuration, speed eases from + // Slide (key Left Ctrl): locked direction, committed for slideDuration, speed eases from // slideBoost x ground top speed down to 1x by slideFalloffTime. const float slideBoost = 2.0f; - const float slideDuration = 0.8f; - const float slideFalloffTime = 0.7f; + const float slideDuration = 1.2f; + const float slideFalloffTime = 1.05f; const float slideDropY = 0.65f; // standing centre height (0.9) minus lying (0.25) const float standEyeOffset = 0.7f; // eyes 0.7 above body centre (body centre 0.9, height 1.8) const float slideEyeOffset = 0.25f; // lying down, eyes just above the floor @@ -408,17 +386,119 @@ void Engine::Initiate() float slideTime = 0.0f; glm::vec3 slideDir(0.0f); + // Shockwave (key Q): projectile that explodes on the first surface it touches. The blast + // launches the player, the NPC and loose props away from the impact point. No damage. + const float shockSpeed = 30.0f; + const float shockGravity = 6.0f; + const float shockLifetime = 3.0f; + const float shockRadius = 7.0f; + const float shockPower = 26.0f; // launch speed at the centre of the blast + const float shockCooldownTime = 6.0f; + const float blastShowTime = 0.25f; + const glm::vec3 hiddenPos(0.0f, -1000.0f, 0.0f); + float shockCooldown = 0.0f; + bool shockActive = false; + float shockAge = 0.0f; + float blastTime = 0.0f; + glm::vec3 shockVel(0.0f); + + Mesh* shockMesh = CreateCubeMesh(); + shockMesh->name = "shockwave"; + shockMesh->meshTexture = blanktex; + shockMesh->t.ScaleTo({ 0.3f, 0.3f, 0.3f }); + shockMesh->t.TranslateTo(hiddenPos); + shockMesh->BindToRenderSystem(); + mainf->AddChild(shockMesh); + + Mesh* blastMesh = CreateCubeMesh(); + blastMesh->name = "shockblast"; + blastMesh->meshTexture = blanktex; + blastMesh->t.TranslateTo(hiddenPos); + blastMesh->BindToRenderSystem(); + mainf->AddChild(blastMesh); + + // fromNet = another player's blast: we only launch ourselves (the other machines launch + // themselves the same way, since remote players are just puppets on this machine). + auto Explode = [&](const glm::vec3& center, bool fromNet = false, + float radius = shockRadius, float power = shockPower) { + auto launch = [&](const glm::vec3& pos, p* body) { + const glm::vec3 offset = pos - center; + const float dist = glm::length(offset); + if (dist >= radius) return; + // Away from the blast, biased upward so grounded targets actually lift off + glm::vec3 dir = dist > 0.001f ? offset / dist : glm::vec3(0.0f, 1.0f, 0.0f); + dir.y = std::max(dir.y, 0.0f) + 0.6f; + dir = glm::normalize(dir); + const float falloff = 1.0f - 0.7f * (dist / radius); // 100% at centre, 30% at the edge + body->linearvelocity += dir * power * falloff; + body->WakeUp(); + + if (body == characterBody && sliding) { + // The slide overrides horizontal velocity every frame, so end it and stand back up + sliding = false; + glm::vec3 p0 = testcharacter->body->t.GetTranslation(); + p0.y += slideDropY + 0.02f; + testcharacter->body->t.TranslateTo(p0); + } + }; + launch(testcharacter->body->t.GetTranslation(), characterBody); + if (!fromNet) { + launch(npc->body->t.GetTranslation(), npcBody); + for (p* obj : physicsengine->GetObjects()) { + if (!obj->velocity || obj == characterBody || obj == npcBody) continue; + launch(obj->pointer->t.GetTranslation(), obj); + } + + NetBlast nb; + nb.center = center; + nb.radius = radius; + nb.power = power; + if (lan && lan->IsActive()) lan->SendBlast(nb); + + shockActive = false; + shockMesh->t.TranslateTo(hiddenPos); + } + blastMesh->t.TranslateTo(center); + blastMesh->t.ScaleTo(glm::vec3(0.5f)); + blastTime = blastShowTime; + camera->AddScreenshake(0.25f); + }; + const int maxHealth = 100; const int bodyDamage = 50; + const int punchDamage = 25; const float headFraction = 0.8f; + + // Random respawn spot on the floor (NPC and player), kept out of the wall (x -9.5..-6.5, z -11..11) + std::mt19937 respawnRng(std::random_device{}()); + auto RandomSpawn = [&]() -> glm::vec3 { + std::uniform_real_distribution spread(-20.0f, 20.0f); + for (;;) { + const float x = spread(respawnRng), z = spread(respawnRng); + if (x > -9.5f && x < -6.5f && z > -11.0f && z < 11.0f) continue; + return { x, 0.9f, z }; + } + }; + auto Shoot = [&]() { Ray ray; ray.origin = camera->t.GetTranslation(); ray.direction = glm::normalize(camera->t.GetFrontVector()); - auto targetHit = IsRayInT(ray, npc->body->t); - if (!targetHit.has_value()) return; - const float targetDist = glm::distance(ray.origin, targetHit.value()); + // Nearest shootable target along the ray: the NPC or any remote (LAN) player + float targetDist = 1e9f; + glm::vec3 targetPoint(0.0f); + t* targetT = nullptr; + int targetRemoteId = -1; // -1 = the NPC + auto consider = [&](t& tr, int remoteId) { + auto h = IsRayInT(ray, tr); + if (!h.has_value()) return; + const float d = glm::distance(ray.origin, h.value()); + if (d < targetDist) { targetDist = d; targetPoint = h.value(); targetT = &tr; targetRemoteId = remoteId; } + }; + consider(npc->body->t, -1); + for (auto& kv : remotes) consider(kv.second.character->body->t, kv.first); + if (!targetT) return; // Walls/props in front of the target block the shot for (p* obj : physicsengine->GetObjects()) { @@ -429,16 +509,27 @@ void Engine::Initiate() return; } - const float heightFrac = (targetHit->y - npc->body->t.GetTranslation().y) - / npc->body->t.GetScale().y + 0.5f; + const float heightFrac = (targetPoint.y - targetT->GetTranslation().y) + / targetT->GetScale().y + 0.5f; const bool headshot = heightFrac >= headFraction; - npc->body->Health -= headshot ? maxHealth : bodyDamage; - std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - NPC HP " << npc->body->Health << "\n"; + const int damage = headshot ? maxHealth : bodyDamage; camera->AddScreenshake(0.05f); + if (targetRemoteId >= 0) { + // Another player: their machine owns their health, so tell it + NetHit hit; + hit.damage = damage; + if (lan && lan->IsActive()) lan->SendHit((uint8_t)targetRemoteId, hit); + std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - player " << targetRemoteId << "\n"; + return; + } + + npc->body->Health -= damage; + std::cout << (headshot ? "HEADSHOT" : "BODY SHOT") << " - NPC HP " << npc->body->Health << "\n"; + if (npc->body->Health <= 0) { npc->body->Health = maxHealth; - npc->body->t.TranslateTo({ 3.0f, 0.9f, 3.0f }); + npc->body->t.TranslateTo(RandomSpawn()); npcBody->linearvelocity = glm::vec3(0.0f); npcBody->WakeUp(); } @@ -454,7 +545,8 @@ void Engine::Initiate() const glm::vec3 camFront = camera->t.GetFrontVector(); const glm::vec3 camRight = camera->t.GetRightVector(); - const float movespeed = 3.0f * 1.3f; + const float walkScale = 0.5f; // WASD speed relative to the original 3.9 + const float movespeed = 3.0f * 1.3f * walkScale; // -------------------------------------------------------------------- // Input � mouse @@ -516,10 +608,30 @@ void Engine::Initiate() npcBody->WakeUp(); npcBody->linearvelocity += playerFront * 5.0f + glm::vec3(0.0f, 3.0f, 0.0f); camera->AddScreenshake(0.15f); // landing a punch feels impactful + + npc->body->Health -= punchDamage; + std::cout << "PUNCH - NPC HP " << npc->body->Health << "\n"; + if (npc->body->Health <= 0) { + npc->body->Health = maxHealth; + npc->body->t.TranslateTo(RandomSpawn()); + npcBody->linearvelocity = glm::vec3(0.0f); + npcBody->WakeUp(); + } } else { std::cout << "MISSED\n"; } + + // Punch LAN players too: their machine applies the damage and knockback + for (auto& kv : remotes) { + if (!TInT(debugcube->t, kv.second.character->body->t).has_value()) continue; + NetHit ph; + ph.damage = punchDamage; + ph.knockback = playerFront * 5.0f + glm::vec3(0.0f, 3.0f, 0.0f); + if (lan && lan->IsActive()) lan->SendHit((uint8_t)kv.first, ph); + camera->AddScreenshake(0.15f); + std::cout << "PUNCH - player " << kv.first << "\n"; + } } if (buffer.GetCode() == GLFW_KEY_LEFT_SHIFT && buffer.IsPress() && !sliding) { glm::vec3 dashDir = glm::length(inputVel) > 0.0f @@ -529,7 +641,7 @@ void Engine::Initiate() characterBody->linearvelocity.z += dashDir.z * 18.0f; camera->AddScreenshake(0.04f); } - if (buffer.GetCode() == 'C' && buffer.IsPress() && !sliding && IsGrounded()) { + if (buffer.GetCode() == GLFW_KEY_LEFT_CONTROL && buffer.IsPress() && !sliding && IsGrounded()) { const glm::vec3 horizVel(characterBody->linearvelocity.x, 0.0f, characterBody->linearvelocity.z); if (glm::length(inputVel) > 0.0f || glm::length(horizVel) > 1.0f) { slideDir = glm::length(inputVel) > 0.0f ? glm::normalize(inputVel) : glm::normalize(horizVel); @@ -570,12 +682,21 @@ void Engine::Initiate() camera->AddScreenshake(0.1f); } } + if (buffer.GetCode() == 'Q' && buffer.IsPress() && !shockActive && shockCooldown <= 0.0f) { + const glm::vec3 front = glm::normalize(camFront); + shockMesh->t.TranslateTo(camera->t.GetTranslation() + front * 0.6f); + shockVel = front * shockSpeed; + shockActive = true; + shockAge = 0.0f; + shockCooldown = shockCooldownTime; + camera->AddScreenshake(0.05f); + } if (buffer.GetCode() == 'G' && buffer.IsPress()) { camera->AddScreenshake(0.2f); } if (buffer.GetCode() == GLFW_KEY_SPACE && buffer.IsPress()) { if (!sliding && IsGrounded()) - characterBody->linearvelocity.y = 5.0f; + characterBody->linearvelocity.y = 12.0f; } } @@ -594,7 +715,7 @@ void Engine::Initiate() // Direction is locked; speed eases from boost x down to 1x (quadratic: drops fastest early) slideTime += deltatime; const float u = glm::clamp(slideTime / slideFalloffTime, 0.0f, 1.0f); - const float speed = groundTopSpeed * (1.0f + (slideBoost - 1.0f) * (1.0f - u) * (1.0f - u)); + const float speed = (groundTopSpeed / walkScale) * (1.0f + (slideBoost - 1.0f) * (1.0f - u) * (1.0f - u)); characterBody->linearvelocity.x = slideDir.x * speed; characterBody->linearvelocity.z = slideDir.z * speed; @@ -634,6 +755,48 @@ void Engine::Initiate() debugcube->t.RotateToQuaternion(LookAt(-playerFront)); } + // Shockwave projectile: arc forward, explode on the first collider along this frame's path + shockCooldown = std::max(shockCooldown - deltatime, 0.0f); + if (shockActive) { + shockVel.y -= shockGravity * deltatime; + shockAge += deltatime; + const glm::vec3 from = shockMesh->t.GetTranslation(); + const glm::vec3 step = shockVel * deltatime; + const float stepLen = glm::length(step); + + Ray ray; + ray.origin = from; + ray.direction = step / stepLen; + float bestDist = stepLen; + glm::vec3 bestPoint(0.0f); + bool found = false; + for (p* obj : physicsengine->GetObjects()) { + if (obj == characterBody || !obj->collision) continue; + auto hit = IsRayInT(ray, obj->pointer->t); + if (!hit.has_value()) continue; + const float d = glm::distance(from, hit.value()); + if (d <= bestDist) { bestDist = d; bestPoint = hit.value(); found = true; } + } + + if (found) Explode(bestPoint); + else if (shockAge >= shockLifetime) { + shockActive = false; + shockMesh->t.TranslateTo(hiddenPos); + } + else shockMesh->t.TranslateTo(from + step); + } + if (blastTime > 0.0f) { + blastTime -= deltatime; + if (blastTime <= 0.0f) { + blastMesh->t.TranslateTo(hiddenPos); + } + else { + // Expand to the full blast radius over its lifetime + const float u = 1.0f - blastTime / blastShowTime; + blastMesh->t.ScaleTo(glm::vec3(0.5f + (shockRadius * 2.0f - 0.5f) * u)); + } + } + rain->t.TranslateTo(camera->t.GetTranslation() + glm::vec3(0.0f, 20.0f, 0.0f)); for (int i = 0; i < 30; i++) rain->Emit(); @@ -647,8 +810,7 @@ void Engine::Initiate() camera->t.TranslateTo(testcharacter->body->t.GetTranslation() + glm::vec3(0.0f, eyeOffset, 0.0f)); Sound* music = static_cast(mainf->GetFirstChildOfName("music")); - Mesh* cube = static_cast(mainf->GetFirstChildOfName("cube")); - music->Update3DPosition(cube->t.GetTranslation()); + music->Update3DPosition(5.0f, 4.0f, 6.5f); // where the old floating cube was soundsystem->SetListenerPosition( camera->t.GetTranslation(), @@ -675,10 +837,21 @@ void Engine::Initiate() } fpsCounter->text = std::to_string(measuredFps) + " FPS"; } + // Player respawn: at 0 HP or after falling off the map, teleport to a random spot + if (testcharacter->body->Health <= 0 || testcharacter->body->t.GetTranslation().y < -20.0f) { + testcharacter->body->Health = maxHealth; + testcharacter->body->t.TranslateTo(RandomSpawn()); + characterBody->linearvelocity = glm::vec3(0.0f); + characterBody->WakeUp(); + sliding = false; + } hpText->text = std::to_string(std::max(testcharacter->body->Health, 0)) + "/100 HP"; grappleText->text = grappleCooldown > 0.0f ? "GRAPPLE " + std::to_string(grappleCooldown).substr(0, 3) + "s" : "GRAPPLE READY"; + grappleText->text += shockCooldown > 0.0f + ? " | SHOCKWAVE " + std::to_string(shockCooldown).substr(0, 3) + "s" + : " | SHOCKWAVE READY (Q)"; // -------------------------------------------------------------------- // Step systems @@ -702,7 +875,20 @@ void Engine::Initiate() const double now = glfwGetTime(); std::vector updates; - lan->Receive(updates); + std::vector blasts; + std::vector hits; + lan->Receive(updates, &blasts, &hits); + for (const NetBlast& b : blasts) + Explode(b.center, true, b.radius, b.power); + // Damage from other players (respawn handles dying, see the player respawn check) + for (const NetHit& h : hits) { + testcharacter->body->Health -= h.damage; + characterBody->linearvelocity += h.knockback; + characterBody->WakeUp(); + camera->AddScreenshake(0.15f); + std::cout << "HIT by player " << (int)h.sender << " for " << h.damage + << " - HP " << testcharacter->body->Health << "\n"; + } for (const NetUpdate& u : updates) { auto it = remotes.find(u.id); if (it == remotes.end()) { @@ -757,10 +943,31 @@ void Engine::Initiate() // ============================================================================ void framebuffer_size_callback(GLFWwindow* window, int w, int h) { + if (w <= 0 || h <= 0) return; // minimised + // Everything pixel-based (viewport, projection aspect, GUI layout, scissor) reads these + ::window->width = w; + ::window->height = h; + glViewport(0, 0, w, h); } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { + // F11: toggle fullscreen on the primary monitor, restoring the old window rect on exit + if (key == GLFW_KEY_F11 && action == GLFW_PRESS) { + static int wx = 100, wy = 100, ww = 1280, wh = 720; + if (glfwGetWindowMonitor(window)) { + glfwSetWindowMonitor(window, nullptr, wx, wy, ww, wh, 0); + } + else { + glfwGetWindowPos(window, &wx, &wy); + glfwGetWindowSize(window, &ww, &wh); + GLFWmonitor* monitor = glfwGetPrimaryMonitor(); + const GLFWvidmode* mode = glfwGetVideoMode(monitor); + glfwSetWindowMonitor(window, monitor, 0, 0, mode->width, mode->height, mode->refreshRate); + } + glfwSwapInterval(1); + } + if (action == GLFW_PRESS) keyboard->KeyDown(key); else if (action == GLFW_RELEASE) keyboard->KeyUp(key); } @@ -773,8 +980,11 @@ void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) { mouse->OnMouseMove(static_cast(xposIn), static_cast(yposIn)); glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - glfwSetCursorPos(window, ::window->width / 2, ::window->height / 2); - mouse->OnMouseMove(::window->width / 2.0f, ::window->height / 2.0f); + // Cursor coordinates are in window units, which can differ from framebuffer pixels (DPI scaling) + int ww, wh; + glfwGetWindowSize(window, &ww, &wh); + glfwSetCursorPos(window, ww / 2.0, wh / 2.0); + mouse->OnMouseMove(ww / 2.0f, wh / 2.0f); } void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) diff --git a/Gui.h b/Gui.h index ab6c003..59d464d 100644 --- a/Gui.h +++ b/Gui.h @@ -225,9 +225,9 @@ class Crosshair { public: glm::vec3 Color = { 0.0f, 1.0f, 0.0f }; float Opacity = 1.0f; - float Length = 8.0f; // arm length - float Thickness = 2.0f; // arm width - float Gap = 4.0f; // distance from center to the start of each arm + float Length = 11.2f; // arm length + float Thickness = 2.8f; // arm width + float Gap = 5.6f; // distance from center to the start of each arm bool Dot = false; // center dot bool Visible = true; diff --git a/Net.cpp b/Net.cpp index 7160f26..fc5f879 100644 --- a/Net.cpp +++ b/Net.cpp @@ -38,6 +38,35 @@ namespace { }; #pragma pack(pop) + constexpr uint32_t NET_MAGIC_BLAST = 0x45494942; // "EIIB" + constexpr int BLAST_COPIES = 4; // each blast is sent this many times + +#pragma pack(push, 1) + struct BlastPacket { + uint32_t magic; + uint16_t version; + uint8_t sender; + uint32_t id; + float center[3]; + float radius; + float power; + }; +#pragma pack(pop) + + constexpr uint32_t NET_MAGIC_HIT = 0x45494948; // "EIIH" + +#pragma pack(push, 1) + struct HitPacket { + uint32_t magic; + uint16_t version; + uint8_t sender; + uint8_t target; + uint32_t id; + int16_t damage; + float knock[3]; + }; +#pragma pack(pop) + constexpr int MAX_ENTRIES = Lan::MaxPlayers; constexpr int MAX_DATAGRAM = (int)sizeof(Header) + MAX_ENTRIES * (int)sizeof(Entry); @@ -92,6 +121,33 @@ struct Lan::Impl { uint32_t lastSeq = 0; double lastRecvTime = 0.0; + // Blasts waiting for their remaining copies to go out + struct PendingBlast { BlastPacket pkt; int copiesLeft; }; + std::vector pendingBlasts; + uint32_t blastSeq = 0; + std::vector seenBlasts; // (type, sender, id) of recent blasts/hits, for de-duplication + + struct PendingHit { HitPacket pkt; int copiesLeft; }; + std::vector pendingHits; + uint32_t hitSeq = 0; + + void SendHitPacket(const sockaddr_in& to, const HitPacket& pkt) { + sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to)); + } + + // True the first time a (type, sender, id) triple is seen. type: 0 = blast, 1 = hit. + bool FirstSighting(uint8_t type, uint8_t sender, uint32_t id) { + const uint64_t key = ((uint64_t)type << 40) | ((uint64_t)sender << 32) | id; + if (std::find(seenBlasts.begin(), seenBlasts.end(), key) != seenBlasts.end()) return false; + seenBlasts.push_back(key); + if (seenBlasts.size() > 64) seenBlasts.erase(seenBlasts.begin()); + return true; + } + + void SendBlastPacket(const sockaddr_in& to, const BlastPacket& pkt) { + sendto(sock, (const char*)&pkt, (int)sizeof(pkt), 0, (const sockaddr*)&to, (int)sizeof(to)); + } + void SendDatagram(const sockaddr_in& to, const std::vector& entries) { char buf[MAX_DATAGRAM]; Header h; @@ -174,14 +230,68 @@ void Lan::Stop() { impl->wsaStarted = false; } impl->peers.clear(); + impl->pendingBlasts.clear(); + impl->pendingHits.clear(); + impl->seenBlasts.clear(); impl->anyRecv = false; active = false; mode = Mode::Off; } +void Lan::SendBlast(const NetBlast& b) { + if (!active) return; + Impl::PendingBlast pb; + pb.pkt.magic = NET_MAGIC_BLAST; + pb.pkt.version = NET_VERSION; + pb.pkt.sender = 0; // the host stamps the real id when relaying a client's blast + pb.pkt.id = ++impl->blastSeq; + pb.pkt.center[0] = b.center.x; pb.pkt.center[1] = b.center.y; pb.pkt.center[2] = b.center.z; + pb.pkt.radius = b.radius; + pb.pkt.power = b.power; + pb.copiesLeft = BLAST_COPIES; + impl->pendingBlasts.push_back(pb); +} + +void Lan::SendHit(uint8_t target, const NetHit& h) { + if (!active) return; + Impl::PendingHit ph; + ph.pkt.magic = NET_MAGIC_HIT; + ph.pkt.version = NET_VERSION; + ph.pkt.sender = 0; // the host stamps the real id when relaying a client's hit + ph.pkt.target = target; + ph.pkt.id = ++impl->hitSeq; + ph.pkt.damage = (int16_t)std::clamp(h.damage, 0, 1000); + ph.pkt.knock[0] = h.knockback.x; ph.pkt.knock[1] = h.knockback.y; ph.pkt.knock[2] = h.knockback.z; + ph.copiesLeft = BLAST_COPIES; + impl->pendingHits.push_back(ph); +} + void Lan::Send(const NetState& s) { if (!active) return; + // One more copy of every pending hit: clients send to the host, the host to the target only + for (Impl::PendingHit& ph : impl->pendingHits) { + if (mode == Mode::Client) impl->SendHitPacket(impl->hostAddr, ph.pkt); + else for (const Impl::Peer& to : impl->peers) + if (to.id == ph.pkt.target) impl->SendHitPacket(to.addr, ph.pkt); + ph.copiesLeft--; + } + impl->pendingHits.erase( + std::remove_if(impl->pendingHits.begin(), impl->pendingHits.end(), + [](const Impl::PendingHit& ph) { return ph.copiesLeft <= 0; }), + impl->pendingHits.end()); + + // One more copy of every pending blast (to the host, or from the host to everyone) + for (Impl::PendingBlast& pb : impl->pendingBlasts) { + if (mode == Mode::Client) impl->SendBlastPacket(impl->hostAddr, pb.pkt); + else for (const Impl::Peer& to : impl->peers) impl->SendBlastPacket(to.addr, pb.pkt); + pb.copiesLeft--; + } + impl->pendingBlasts.erase( + std::remove_if(impl->pendingBlasts.begin(), impl->pendingBlasts.end(), + [](const Impl::PendingBlast& pb) { return pb.copiesLeft <= 0; }), + impl->pendingBlasts.end()); + if (mode == Mode::Client) { impl->SendDatagram(impl->hostAddr, { MakeEntry(0, s) }); return; @@ -199,7 +309,7 @@ void Lan::Send(const NetState& s) { } } -void Lan::Receive(std::vector& out) { +void Lan::Receive(std::vector& out, std::vector* blasts, std::vector* hits) { if (!active) return; char buf[MAX_DATAGRAM + 64]; @@ -213,6 +323,71 @@ void Lan::Receive(std::vector& out) { } if (r < (int)sizeof(Header)) continue; + uint32_t magic; + std::memcpy(&magic, buf, sizeof(magic)); + if (magic == NET_MAGIC_BLAST) { + if (r != (int)sizeof(BlastPacket)) continue; + BlastPacket bp; + std::memcpy(&bp, buf, sizeof(bp)); + if (bp.version != NET_VERSION) continue; + + if (mode == Mode::Host) { + // Only known players may blast. Stamp their id and relay to everyone else. + const Impl::Peer* sender = nullptr; + for (const Impl::Peer& pe : impl->peers) + if (SameAddress(pe.addr, from)) { sender = &pe; break; } + if (!sender) continue; + bp.sender = sender->id; + for (const Impl::Peer& to : impl->peers) + if (to.id != sender->id) impl->SendBlastPacket(to.addr, bp); + } + else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host + + if (blasts && impl->FirstSighting(0, bp.sender, bp.id)) { + NetBlast nb; + nb.sender = bp.sender; + nb.id = bp.id; + nb.center = glm::vec3(bp.center[0], bp.center[1], bp.center[2]); + nb.radius = bp.radius; + nb.power = bp.power; + blasts->push_back(nb); + } + continue; + } + + if (magic == NET_MAGIC_HIT) { + if (r != (int)sizeof(HitPacket)) continue; + HitPacket hp; + std::memcpy(&hp, buf, sizeof(hp)); + if (hp.version != NET_VERSION) continue; + + bool forUs = true; + if (mode == Mode::Host) { + // Only known players may shoot. Stamp their id; deliver to us or forward to the target. + const Impl::Peer* sender = nullptr; + for (const Impl::Peer& pe : impl->peers) + if (SameAddress(pe.addr, from)) { sender = &pe; break; } + if (!sender) continue; + hp.sender = sender->id; + if (hp.target != 0) { + forUs = false; + for (const Impl::Peer& to : impl->peers) + if (to.id == hp.target) impl->SendHitPacket(to.addr, hp); + } + } + else if (!SameAddress(from, impl->hostAddr)) continue; // clients only listen to the host + + if (forUs && hits && impl->FirstSighting(1, hp.sender, hp.id)) { + NetHit nh; + nh.sender = hp.sender; + nh.id = hp.id; + nh.damage = hp.damage; + nh.knockback = glm::vec3(hp.knock[0], hp.knock[1], hp.knock[2]); + hits->push_back(nh); + } + continue; + } + Header h; std::memcpy(&h, buf, sizeof(h)); if (h.magic != NET_MAGIC || h.version != NET_VERSION) continue; diff --git a/Net.h b/Net.h index a0d094b..d466033 100644 --- a/Net.h +++ b/Net.h @@ -24,6 +24,25 @@ struct NetUpdate { NetState state; }; +// A shockwave explosion. Every machine applies the launch to its own player, since remote +// players are only puppets here. Blasts are re-sent a few times (UDP) and de-duplicated on receive. +struct NetBlast { + uint8_t sender = 0; // filled in by the host; ignored on send + uint32_t id = 0; // filled in by SendBlast + glm::vec3 center = glm::vec3(0.0f); + float radius = 0.0f; + float power = 0.0f; +}; + +// Damage dealt to another player. Each machine owns its own player's health, so the shooter +// aims locally and sends this to the victim, who applies it. Re-sent a few times and de-duplicated. +struct NetHit { + uint8_t sender = 0; // filled in by the host + uint32_t id = 0; // filled in by SendHit + int damage = 0; + glm::vec3 knockback = glm::vec3(0.0f); // added to the victim's velocity +}; + class Lan { public: enum class Mode { Off, Host, Client }; @@ -44,7 +63,14 @@ public: // Call once per frame with our own state. void Send(const NetState& state); // Drains all waiting packets and appends the other players' newest states to 'out'. - void Receive(std::vector& out); + // If 'blasts' is given, new (de-duplicated) shockwave blasts from other players are appended to it. + // 'hits' gets damage aimed at us (de-duplicated). + void Receive(std::vector& out, std::vector* blasts = nullptr, + std::vector* hits = nullptr); + // Damage another player. 'target' is the id from their NetUpdate (the host is 0). + void SendHit(uint8_t target, const NetHit& hit); + // Tell the other players about a blast we caused. Sent over the next few Send() calls. + void SendBlast(const NetBlast& blast); // Host: at least one player has joined. Client: the host has been heard from in the last 2 s. bool IsConnected() const;