From e24ad18dd0b7e75621f366ca0263fd75ce278dd2 Mon Sep 17 00:00:00 2001 From: qxdFox Date: Wed, 1 Apr 2026 15:51:42 +0200 Subject: [PATCH 1/2] Add Hide and Seek --- CMakeLists.txt | 2 + src/base/system.cpp | 8 +- src/base/system.h | 3 + src/engine/shared/config_variables_foxnet.h | 15 + src/game/gamecore.cpp | 5 +- src/game/quad_data.h | 1 + src/game/server/entities/character.cpp | 119 +- src/game/server/entities/character.h | 12 +- src/game/server/entities/projectile.cpp | 18 +- src/game/server/entities/projectile.h | 4 +- src/game/server/foxnet/commands.cpp | 6 + src/game/server/foxnet/component.h | 80 ++ .../foxnet/components/zones/hidenseek.cpp | 1125 +++++++++++++++++ .../foxnet/components/zones/hidenseek.h | 144 +++ .../foxnet/components/zones/roulette.cpp | 49 +- .../server/foxnet/components/zones/roulette.h | 14 +- .../server/foxnet/components/zones/zone.cpp | 6 + .../server/foxnet/components/zones/zone.h | 23 + .../foxnet/components/zones/zonemanager.cpp | 184 ++- .../foxnet/components/zones/zonemanager.h | 19 + src/game/server/foxnet/entities/roulette.cpp | 4 - src/game/server/foxnet/gamecontext.cpp | 43 + src/game/server/foxnet/player.cpp | 50 +- src/game/server/gamecontext.cpp | 6 + src/game/server/gamecontext.h | 9 +- src/game/server/gamecontroller.cpp | 5 +- src/game/server/player.cpp | 34 +- src/game/server/player.h | 11 +- src/game/server/teams.cpp | 14 +- src/game/tuning.h | 2 + 30 files changed, 1901 insertions(+), 114 deletions(-) create mode 100644 src/game/server/foxnet/components/zones/hidenseek.cpp create mode 100644 src/game/server/foxnet/components/zones/hidenseek.h diff --git a/CMakeLists.txt b/CMakeLists.txt index dd352af9751..c8e880af82c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2893,6 +2893,8 @@ if(SERVER) foxnet/components/zones/death.h foxnet/components/zones/freeze.cpp foxnet/components/zones/freeze.h + foxnet/components/zones/hidenseek.cpp + foxnet/components/zones/hidenseek.h foxnet/components/zones/roulette.cpp foxnet/components/zones/roulette.h foxnet/components/zones/unfreeze.cpp diff --git a/src/base/system.cpp b/src/base/system.cpp index 7b362b8ece7..0658d48fdf6 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -1,4 +1,4 @@ - + #include "system.h" #include "dbg.h" @@ -543,5 +543,11 @@ std::string SanitizeMessage(const char *pMessage) return Out; } +std::mt19937 &Rng() +{ + static std::random_device rd; + static std::mt19937 gen(rd()); + return gen; +} // FoxNet> diff --git a/src/base/system.h b/src/base/system.h index 94832bc2cdb..0ab873b84c5 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -14,6 +14,7 @@ #include #include +#include char str_lowercase(char c); void str_lower(char *pOut); @@ -40,4 +41,6 @@ const char *GetParsedArgument(const char *pStr, int Index, bool Rest); void Rotate(vec2 Center, vec2 *pPoint, float Rotation); std::string SanitizeMessage(const char *pMessage); +std::mt19937 &Rng(); + #endif diff --git a/src/engine/shared/config_variables_foxnet.h b/src/engine/shared/config_variables_foxnet.h index 1d8ba7de83f..09c3af49299 100644 --- a/src/engine/shared/config_variables_foxnet.h +++ b/src/engine/shared/config_variables_foxnet.h @@ -152,6 +152,21 @@ MACRO_CONFIG_INT(SvExecBasedOnPort, sv_execute_based_on_port, 0, 0, 1, CFGFLAG_S MACRO_CONFIG_INT(SvTeeCursor, sv_tee_cursor, 0, 0, 1, CFGFLAG_SERVER, "Display everyones position at their cursor") MACRO_CONFIG_INT(SvNoVel, sv_no_vel, 0, 0, 1, CFGFLAG_SERVER, "Set everyones snapping velocity to 0 (disables interpolation on the client)") + +// Hide and Seek +MACRO_CONFIG_INT(SvMinigamesSameIp, sv_minigames_same_ip, 0, 0, 1, CFGFLAG_SERVER, "Whether to allow the same ip in minigames") + + +MACRO_CONFIG_INT(SvHideSeekGiveXp, sv_hide_seek_give_xp, 1, 0, 1, CFGFLAG_SERVER, "Whether the minigame should give xp on game finish") +MACRO_CONFIG_INT(SvHideSeekWarmupTime, sv_hide_seek_warmup_time, 15, 1, 60, CFGFLAG_SERVER, "Delay in seconds before the game starts") +MACRO_CONFIG_INT(SvHideSeekFreezeDuration, sv_hide_seek_freeze_duration, 10, 1, 30, CFGFLAG_SERVER, "How long seekers are frozen on game start") +MACRO_CONFIG_INT(SvHideSeekSeekersTime, sv_hide_seek_seekers_time, 80, 1, 500, CFGFLAG_SERVER, "How much time seekers have to find all hiders") +MACRO_CONFIG_INT(SvHideSeekSeekersGunCooldown, sv_hide_seek_seekers_gun_cooldown, 5000, 1, 10000, CFGFLAG_SERVER, "How long the cooldown for the seekers gun is (like normal weapons)") +MACRO_CONFIG_INT(SvHideSeekSeekersGunFreeze, sv_hide_seek_seekers_gun_freeze, 0, 0, 10000, CFGFLAG_SERVER, "Whether the gun should freeze hit hiders and how long (in ticks, 50 ticks in a second)") +MACRO_CONFIG_INT(SvHideSeekSeekersHammerDelay, sv_hide_seek_seekers_hammer_delay, 275, 1, 10000, CFGFLAG_SERVER, "How long the cooldown for the seekers hammer is (like normal weapons)") +MACRO_CONFIG_INT(SvHideSeekHidersGhostDuration, sv_hide_seek_hiders_ghost_duration, 35, 1, 1000, CFGFLAG_SERVER, "How long hiders remain in ghost mode in second Seconds (30 -> 3 seconds)") +MACRO_CONFIG_INT(SvHideSeekHidersGhostCooldown, sv_hide_seek_hiders_ghost_cooldown, 55, 1, 1000, CFGFLAG_SERVER, "Cooldown time for hiders' ghost mode in second Seconds (30 -> 3 seconds)") + // Scripting // Notes: diff --git a/src/game/gamecore.cpp b/src/game/gamecore.cpp index 1aedee3bd0a..8290c6d3cc3 100644 --- a/src/game/gamecore.cpp +++ b/src/game/gamecore.cpp @@ -518,7 +518,7 @@ void CCharacterCore::TickDeferred() { vec2 Dir = normalize(m_Pos - pCharCore->m_Pos); - bool CanCollide = (m_Super || pCharCore->m_Super) || (!m_CollisionDisabled && !pCharCore->m_CollisionDisabled && Tuning.m_PlayerCollision); + bool CanCollide = (m_Super || pCharCore->m_Super) || (!m_CollisionDisabled && !pCharCore->m_CollisionDisabled && Tuning.m_PlayerCollision && pCharCore->m_Tuning.m_PlayerCollision); // @@ -638,6 +638,9 @@ void CCharacterCore::Move() if(!m_Collidable) continue; + if(!pCharCore->m_Tuning.m_PlayerCollision) + continue; + if(!g_Config.m_SvMultimapAllowInteraction && m_MultiMapIdx != pCharCore->m_MultiMapIdx) continue; // FoxNet> diff --git a/src/game/quad_data.h b/src/game/quad_data.h index 05cb5b17460..f4713bc288b 100644 --- a/src/game/quad_data.h +++ b/src/game/quad_data.h @@ -16,6 +16,7 @@ enum class EZoneType Hookable, Unhookable, Roulette, + HideNSeek, Num, }; diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp index 65db856dd94..5bedc924dcb 100644 --- a/src/game/server/entities/character.cpp +++ b/src/game/server/entities/character.cpp @@ -8,10 +8,11 @@ #include +#include #include #include +#include #include -#include #include #include @@ -28,13 +29,12 @@ #include #include #include -#include #include #include #include #include +#include #include -#include #include #include #include @@ -58,8 +58,9 @@ #include #include +#include +#include #include -#include #include #include #include @@ -336,7 +337,7 @@ void CCharacter::HandleJetpack() { if(m_Core.m_Jetpack) { - float Strength = GetTuning(GetOverriddenTuneZone())->m_JetpackStrength; + float Strength = GetCurrentTuning()->m_JetpackStrength; TakeDamage(Direction * -1.0f * (Strength / 100.0f / 6.11f), 0, m_pPlayer->GetCid(), m_Core.m_ActiveWeapon); } } @@ -381,8 +382,8 @@ void CCharacter::HandleNinja() m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * g_pData->m_Weapons.m_Ninja.m_Velocity; vec2 OldPos = m_Pos; vec2 GroundElasticity = vec2( - GetTuning(GetOverriddenTuneZone())->m_GroundElasticityX, - GetTuning(GetOverriddenTuneZone())->m_GroundElasticityY); + GetCurrentTuning()->m_GroundElasticityX, + GetCurrentTuning()->m_GroundElasticityY); Collision()->MoveBox(&m_Core.m_Pos, &m_Core.m_Vel, vec2(GetProximityRadius(), GetProximityRadius()), GroundElasticity); @@ -582,6 +583,12 @@ void CCharacter::FireWeapon() if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo) return; + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + if(!pComponent->OnCharacterFire(m_pPlayer->GetCid(), m_Core.m_ActiveWeapon)) + return; + } + vec2 ProjStartPos = m_Pos + Direction * GetProximityRadius() * 0.75f; switch(m_Core.m_ActiveWeapon) @@ -600,7 +607,10 @@ void CCharacter::FireWeapon() if(m_Core.m_HammerHitDisabled) break; - float Strength = GetTuning(GetOverriddenTuneZone())->m_HammerStrength; + if(!GetCurrentTuning()->m_PlayerHammering) + break; + + float Strength = GetCurrentTuning()->m_HammerStrength; if(g_Config.m_SvDropsHammerable) GameServer()->OnHammerHit(this, ProjStartPos, Strength); @@ -646,13 +656,16 @@ void CCharacter::FireWeapon() Antibot()->OnHammerHit(m_pPlayer->GetCid(), pTarget->GetPlayer()->GetCid()); + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + pComponent->OnCharacterHammerHit(m_pPlayer->GetCid(), pTarget->GetPlayer()->GetCid()); + Hits++; } // if we Hit anything, we have to wait for the reload if(Hits != 0) { - float FireDelay = GetTuning(GetOverriddenTuneZone())->m_HammerHitFireDelay; + float FireDelay = GetCurrentTuning()->m_HammerHitFireDelay; m_ReloadTimer = FireDelay * Server()->TickSpeed() / 1000; } } @@ -666,7 +679,7 @@ void CCharacter::FireWeapon() case WEAPON_SHOTGUN: { - float LaserReach = GetTuning(GetOverriddenTuneZone())->m_LaserReach; + float LaserReach = GetCurrentTuning()->m_LaserReach; new CLaser(&GameServer()->m_World, MultiMapIdx(), m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_SHOTGUN); GameServer()->CreateSound(m_Pos, SOUND_SHOTGUN_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) @@ -676,7 +689,7 @@ void CCharacter::FireWeapon() case WEAPON_GRENADE: { // m_TeleGrenade) // KoG Compatibility + if(GetCurrentTuning()->m_TeleGrenade) // KoG Compatibility { bool Teleported = false; std::vector vpEntities = GameWorld()->FindEntitiesWithOwner(CGameWorld::ENTTYPE_PROJECTILE, GetPlayer()->GetCid()); @@ -717,7 +730,7 @@ void CCharacter::FireWeapon() } // FoxNet> - int Lifetime = (int)(Server()->TickSpeed() * GetTuning(GetOverriddenTuneZone())->m_GrenadeLifetime); + int Lifetime = (int)(Server()->TickSpeed() * GetCurrentTuning()->m_GrenadeLifetime); new CProjectile( GameWorld(), @@ -739,7 +752,7 @@ void CCharacter::FireWeapon() case WEAPON_LASER: { - float LaserReach = GetTuning(GetOverriddenTuneZone())->m_LaserReach; + float LaserReach = GetCurrentTuning()->m_LaserReach; new CLaser(GameWorld(), MultiMapIdx(), m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_LASER); GameServer()->CreateSound(m_Pos, SOUND_LASER_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) @@ -822,16 +835,16 @@ float CCharacter::GetFireDelay(int Weapon) switch(Weapon) { case WEAPON_NONE: return 0.0f; // shows no weapon - meaning no delay since it does nothing - case WEAPON_HAMMER: return (float)GetTuning(GetOverriddenTuneZone())->m_HammerFireDelay; - case WEAPON_GUN: return (float)GetTuning(GetOverriddenTuneZone())->m_GunFireDelay; - case WEAPON_SHOTGUN: return (float)GetTuning(GetOverriddenTuneZone())->m_ShotgunFireDelay; - case WEAPON_GRENADE: return (float)GetTuning(GetOverriddenTuneZone())->m_GrenadeFireDelay; - case WEAPON_LASER: return (float)GetTuning(GetOverriddenTuneZone())->m_LaserFireDelay; - case WEAPON_NINJA: return (float)GetTuning(GetOverriddenTuneZone())->m_NinjaFireDelay; - case WEAPON_HEARTGUN: return (float)GetTuning(GetOverriddenTuneZone())->m_HeartgunFireDelay; - case WEAPON_TELEKINESIS: return (float)GetTuning(GetOverriddenTuneZone())->m_TelekinesisFireDelay; - case WEAPON_LIGHTSABER: return (float)GetTuning(GetOverriddenTuneZone())->m_LightsaberFireDelay; - case WEAPON_PORTALGUN: return (float)GetTuning(GetOverriddenTuneZone())->m_PortalgunFireDelay; + case WEAPON_HAMMER: return (float)GetCurrentTuning()->m_HammerFireDelay; + case WEAPON_GUN: return (float)GetCurrentTuning()->m_GunFireDelay; + case WEAPON_SHOTGUN: return (float)GetCurrentTuning()->m_ShotgunFireDelay; + case WEAPON_GRENADE: return (float)GetCurrentTuning()->m_GrenadeFireDelay; + case WEAPON_LASER: return (float)GetCurrentTuning()->m_LaserFireDelay; + case WEAPON_NINJA: return (float)GetCurrentTuning()->m_NinjaFireDelay; + case WEAPON_HEARTGUN: return (float)GetCurrentTuning()->m_HeartgunFireDelay; + case WEAPON_TELEKINESIS: return (float)GetCurrentTuning()->m_TelekinesisFireDelay; + case WEAPON_LIGHTSABER: return (float)GetCurrentTuning()->m_LightsaberFireDelay; + case WEAPON_PORTALGUN: return (float)GetCurrentTuning()->m_PortalgunFireDelay; default: dbg_assert(false, "invalid weapon"); return 0.0f; // this value should not be reached } } @@ -1367,6 +1380,10 @@ void CCharacter::SnapCharacter(int SnappingClient, int Id) if(MultiMapIdx() != m_pPlayer->MultiMapIdx() && !g_Config.m_SvMultimapAllowInteraction) Faketuning |= FAKETUNE_SOLO; + + if(!GetCurrentTuning()->m_PlayerHammering) + Faketuning |= FAKETUNE_NOHAMMER; + // FoxNet> } if(Faketuning != m_NeededFaketuning) @@ -1508,12 +1525,12 @@ bool CCharacter::CanSnapCharacter(int SnappingClient) if(pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()) { - if(pSnapPlayer->SpectatorId() != SPEC_FREEVIEW && !CanCollide(pSnapPlayer->SpectatorId()) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(pSnapPlayer->SpectatorId())))) + if(pSnapPlayer->SpectatorId() != SPEC_FREEVIEW && !CanCollide(pSnapPlayer->SpectatorId()) && (pSnapPlayer->GetShowOthers() == SHOW_OTHERS_OFF || (pSnapPlayer->GetShowOthers() == SHOW_OTHERS_ONLY_TEAM && !SameTeam(pSnapPlayer->SpectatorId())))) return false; else if(pSnapPlayer->SpectatorId() == SPEC_FREEVIEW && !CanCollide(SnappingClient) && pSnapPlayer->m_SpecTeam && !SameTeam(SnappingClient)) return false; } - else if(pSnapChar && !pSnapChar->m_Core.m_Super && !CanCollide(SnappingClient) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(SnappingClient)))) + else if(pSnapChar && !pSnapChar->m_Core.m_Super && !CanCollide(SnappingClient) && (pSnapPlayer->GetShowOthers() == SHOW_OTHERS_OFF || (pSnapPlayer->GetShowOthers() == SHOW_OTHERS_ONLY_TEAM && !SameTeam(SnappingClient)))) return false; return true; @@ -1567,6 +1584,12 @@ void CCharacter::Snap(int SnappingClient) if(!GameServer()->m_apPlayers[SnappingClient]->m_Invisible && Server()->GetAuthedState(SnappingClient) < AUTHED_MOD) return; + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + if(!pComponent->CanSnapCharacter(this, SnappingClient)) + return; + } + // Multimap { CPlayer *SnapPlayer = (SnappingClient >= 0 && SnappingClient < MAX_CLIENTS) ? GameServer()->m_apPlayers[SnappingClient] : nullptr; @@ -1594,9 +1617,9 @@ void CCharacter::Snap(int SnappingClient) pDDNetCharacter->m_Flags |= CHARACTERFLAG_INVINCIBLE; if(m_Core.m_EndlessHook) pDDNetCharacter->m_Flags |= CHARACTERFLAG_ENDLESS_HOOK; - if(m_Core.m_CollisionDisabled || !GetTuning(GetOverriddenTuneZone())->m_PlayerCollision) + if(m_Core.m_CollisionDisabled || !GetCurrentTuning()->m_PlayerCollision) pDDNetCharacter->m_Flags |= CHARACTERFLAG_COLLISION_DISABLED; - if(m_Core.m_HookHitDisabled || !GetTuning(GetOverriddenTuneZone())->m_PlayerHooking) + if(m_Core.m_HookHitDisabled || !GetCurrentTuning()->m_PlayerHooking) pDDNetCharacter->m_Flags |= CHARACTERFLAG_HOOK_HIT_DISABLED; if(m_Core.m_EndlessJump) pDDNetCharacter->m_Flags |= CHARACTERFLAG_ENDLESS_JUMP; @@ -1730,6 +1753,8 @@ void CCharacter::Snap(int SnappingClient) } if(!m_Core.m_Collidable && Id == SnappingClient) pDDNetCharacter->m_Flags |= CHARACTERFLAG_COLLISION_DISABLED; + if(!GetCurrentTuning()->m_PlayerHammering) + pDDNetCharacter->m_Flags |= CHARACTERFLAG_HAMMER_HIT_DISABLED; // FoxNet> } @@ -1941,8 +1966,8 @@ void CCharacter::HandleSkippableTiles(int Index) constexpr float MaxSpeedScale = 5.0f; if(MaxSpeed == 0) { - float MaxRampSpeed = GetTuning(GetOverriddenTuneZone())->m_VelrampRange / (50 * log(maximum((float)GetTuning(GetOverriddenTuneZone())->m_VelrampCurvature, 1.01f))); - MaxSpeed = maximum(MaxRampSpeed, GetTuning(GetOverriddenTuneZone())->m_VelrampStart / 50) * MaxSpeedScale; + float MaxRampSpeed = GetCurrentTuning()->m_VelrampRange / (50 * log(maximum((float)GetCurrentTuning()->m_VelrampCurvature, 1.01f))); + MaxSpeed = maximum(MaxRampSpeed, GetCurrentTuning()->m_VelrampStart / 50) * MaxSpeedScale; } // (signed) length of projection @@ -2760,15 +2785,21 @@ void CCharacter::DDRacePostCoreTick() HandleBroadcast(); } +bool CCharacter::FreezeForce(int Ticks) +{ + m_Armor = 0; + m_FreezeTime = Ticks; + m_Core.m_FreezeStart = Server()->Tick(); + return true; +} + bool CCharacter::Freeze(int Seconds) { if(Seconds <= 0 || m_Core.m_Super || m_Core.m_Invincible || m_FreezeTime > Seconds * Server()->TickSpeed()) return false; if(m_FreezeTime == 0 || m_Core.m_FreezeStart < Server()->Tick() - Server()->TickSpeed()) { - m_Armor = 0; - m_FreezeTime = Seconds * Server()->TickSpeed(); - m_Core.m_FreezeStart = Server()->Tick(); + FreezeForce(Seconds * Server()->TickSpeed()); return true; } return false; @@ -3043,6 +3074,9 @@ CAccountSession *CCharacter::Acc() void CCharacter::OnDie(int Killer, int Weapon, bool SendKillMsg) { + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + pComponent->OnCharacterDie(GetPlayer()->GetCid(), Killer, Weapon, SendKillMsg); + if(!Server()->IsRconAuthed(GetPlayer()->GetCid())) GameServer()->UnsetTelekinesis(GetPlayer()->GetCid()); @@ -3126,7 +3160,7 @@ void CCharacter::FoxNetTick() vec2 CursorPos = GetCursorPos(); vec2 HookPos = m_Core.m_HookPos; - float HookLength = abs(GetTuning(GetOverriddenTuneZone())->m_HookLength - 20.0f); + float HookLength = abs(GetCurrentTuning()->m_HookLength - 20.0f); HookLength = std::clamp(distance(CursorPos, Pos), -HookLength, HookLength); bool NoHit = m_Core.m_HookState == HOOK_RETRACT_START || (HookLength < distance(HookPos, Pos) && m_Core.m_HookState == HOOK_FLYING); @@ -3166,6 +3200,8 @@ void CCharacter::FoxNetSpawn() void CCharacter::RouletteTileHandle() { + if(Team() != TEAM_FLOCK) + return; if(!IsAlive()) return; if(GetPlayer()->m_Area != EArea::Roulette) @@ -3256,7 +3292,7 @@ void CCharacter::DoGunFire(vec2 ProjStartPos, vec2 Direction, vec2 MouseTarget) { if(!m_Core.m_Jetpack || !m_pPlayer->m_NinjaJetpack || m_Core.m_HasTelegunGun) { - int Lifetime = (int)(Server()->TickSpeed() * GetTuning(GetOverriddenTuneZone())->m_GunLifetime); + int Lifetime = (int)(Server()->TickSpeed() * GetCurrentTuning()->m_GunLifetime); new CProjectile( GameWorld(), @@ -3301,7 +3337,7 @@ void CCharacter::DoTelekinesis() if(pChr && pChr->m_TelekinesisId == pClosest->GetPlayer()->GetCid()) return; // already telekinesis } - if(GetPlayer()->m_ShowOthers != SHOW_OTHERS_ON) + if(GetPlayer()->GetShowOthers() != SHOW_OTHERS_ON) { if(!Teams()->m_Core.SameTeam(GetPlayer()->GetCid(), pClosest->GetPlayer()->GetCid()) && Team() != TEAM_SUPER) return; // not same team @@ -3328,7 +3364,7 @@ vec2 CCharacter::GetCursorPos() void CCharacter::SetTuneOverride(int pZone) { - int Zone = std::clamp(pZone, -1, 255); + int Zone = std::clamp(pZone, -1, TuneZone::NUM - 1); if(Zone == 0) Zone = -1; m_TuneZoneOverride = Zone; @@ -3507,7 +3543,7 @@ void CCharacter::UpdateWeaponIndicator() m_LastWeaponIndTick = Server()->Tick(); } -bool CCharacter::CanDropWeapon(int Type) const +bool CCharacter::CanDropWeapon(int Type) { if(!g_Config.m_SvAllowWeaponDrops) return false; @@ -3524,6 +3560,9 @@ bool CCharacter::CanDropWeapon(int Type) const if(Type == WEAPON_NINJA) return false; + if(GetPlayer()->m_Area != EArea::Game) + return false; + return true; } @@ -3532,6 +3571,12 @@ void CCharacter::DropWeapon(int Type, vec2 Dir, bool Death) if(!CanDropWeapon(Type)) return; + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + if(!pComponent->CanDropWeapon(this, Type)) + return; + } + int Lifetime = 300; if(Type <= WEAPON_GUN) Lifetime = 120; diff --git a/src/game/server/entities/character.h b/src/game/server/entities/character.h index ffc6fcf8660..1b91a35c44d 100644 --- a/src/game/server/entities/character.h +++ b/src/game/server/entities/character.h @@ -115,6 +115,7 @@ class CCharacter : public CEntity int NeededFaketuning() const { return m_NeededFaketuning; } bool IsAlive() const { return m_Alive; } + // /spec bool IsPaused() const { return m_Paused; } class CPlayer *GetPlayer() { return m_pPlayer; } CClientMask TeamMask(); @@ -217,6 +218,8 @@ class CCharacter : public CEntity void FillAntibot(CAntibotCharacterData *pData); void Pause(bool Pause); + // FoxNet + bool FreezeForce(int Seconds); bool Freeze(int Seconds); bool Freeze(); bool Unfreeze(); @@ -224,6 +227,7 @@ class CCharacter : public CEntity void ResetPickups(); void ResetJumps(); ERaceState m_DDRaceState; + // DDRace Team int Team(); bool CanCollide(int ClientId) override; bool SameTeam(int ClientId); @@ -306,6 +310,7 @@ class CCharacter : public CEntity CSaveTee &GetLastRescueTeeRef(int Mode = RESCUEMODE_AUTO) { return m_RescueTee[Mode]; } CTuningParams *GetTuning(int Zone) { return &TuningList()[Zone]; } + CTuningParams *GetCurrentTuning() { return GetTuning(GetOverriddenTuneZone()); } // MultiMapIdx(); } CCollision *Collision() override; -private: void DoGunFire(vec2 ProjStartPos, vec2 Direction, vec2 MouseTarget); + float GetFireDelay(int Weapon); + +private: void RouletteTileHandle(); - bool CanDropWeapon(int Type) const; + bool CanDropWeapon(int Type); vec2 m_HookBasePos = vec2(0, 0); void OnPlayerHook(); - float GetFireDelay(int Weapon); int m_VoteActionDelay; diff --git a/src/game/server/entities/projectile.cpp b/src/game/server/entities/projectile.cpp index bd3685db5ce..a3546d777be 100644 --- a/src/game/server/entities/projectile.cpp +++ b/src/game/server/entities/projectile.cpp @@ -30,7 +30,7 @@ CProjectile::CProjectile( vec2 Pos, vec2 Dir, int Span, - bool Freeze, + int FreezeTicks, bool Explosive, int SoundImpact, vec2 InitDir, @@ -49,7 +49,7 @@ CProjectile::CProjectile( m_Layer = Layer; m_Number = Number; - m_Freeze = Freeze; + m_FreezeTicks = FreezeTicks; m_InitDir = InitDir; // m_TuneZone = Collision()->IsTune(Collision()->GetMapIndex(m_Pos)); @@ -145,7 +145,7 @@ void CProjectile::Tick() CCharacter *pTargetChr = nullptr; if(pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit) - pTargetChr = GameServer()->m_World.IntersectCharacter(PrevPos, ColPos, m_Freeze ? 1.0f : 6.0f, ColPos, pOwnerChar, m_Owner); + pTargetChr = GameServer()->m_World.IntersectCharacter(PrevPos, ColPos, m_FreezeTicks ? 1.0f : 6.0f, ColPos, pOwnerChar, m_Owner); // Core()->m_Passive) || (pOwnerChar && pOwnerChar->Core()->m_Passive)) @@ -187,7 +187,7 @@ void CProjectile::Tick() if(((pTargetChr && (pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit || m_Owner == -1 || pTargetChr == pOwnerChar)) || Collide || GLClipped) && !IsWeaponCollide) { - if(m_Explosive /*??*/ && (!pTargetChr || (pTargetChr && (!m_Freeze || (m_Type == WEAPON_SHOTGUN && Collide))))) + if(m_Explosive /*??*/ && (!pTargetChr || (pTargetChr && (m_FreezeTicks <= 0 || (m_Type == WEAPON_SHOTGUN && Collide))))) { int Number = 1; if(GameServer()->EmulateBug(BUG_GRENADE_DOUBLEEXPLOSION) && m_LifeSpan == -1) @@ -202,7 +202,7 @@ void CProjectile::Tick() (m_Owner != -1) ? TeamMask : CClientMask().set()); } } - else if(m_Freeze) + else if(m_FreezeTicks > 0) { CEntity *apEnts[MAX_CLIENTS]; int Num = GameWorld()->FindEntities(CurPos, 1.0f, apEnts, MAX_CLIENTS, CGameWorld::ENTTYPE_CHARACTER, MultiMapIdx()); @@ -210,7 +210,7 @@ void CProjectile::Tick() { auto *pChr = static_cast(apEnts[i]); if(pChr && (m_Layer != LAYER_SWITCH || (m_Layer == LAYER_SWITCH && m_Number > 0 && Switchers()[m_Number].m_aStatus[pChr->Team()]))) - pChr->Freeze(); + pChr->FreezeForce(m_FreezeTicks); } } else if(pTargetChr) @@ -278,7 +278,7 @@ void CProjectile::Tick() } else { - if(!m_Freeze) + if(m_FreezeTicks <= 0) { m_MarkedForDestroy = true; return; @@ -538,7 +538,7 @@ bool CProjectile::FillExtraInfoLegacy(CNetObj_DDRaceProjectile *pProj) Data |= (m_Bouncing & 3) << 10; if(m_Explosive) Data |= LEGACYPROJECTILEFLAG_EXPLOSIVE; - if(m_Freeze) + if(m_FreezeTicks > 0) Data |= LEGACYPROJECTILEFLAG_FREEZE; pProj->m_X = (int)(m_Pos.x * 100.0f); @@ -565,7 +565,7 @@ void CProjectile::FillExtraInfo(CNetObj_DDNetProjectile *pProj) { Flags |= PROJECTILEFLAG_EXPLOSIVE; } - if(m_Freeze) + if(m_FreezeTicks > 0) { Flags |= PROJECTILEFLAG_FREEZE; } diff --git a/src/game/server/entities/projectile.h b/src/game/server/entities/projectile.h index 509297d72a6..6dbc807609e 100644 --- a/src/game/server/entities/projectile.h +++ b/src/game/server/entities/projectile.h @@ -19,7 +19,7 @@ class CProjectile : public CEntity { public: CProjectile(CGameWorld *pGameWorld, int MultiMapIdx, int Type, int Owner, vec2 Pos, vec2 Dir, - int Span, bool Freeze, bool Explosive, int SoundImpact, vec2 InitDir, int Layer = 0, int Number = 0); + int Span, int FreezeTicks, bool Explosive, int SoundImpact, vec2 InitDir, int Layer = 0, int Number = 0); vec2 GetPos(float Time, int ClientId = -1); void FillInfo(CNetObj_Projectile *pProj); @@ -50,7 +50,7 @@ class CProjectile : public CEntity // DDRace int m_Bouncing; - bool m_Freeze; + int m_FreezeTicks; int m_TuneZone; bool m_BelongsToPracticeTeam; int m_DDRaceTeam; diff --git a/src/game/server/foxnet/commands.cpp b/src/game/server/foxnet/commands.cpp index 3b35be20558..aab34d3f768 100644 --- a/src/game/server/foxnet/commands.cpp +++ b/src/game/server/foxnet/commands.cpp @@ -1637,6 +1637,12 @@ void CGameContext::ConSendAsPlayer(IConsole::IResult *pResult, void *pUserData) pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now; pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4; + for(CServerComponent *pComponent : pSelf->m_vpComponents) + { + if(!pComponent->CanUseCommand(pPlayer, pText + 1)) + return; + } + pSelf->Console()->SetFlagMask(CFGFLAG_CHAT); pSelf->Console()->ExecuteLine(pText + 1, ClientId, false); diff --git a/src/game/server/foxnet/component.h b/src/game/server/foxnet/component.h index 398111c8ddc..1085fd45540 100644 --- a/src/game/server/foxnet/component.h +++ b/src/game/server/foxnet/component.h @@ -3,6 +3,8 @@ #include +#include + #include class CGameContext; @@ -44,14 +46,92 @@ class CServerComponent */ virtual void OnMapUnload(size_t MapIdx) {} + /* + * Called when the console initializes + */ virtual void OnConsoleInit() {} + /* + * Called after When the main map gets loaded + */ virtual void OnInit() {} + /* + * Called when the server is shutting down, pPersistentData is a pointer to a struct that can be used to store data that should persist through the shutdown and the next init, for example for map rotation in map voting + */ virtual void OnShutdown(void *pPersistentData) {} + /* + * Called every tick + */ virtual void OnTick() {} + /* + * Called every snap + */ virtual void OnSnap(int ClientId, bool GlobalSnap, bool RecordingDemo) {} + /* + * Used to override Game Info Snapdata + */ + virtual void OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) {} virtual void OnPostGlobalSnap() {} + /* + * Called when a client connects + */ virtual void OnClientEnter(int ClientId) {} + /* + * Called when a client disconnects + */ virtual void OnClientDrop(int ClientId, const char *pReason) {} + /* + * Called when CPlayer::m_ShowOthers is accesses anywhere + * retuning -1 will just return m_ShowOthers + * returning anything else overrides m_ShowOthers for the player + */ + virtual int ShowOthers(CPlayer *pPlayer) { return -1; } + /* + * Called when a client tries to execute a chat command, return false to prevent the command from being executed + */ + virtual bool CanUseCommand(CPlayer *pPlayer, const char *pCommand) { return true; } + // Minigame specific hooks + /* + * Called when a player tries to spectate someone + * + * @param pPlayer the player that tries to spectate + * @param pTarget the wanted spectate target + */ + virtual bool CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) { return true; } + /* + * Called when a character is snapped, return false to prevent it from being snapped for the client + * + * @param pChr the character that is being snapped + * @param SnappingClient the client that is snapping this character + */ + virtual bool CanSnapCharacter(CCharacter *pChr, int SnappingClient) { return true; } + /* + * Called when a player tries to drop a weapon, return false to prevent dropping the weapon + */ + virtual bool CanDropWeapon(CCharacter *pChr, int Weapon) { return true; } + /* + * Called when a character spawns, Pos is the spawn position + */ + virtual void OnCharacterSpawn(int ClientId, vec2 Pos) {} + /* + * Called when a character dies + */ + virtual void OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) {} + /* + * Called when a character fires a weapon, return false to prevent firing the weapon + */ + virtual bool OnCharacterFire(int ClientId, int Weapon) { return true; } + /* + * Called when a character hits someone with the hammer + */ + virtual void OnCharacterHammerHit(int ClientId, int Target) {} + /* + * Called when a Mask is being set for a client, return false to prevent the mask from being set + */ + virtual bool SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) { return true; } + /* + * Used to override player snap data + */ + virtual void OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) {} }; #endif // GAME_SERVER_FOXNET_COMPONENT_H diff --git a/src/game/server/foxnet/components/zones/hidenseek.cpp b/src/game/server/foxnet/components/zones/hidenseek.cpp new file mode 100644 index 00000000000..281297402b4 --- /dev/null +++ b/src/game/server/foxnet/components/zones/hidenseek.cpp @@ -0,0 +1,1125 @@ +#include "hidenseek.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ToDo @qxdFox: +// ~~Dont allow weapon drops~~ +// ~~give players gun and hammer~~ when hide and seek starts and reset to their old state after + +// Add foxnet_accounts_stats to save hide and seek stats and future gamemode data? +// playtest with multiple people +// ~~^ Seekers can see "ghost" skin in scoreboard when hider goes ghost~~ +// ~~^ new joining players arent dead if game is running~~ + +// Feedback: +// add infection +// allow hiders to pass trough eachother? +// ~~pistol should freeze if it touches player~~ + + +// ~~if seeker doesnt move for too long, choose new one (or end game if its a 1v1)~~ + +constexpr static float MaxSpawnPointOffset = 16.0f; +constexpr static float MaxAfkSeconds = 30.0f; // seconds + +static int TimeToTicks(int TenthsOfSeconds, int TickSpeed) +{ + return TenthsOfSeconds * TickSpeed / 10; +} + +static std::string MakeHudBar(const char *pLabel, int Current, int Maximum, int Width = 12) +{ + Maximum = std::max(Maximum, 1); + Current = std::clamp(Current, 0, Maximum); + + const int Filled = std::clamp(Current * Width / Maximum, 0, Width); + + std::string Bar; + Bar.reserve(str_length(pLabel) + Width + 8); + Bar.append(pLabel); + Bar.append(": ["); + Bar.append(Filled, ':'); + Bar.append(Width - Filled, ' '); + Bar.append("]"); + + return Bar; +} + +void CHideAndSeekZone::OnTick() +{ + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) + ClientTick(ClientId); + + int NumPlayers = GetNumCandidates(); + + if(m_State == EState::BadMap) + { + for(CCharacter *pChr : m_vCandidates) + pChr->GetPlayer()->SendBroadcast("Hide and Seek cannot be started on this map."); + return; + } + + int NumSeekers = 0; + for(CCharacter *pChr : m_vCandidates) + { + if(m_aClientData[pChr->GetPlayer()->GetCid()].m_IsSeeker) + NumSeekers++; + } + + if(NumPlayers < 2 && m_State != EState::WaitingForPlayers) + { + EndGame(EWinState::None); + m_State = EState::WaitingForPlayers; + } + + if(m_State == EState::WaitingForPlayers) + { + if(NumPlayers < 2) + { + for(CCharacter *pChr : m_vCandidates) + pChr->GetPlayer()->SendBroadcast("Not enough players to start hide and seek."); + return; + } + else + { + m_State = EState::Warmup; + } + } + else if(m_State == EState::Warmup) + { + m_WarmUpTime = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvHideSeekWarmupTime; + m_State = EState::DoWarmup; + } + else if(m_State == EState::DoWarmup) + { + if(Server()->Tick() < m_WarmUpTime) + { + for(CCharacter *pChr : m_vCandidates) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "Hide and Seek starts in %d seconds", (int)((m_WarmUpTime - Server()->Tick()) / Server()->TickSpeed())); + pChr->GetPlayer()->SendBroadcast(aBuf); + } + } + else + { + m_State = EState::Playing; + StartGame(); + } + } + else if(m_State == EState::Playing) + { + if(NumSeekers == 0) + { + EndGame(EWinState::None); + m_State = EState::WaitingForPlayers; + return; + } + + if(m_SeekTimeRemaining > 0) + { + m_SeekTimeRemaining--; + } + else + { + EndGame(EWinState::Hiders); // Seek time is over, end the game + m_State = EState::Finished; + m_FinishedDelay = Server()->Tick() + Server()->TickSpeed() * 5; + return; // Don't check for win conditions anymore + } + + int NumHiders = 0; + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) + { + if(IsCandidate(ClientId) && !m_aClientData[ClientId].m_IsSeeker && m_aClientData[ClientId].m_Alive) + NumHiders++; + } + if(NumHiders == 0) + { + EndGame(EWinState::Seeker); + m_State = EState::Finished; + m_FinishedDelay = Server()->Tick() + Server()->TickSpeed() * 5; + } + } + else if(m_State == EState::Finished) + { + if(Server()->Tick() > m_FinishedDelay) + { + m_State = EState::WaitingForPlayers; + for(CCharacter *pChr : m_vCandidates) + { + if(pChr->GetPlayer()->m_Area != EArea::HideAndSeek) + continue; + + vec2 SpawnPos = GetRandomSpawnPos(); + pChr->ForceSetPos(SpawnPos); + } + } + } +} + +void CHideAndSeekZone::OnClientDrop(int ClientId, const char *pReason) +{ + CClientData &Data = m_aClientData[ClientId]; + Data.Reset(); + Data.m_MarkedAfk = false; + Data.m_NumWins = 0; // Reset wins, wins should get saved in foxnet_accounts_stats when implemented +} + +void CHideAndSeekZone::OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) +{ + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; + if(!pPlayer) + return; + + if(pPlayer->m_Area != EArea::HideAndSeek) + return; + + pGameInfoEx->m_Flags &= ~GAMEINFOFLAG_TIMESCORE; + + if(m_State == EState::Playing) + { + if(m_aClientData[ClientId].m_Alive) + pGameInfoEx->m_Flags &= ~GAMEINFOFLAG_ALLOW_ZOOM; + + if(m_aClientData[ClientId].m_IsSeeker || !m_aClientData[ClientId].m_Alive) + { + pGameInfoObj->m_TimeLimit = m_GameInfoTimeLimit; + pGameInfoObj->m_RoundStartTick = m_GameInfoRoundStartTick; + pGameInfoObj->m_WarmupTimer = 0; + pGameInfoObj->m_GameStateFlags &= ~GAMESTATEFLAG_RACETIME; + } + } + else if(m_State == EState::Finished && !m_aClientData[ClientId].m_Alive) + { + pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_GAMEOVER; + } +} + +void CHideAndSeekZone::ClientTick(int ClientId) +{ + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; + if(!pPlayer || !pPlayer->GetCharacter()) + return; + if(pPlayer->MultiMapIdx() != (int)MultiMapIndex()) + return; + CCharacter *pChr = pPlayer->GetCharacter(); + if(!pChr->IsAlive()) + return; + + CClientData &Data = m_aClientData[ClientId]; + + bool InArea = false; + Data.m_InHiddenZone = false; + for(const CQuadData &QuadData : Quads()) + { + if(QuadData.m_SubType == (uint8_t)ESubType::Area) + { + if(InArea) + continue; + + vec2 Points[4] = {QuadData.m_Pos[0], QuadData.m_Pos[1], QuadData.m_Pos[3], QuadData.m_Pos[2]}; + if(InsideQuad(pChr->GetPos(), Points, vec2(0, 0))) + { + // check ip + if(!g_Config.m_SvMinigamesSameIp) + { + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(i == ClientId) + continue; + CPlayer *pOtherPlayer = GameServer()->m_apPlayers[i]; + if(!pOtherPlayer || !pOtherPlayer->GetCharacter()) + continue; + if(pOtherPlayer->MultiMapIdx() != (int)MultiMapIndex()) + continue; + + if(pOtherPlayer->m_Area != EArea::HideAndSeek) + continue; + + // compare addresses + if(net_addr_comp_noport(Server()->ClientAddr(ClientId), Server()->ClientAddr(i)) == 0) + { + pPlayer->SendChat("A player with the same ip is already in that zone."); + pPlayer->KillCharacter(); + return; + } + } + } + InArea = true; + } + } + else if(QuadData.m_SubType == (uint8_t)ESubType::Hidden) + { + if(Data.m_InHiddenZone) + continue; + + vec2 Points[4] = {QuadData.m_Pos[0], QuadData.m_Pos[1], QuadData.m_Pos[3], QuadData.m_Pos[2]}; + if(InsideQuad(pChr->GetPos(), Points, vec2(0, 0))) + Data.m_InHiddenZone = true; + } + } + + if(InArea) + { + if(pPlayer->m_Area != EArea::HideAndSeek) + { + pPlayer->SetArea(EArea::HideAndSeek); + Data.m_JoinedAt = Server()->Tick(); + Data.m_LastMovement = Server()->Tick(); + pChr->SetTuneOverride(-1); + pChr->SetSolo(false); + } + } + else + { + if(pPlayer->m_Area == EArea::HideAndSeek) + { + pPlayer->SetArea(EArea::Game); + pChr->SetTuneOverride(-1); + pChr->SetSolo(false); + } + } + + if(pChr->m_Pos != pChr->m_PrevPos) + { + Data.m_LastMovement = Server()->Tick(); + Data.m_MarkedAfk = false; + if(m_State != EState::Playing) + pChr->SetSolo(false); + } + + if(m_State == EState::Playing) + { + if(Data.m_JoinedAt == Server()->Tick()) + SetDead(ClientId, false); + if(pPlayer->IsAfk()) + SetDead(ClientId, false); + + if(!Data.m_Alive) + return; + + if(Data.m_IsSeeker && Server()->Tick() - Data.m_LastMovement > MaxAfkSeconds * Server()->TickSpeed()) + { + TryReplaceAfkSeeker(ClientId); + return; + } + + std::vector Messages; + std::string Msg = ""; + + if(!Data.m_IsSeeker) + { + // If the player is currently a ghost, we don't want to decrease the cooldown until the timeout is over + if(Data.m_GhostCooldown > 0 && Data.m_GhostDuration <= 0) + Data.m_GhostCooldown--; + + Data.m_NumHiddenTicks++; + + if(Data.m_GhostDuration > 0) + { + Data.m_GhostDuration--; + if(Data.m_GhostDuration == 0) + pChr->SetTuneOverride(m_HiderTuneZone); + } + else + { + Data.m_LastKnownPos = pChr->GetPos(); + } + + const int GhostCooldownTicks = TimeToTicks(g_Config.m_SvHideSeekHidersGhostCooldown, Server()->TickSpeed()); + const int GhostDurationTicks = TimeToTicks(g_Config.m_SvHideSeekHidersGhostDuration, Server()->TickSpeed()); + + if(Data.m_GhostDuration > 0) + { + Messages.push_back(MakeHudBar("Ghost", Data.m_GhostDuration, GhostDurationTicks)); + } + else + { + const int GhostCharge = GhostCooldownTicks - Data.m_GhostCooldown; + Msg = MakeHudBar("Ghost", GhostCharge, GhostCooldownTicks); + //if(Data.m_GhostCooldown <= 0) + // Msg += " ✓"; + Messages.push_back(Msg); + } + } + else + { + if(Data.m_GunReloadTimer > 0) + Data.m_GunReloadTimer--; + + const int GunCooldownTicks = g_Config.m_SvHideSeekSeekersGunCooldown * Server()->TickSpeed() / 1000; + + if(Data.m_GunReloadTimer > 0) + { + const int ReloadCharge = GunCooldownTicks - Data.m_GunReloadTimer; + Messages.push_back(MakeHudBar("Reload", ReloadCharge, GunCooldownTicks)); + } + else + { + Msg = MakeHudBar("Reload", GunCooldownTicks, GunCooldownTicks); + Messages.push_back(Msg); + } + } + + if(!Messages.empty()) + pPlayer->SendBroadcastHud(Messages, 0); + } + else if(m_State == EState::Finished && IsCandidate(ClientId)) + { + // Only freeze players who were actually part of the game (joined before the game ended) + if(!Data.m_Alive) + { + pChr->SetVelocity(vec2(0, 0)); + pChr->ResetHook(); + pChr->ForceSetPos(pChr->m_PrevPos); + } + } +} + +void CHideAndSeekZone::StartGame() +{ + if(!InitTuning()) + { + m_State = EState::BadMap; + return; + } + + for(CCharacter *pChr : m_vCandidates) + { + int ClientId = pChr->GetPlayer()->GetCid(); + m_aClientData[ClientId].m_Alive = true; + m_aClientData[ClientId].m_IsSeeker = false; + m_aClientData[ClientId].m_LastMovement = Server()->Tick(); + pChr->GetPlayer()->ClearBroadcast(); + pChr->GetPlayer()->Pause(CPlayer::PAUSE_NONE, true); + pChr->SetSolo(false); + pChr->GiveWeapon(WEAPON_GUN); + pChr->GiveWeapon(WEAPON_HAMMER); + } + + const int NumCandidates = (int)m_vCandidates.size(); + const int DesiredSeekers = std::min(NumCandidates, std::max(1, (NumCandidates + 3) / 4)); + + std::vector vCandidateIds; + vCandidateIds.reserve(m_vCandidates.size()); + for(CCharacter *pChr : m_vCandidates) + vCandidateIds.push_back(pChr->GetPlayer()->GetCid()); + + std::shuffle(vCandidateIds.begin(), vCandidateIds.end(), Rng()); + + for(int i = 0; i < DesiredSeekers; i++) + m_aClientData[vCandidateIds[i]].m_IsSeeker = true; + + const int NumSeekers = DesiredSeekers; + const int NumHiders = NumCandidates - NumSeekers; + + // move players to spawn points + for(CCharacter *pChr : m_vCandidates) + { + int ClientId = pChr->GetPlayer()->GetCid(); + vec2 SpawnPos = GetRandomSpawnPos(); + pChr->ForceSetPos(SpawnPos); + pChr->SetVelocity(vec2(0, 0)); + pChr->ResetHook(); + + if(m_aClientData[ClientId].m_IsSeeker) + { + pChr->FreezeForce(g_Config.m_SvHideSeekFreezeDuration * Server()->TickSpeed()); + pChr->SetTuneOverride(m_SeekerTuneZone); + } + else + { + pChr->SetTuneOverride(m_HiderTuneZone); + } + } + + // Set Seeker Time based on number of players + int BaseTime = (g_Config.m_SvHideSeekSeekersTime + g_Config.m_SvHideSeekFreezeDuration); + + int Ticks = (BaseTime + ((NumHiders / NumSeekers) * 5)) * Server()->TickSpeed(); + m_SeekTimeTotal = Ticks; + m_SeekTimeRemaining = Ticks; + + const int SeekTimeSeconds = maximum(0, m_SeekTimeTotal / Server()->TickSpeed()); + m_GameInfoTimeLimit = maximum(1, (SeekTimeSeconds + 59) / 60); + + const int ElapsedSeconds = m_GameInfoTimeLimit * 60 - SeekTimeSeconds; + m_GameInfoRoundStartTick = Server()->Tick() - ElapsedSeconds * Server()->TickSpeed(); +} + +void CHideAndSeekZone::EndGame(EWinState WinState) +{ + int NumSeekers = 0; + int NumHiders = 0; + for (CCharacter *pChr : m_vCandidates) + { + if(m_aClientData[pChr->GetPlayer()->GetCid()].m_IsSeeker) + NumSeekers++; + else + NumHiders++; + } + + for(CCharacter *pChr : m_vCandidates) + { + CClientData &Data = m_aClientData[pChr->GetPlayer()->GetCid()]; + + CPlayer *pPlayer = pChr->GetPlayer(); + if(WinState == EWinState::None) + { + pPlayer->SendChat("Game ended prematurely"); + } + else if(WinState == EWinState::Hiders) + { + if(NumHiders != 1) + pPlayer->SendChat("The Hiders won the game!"); + else + pPlayer->SendChatFmt("'%s' won the game!", Server()->ClientName(pPlayer->GetCid())); + + // Xp for hiders gets given based on how long they werent hidden for + if(!Data.m_IsSeeker) + { + if(g_Config.m_SvHideSeekGiveXp) + { + int NumSeekingSeconds = m_SeekTimeTotal / (float)Server()->TickSpeed(); + int HiddenSeconds = Data.m_NumHiddenTicks / (float)Server()->TickSpeed(); + + int a = NumSeekingSeconds / std::max(HiddenSeconds, 1); + int b = std::clamp(a, 1, 10); + + pPlayer->GiveXP(5 * b, "", false); // Give XP to the hiders + } + Data.m_NumWins++; + } + } + else + { + if(NumSeekers != 1) + pPlayer->SendChat("The Seeker won the game!"); + else + pPlayer->SendChatFmt("'%s' won the game!", Server()->ClientName(pPlayer->GetCid())); + if(Data.m_IsSeeker) + { + if(g_Config.m_SvHideSeekGiveXp) + { + if(m_SeekTimeRemaining > m_SeekTimeTotal * 0.5f) + pPlayer->GiveXP(5 + Data.m_NumKills * 7, "", false); + } + Data.m_NumWins++; + } + } + + pChr->SetTuneOverride(-1); + pChr->SetSolo(false); + if(Data.m_IsSeeker) + pChr->Unfreeze(); + pPlayer->ClearBroadcast(); + Data.Reset(); + } + + m_GameInfoTimeLimit = 0; + m_GameInfoRoundStartTick = 0; + + if(m_SeekerTuneZone > 0) + { + GameServer()->TuningList(MultiMapIndex())[m_SeekerTuneZone] = CTuningParams::DEFAULT; + m_SeekerTuneZone = -1; + } + if(m_HiderTuneZone > 0) + { + GameServer()->TuningList(MultiMapIndex())[m_HiderTuneZone] = CTuningParams::DEFAULT; + m_HiderTuneZone = -1; + } + if(m_GhostTuneZone > 0) + { + GameServer()->TuningList(MultiMapIndex())[m_GhostTuneZone] = CTuningParams::DEFAULT; + m_GhostTuneZone = -1; + } +} + +bool CHideAndSeekZone::InitTuning() +{ + const CTuningParams &BaseTuning = GameServer()->DDNetDefaultTuning(); + + // Find 3 free tuning zones. Zone 0 is the global tuning slot. + for(int i = 1; i < TuneZone::NUM; i++) + { + if(mem_comp(&BaseTuning, &GameServer()->TuningList(MultiMapIndex())[i], sizeof(CTuningParams)) == 0) + { + if(m_SeekerTuneZone == -1) + m_SeekerTuneZone = i; + else if(m_HiderTuneZone == -1) + m_HiderTuneZone = i; + else if(m_GhostTuneZone == -1) + m_GhostTuneZone = i; + else + break; + } + } + if(m_SeekerTuneZone == -1 || m_HiderTuneZone == -1 || m_GhostTuneZone == -1) + { + log_error("hide-n-seek", "Failed to find 3 free tune zonse for hide and seek, aborting game"); + return false; + } + + CTuningParams *pTune = GameServer()->TuningList(MultiMapIndex()); + + pTune[m_HiderTuneZone].m_PlayerHooking = 0; + pTune[m_HiderTuneZone].m_PlayerHammering = 0; + + pTune[m_SeekerTuneZone].m_HammerFireDelay = g_Config.m_SvHideSeekSeekersHammerDelay; + pTune[m_SeekerTuneZone].m_HammerHitFireDelay = pTune[m_SeekerTuneZone].m_HammerFireDelay + 150; + + pTune[m_GhostTuneZone].m_PlayerHooking = 0; + pTune[m_GhostTuneZone].m_PlayerHammering = 0; + pTune[m_GhostTuneZone].m_PlayerCollision = 0; + + return true; +} + +void CHideAndSeekZone::SetGhost(int ClientId, int Duration) +{ + if(!IsCandidate(ClientId)) + return; + + CClientData &Data = m_aClientData[ClientId]; + + if(Data.m_GhostCooldown > 0 || Data.m_GhostDuration > 0) + return; + + CCharacter *pChr = GameServer()->GetPlayerChar(ClientId); + if(pChr) + { + pChr->SetTuneOverride(m_GhostTuneZone); + GameServer()->CreatePlayerSpawn(pChr->GetPos(), pChr->TeamMask()); + } + + Data.m_GhostDuration = Duration; + Data.m_GhostCooldown = TimeToTicks(g_Config.m_SvHideSeekHidersGhostCooldown, Server()->TickSpeed()); +} + +int CHideAndSeekZone::ShowOthers(CPlayer *pPlayer) +{ + if(!IsCandidate(pPlayer->GetCid())) + return -1; + if(m_State != EState::Playing) + return -1; + return SHOW_OTHERS_ONLY_TEAM; + + return -1; +} + +bool CHideAndSeekZone::CanUseCommand(CPlayer *pPlayer, const char *pCommand) +{ + if(!IsCandidate(pPlayer->GetCid())) + return true; + if(m_State != EState::Playing) + return true; + + if(str_startswith_nocase(pCommand, "team") || str_startswith_nocase(pCommand, "spec")) + return false; + + if(!m_aClientData[pPlayer->GetCid()].m_Alive) + return true; + + if(str_startswith_nocase(pCommand, "pause") || + str_startswith_nocase(pCommand, "team") || + str_startswith_nocase(pCommand, "swap")) + { + pPlayer->SendChat("You cannot use that command right now."); + return false; + } + + return true; +} + +bool CHideAndSeekZone::CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) +{ + if(m_aClientData[pPlayer->GetCid()].m_IsSeeker && !m_aClientData[pTarget->GetCid()].m_IsSeeker) + { + pPlayer->SendChat("You can't spectate hiders!"); + return false; // Don't allow spectators to spectate hiders, they might be invisible + } + + return true; +} + +bool CHideAndSeekZone::CanSnapCharacter(CCharacter *pChr, int SnappingClient) +{ + if(!pChr) + return true; // ? + + if(SnappingClient == SERVER_DEMO_CLIENT) + return true; + + if(m_State != EState::Playing) + return true; + + CPlayer *pPlayer = pChr->GetPlayer(); + int ClientId = pPlayer->GetCid(); + + if(SnappingClient == ClientId) + return true; + + CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient]; + + bool IsSnapClientCandidate = IsCandidate(SnappingClient); + + if(!pSnapPlayer) + return false; + CCharacter *pSnapChr = pSnapPlayer->GetCharacter(); + + if(!m_aClientData[ClientId].m_Alive && !m_aClientData[SnappingClient].m_Alive) + return true; + + if(!m_aClientData[ClientId].m_Alive && IsSnapClientCandidate) + return false; + + if(m_aClientData[ClientId].m_IsSeeker && IsSnapClientCandidate) + return true; + + if(m_aClientData[SnappingClient].m_IsSeeker) + { + if(pSnapChr) + { + if(pSnapChr->Core()->HookedPlayer() == ClientId) + return true; // Forcefully show the hider + if(pSnapChr->m_FreezeTime > 0) + return false; // warmup for hiders so they can run + } + + if(m_aClientData[ClientId].m_GhostDuration > 0) + return false; // if the hider is currently a ghost, they are invisible to seekers + + if(m_aClientData[ClientId].m_InHiddenZone) + return false; // if the hider is in a hidden zone, they are invisible to seekers + } + + return true; +} + +bool CHideAndSeekZone::CanDropWeapon(CCharacter *pChr, int Weapon) +{ + if(pChr->GetPlayer()->m_Area == EArea::HideAndSeek) + return false; + return true; +} + +void CHideAndSeekZone::OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) +{ + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; + + if(pPlayer->m_Area == EArea::HideAndSeek) + pPlayer->SetArea(EArea::Game); + m_aClientData[ClientId].Reset(); +} + +bool CHideAndSeekZone::OnCharacterFire(int ClientId, int Weapon) +{ + if(m_State != EState::Playing) + return true; + + if(!IsCandidate(ClientId)) + return true; + + if(!m_aClientData[ClientId].m_Alive) + return false; + + CCharacter *pChr = GameServer()->GetPlayerChar(ClientId); + + if(m_aClientData[ClientId].m_IsSeeker) + { + if(Weapon == WEAPON_GUN) + { + int ClosestId = GetClosestHiderId(ClientId); + if(ClosestId != -1 && m_aClientData[ClientId].m_GunReloadTimer <= 0) + { + vec2 Dir = normalize(m_aClientData[ClosestId].m_LastKnownPos - pChr->GetPos()); + + vec2 ProjStartPos = pChr->GetPos() + Dir * pChr->GetProximityRadius() * 1.35f; + + int Lifetime = (int)(Server()->TickSpeed() * pChr->GetCurrentTuning()->m_GunLifetime); + + new CProjectile( + &GameServer()->m_World, + MultiMapIndex(), + WEAPON_GUN, // Type + -1, // Owner + ProjStartPos, // Pos + Dir, // Dir + Lifetime, // Span + g_Config.m_SvHideSeekSeekersGunFreeze, // Freeze + false, // Explosive + -1, // SoundImpact + vec2(0, 0) // InitDir + ); + GameServer()->CreateSound(pChr->GetPos(), SOUND_GUN_FIRE, pChr->TeamMask()); + + m_aClientData[ClientId].m_GunReloadTimer = g_Config.m_SvHideSeekSeekersGunCooldown * Server()->TickSpeed() / 1000; + } + return false; + } + } + else + { + if(Weapon == WEAPON_HAMMER) + SetGhost(ClientId, TimeToTicks(g_Config.m_SvHideSeekHidersGhostDuration, Server()->TickSpeed())); + } + return true; +} + +void CHideAndSeekZone::OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) +{ + if(SnappingClient == SERVER_DEMO_CLIENT) + return; + CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient]; + if(!pSnapPlayer) + return; + + int ClientId = pPlayer->GetCid(); + + CClientData &Data = m_aClientData[ClientId]; + CClientData &SnapData = m_aClientData[SnappingClient]; + + bool Candidate = IsCandidate(ClientId); + bool SnapCandidate = IsCandidate(SnappingClient); + + if(SnapCandidate || SnapData.m_MarkedAfk) + *pScore = m_aClientData[ClientId].m_NumWins; + + if(m_State != EState::Playing && m_State != EState::Finished) + return; + + if(!Candidate && ClientId != SnappingClient) + { + *pTeam = (int)TEAM_SPECTATORS; + return; + } + + if(!Candidate) + return; + + if(!Data.m_Alive) + { + pClientInfo->m_UseCustomColor = false; + StrToInts(pClientInfo->m_aSkin, std::size(pClientInfo->m_aSkin), "x_spec"); + } + else if(!Data.m_IsSeeker) + { + if(m_State == EState::Finished) + return; + + constexpr int ColorHider = 10401598; // Blue + pClientInfo->m_UseCustomColor = true; + pClientInfo->m_ColorBody = ColorHider; + pClientInfo->m_ColorFeet = ColorHider; + + bool CanSeePlayer = !SnapData.m_IsSeeker || CanSnapCharacter(pPlayer->GetCharacter(), SnappingClient); + + if(Data.m_GhostDuration > 0 && CanSeePlayer) + { + pClientInfo->m_UseCustomColor = false; + StrToInts(pClientInfo->m_aSkin, std::size(pClientInfo->m_aSkin), "ghost"); + } + } + else + { + if(m_State == EState::Finished) + return; + constexpr int ColorSeeker = 16758590; // Red + pClientInfo->m_UseCustomColor = true; + pClientInfo->m_ColorBody = ColorSeeker; + pClientInfo->m_ColorFeet = ColorSeeker; + } +} + +void CHideAndSeekZone::OnCharacterHammerHit(int ClientId, int Target) +{ + if(!IsCandidate(ClientId) || !IsCandidate(Target)) + return; + if(m_aClientData[Target].m_Alive) + { + if(m_aClientData[ClientId].m_IsSeeker && !m_aClientData[Target].m_IsSeeker) + { + // m_aClientData[Target].m_IsSeeker = true; + CCharacter *pTargetChr = GameServer()->m_apPlayers[Target]->GetCharacter(); + + if(pTargetChr) + { + m_aClientData[ClientId].m_NumKills++; + SetDead(Target, true); + } + } + } +} + +bool CHideAndSeekZone::SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) +{ + if(Asker == SERVER_DEMO_CLIENT) + return true; + + if(!CheckClientId(Asker) || !CheckClientId(ClientId)) + return true; // Ignore + + CCharacter *pChr = GameServer()->GetPlayerChar(Asker); + if(!pChr) + return true; // Ignore + CCharacter *pTargetChr = GameServer()->GetPlayerChar(ClientId); + if(!pTargetChr) + return true; // Ignore + + return CanSnapCharacter(pChr, ClientId); +} + +int CHideAndSeekZone::GetClosestHiderId(int SeekerId) +{ + float Dist = std::numeric_limits::max(); + int ClosestId = -1; + for(CCharacter *pChr : m_vCandidates) + { + int ClientId = pChr->GetPlayer()->GetCid(); + if(ClientId == SeekerId) + continue; + if(m_aClientData[ClientId].m_IsSeeker) + continue; + if(!m_aClientData[ClientId].m_Alive) + continue; + if(!pChr->IsAlive()) + continue; + + float NewDist = distance(pChr->GetPos(), GameServer()->m_apPlayers[SeekerId]->GetCharacter()->GetPos()); + if(NewDist < Dist) + { + Dist = NewDist; + ClosestId = ClientId; + } + } + return ClosestId; +} + +bool CHideAndSeekZone::TryReplaceAfkSeeker(int ClientId) +{ + if(!m_aClientData[ClientId].m_IsSeeker || !m_aClientData[ClientId].m_Alive) + return false; + + int AliveSeekers = 0; + for(CCharacter *pCandChar : m_vCandidates) + { + const int CandidateId = pCandChar->GetPlayer()->GetCid(); + if(!m_aClientData[CandidateId].m_Alive) + continue; + if(m_aClientData[CandidateId].m_IsSeeker) + AliveSeekers++; + } + + CCharacter *pChr = GameServer()->GetPlayerChar(ClientId); + if(pChr) + { + pChr->Unfreeze(); + pChr->SetTuneOverride(-1); + } + + m_aClientData[ClientId].m_IsSeeker = false; + m_aClientData[ClientId].m_MarkedAfk = true; + SetDead(ClientId, false); + + for(CCharacter *pCandChar : m_vCandidates) + pCandChar->GetPlayer()->SendChat("A seeker went AFK and was eliminated."); + + std::vector vAliveHiders; + vAliveHiders.reserve(m_vCandidates.size()); + int AlivePlayers = 0; + for(CCharacter *pCandChar : m_vCandidates) + { + const int CandidateId = pCandChar->GetPlayer()->GetCid(); + if(!m_aClientData[CandidateId].m_Alive) + continue; + + AlivePlayers++; + if(!m_aClientData[CandidateId].m_IsSeeker) + vAliveHiders.push_back(CandidateId); + } + + if(AliveSeekers > 1 || AlivePlayers <= 2 || vAliveHiders.empty()) + return false; + + std::uniform_int_distribution Range(0, vAliveHiders.size() - 1); + const int NewSeekerId = vAliveHiders[Range(Rng())]; + CClientData &NewSeekerData = m_aClientData[NewSeekerId]; + NewSeekerData.m_IsSeeker = true; + NewSeekerData.m_LastMovement = Server()->Tick(); + + CCharacter *pNewSeekerChr = GameServer()->GetPlayerChar(NewSeekerId); + if(pNewSeekerChr) + pNewSeekerChr->SetTuneOverride(m_SeekerTuneZone); + + CPlayer *pNewSeeker = GameServer()->m_apPlayers[NewSeekerId]; + if(pNewSeeker) + pNewSeeker->SendChat("You are the new seeker!"); + + + for(CCharacter *pCandChar : m_vCandidates) + { + if(pCandChar->GetPlayer()->GetCid() != NewSeekerId) + { + pCandChar->GetPlayer()->SendChatFmt("'%s' is the new seeker!", Server()->ClientName(NewSeekerId)); + } + else + { + pCandChar->FreezeForce(g_Config.m_SvHideSeekFreezeDuration * Server()->TickSpeed()); + pCandChar->ForceSetPos(GetRandomSpawnPos()); + m_SeekTimeRemaining += MaxAfkSeconds * Server()->TickSpeed(); + } + } + + return true; +} + +void CHideAndSeekZone::SetDead(int ClientId, bool SendKillMsg) +{ + m_aClientData[ClientId].m_Alive = false; + CCharacter *pChr = GameServer()->GetPlayerChar(ClientId); + if(pChr) + pChr->SetSolo(true); + if(SendKillMsg) + { + for(CCharacter *pCandChar : m_vCandidates) + { + if(pCandChar->GetPlayer()->GetCid() == ClientId) + { + pCandChar->GetPlayer()->SendChat("You got found!"); + } + else + { + pCandChar->GetPlayer()->SendChatFmt("'%s' got found!", Server()->ClientName(ClientId)); + } + } + } +} + +int CHideAndSeekZone::GetNumCandidates() +{ + m_vCandidates.clear(); + for(CPlayer *pPlayer : GameServer()->m_apPlayers) + { + if(!pPlayer) + continue; + CCharacter *pChr = pPlayer->GetCharacter(); + if(!pChr) + continue; + if(!pChr->IsAlive()) + continue; + if(pChr->Team() != TEAM_FLOCK) + return false; + if(pPlayer->IsAfk()) + continue; + if(m_aClientData[pPlayer->GetCid()].m_MarkedAfk) + continue; + if(pPlayer->MultiMapIdx() != (int)MultiMapIndex()) + continue; + + if(pPlayer->m_Area == EArea::HideAndSeek) + m_vCandidates.push_back(pPlayer->GetCharacter()); + } + + return m_vCandidates.size(); +} + +bool CHideAndSeekZone::IsCandidate(int ClientId) const +{ + for(CCharacter *pCharacter : m_vCandidates) + { + if(!pCharacter || !pCharacter->GetPlayer()) + continue; // instead of return false + + if(pCharacter->GetPlayer()->GetCid() == ClientId) + return true; + } + + return false; +} + +vec2 CHideAndSeekZone::GetRandomSpawnPos() +{ + if(m_vSpawnPoints.empty()) + return vec2(0, 0); + + std::uniform_int_distribution Range(0, m_vSpawnPoints.size() - 1); + vec2 Pos = m_vSpawnPoints[Range(Rng())]; + Pos += vec2(random_float() - 0.5f, random_float() - 0.5f) * MaxSpawnPointOffset; // add some random offset to prevent players from spawning on top of each other + return Pos; +} + +void CHideAndSeekZone::Init(CMapItemLayerQuads *pQuadsLayer) +{ + char aLayerName[30]; + IntsToStr(pQuadsLayer->m_aName, std::size(pQuadsLayer->m_aName), aLayerName, std::size(aLayerName)); + + CQuad *pQuads = (CQuad *)GameServer()->Map(MultiMapIndex())->GetDataSwapped(pQuadsLayer->m_Data); + m_vQuads.reserve(pQuadsLayer->m_NumQuads); + ESubType SubType = ESubType::Area; + if(!str_comp(aLayerName, "Area")) + SubType = ESubType::Area; + else if(!str_comp(aLayerName, "Spawn")) + SubType = ESubType::Spawn; + else if(!str_comp(aLayerName, "Hidden")) + SubType = ESubType::Hidden; + else + return; + + for(int NumQuads = 0; NumQuads < pQuadsLayer->m_NumQuads; NumQuads++) + { + CQuadData QuadData; + QuadData.m_pQuad = &pQuads[NumQuads]; + QuadData.m_pLayer = pQuadsLayer; + QuadData.m_Type = EZoneType::HideNSeek; + QuadData.m_SubType = (uint8_t)SubType; + for(int j = 0; j < 5; j++) + QuadData.m_Pos[j] = vec2(fx2f(QuadData.m_pQuad->m_aPoints[j].x), fx2f(QuadData.m_pQuad->m_aPoints[j].y)); + + QuadData.m_MapIndex = MultiMapIndex(); + + if(SubType == ESubType::Spawn) + { + for(int j = 0; j < 4; j++) + { + vec2 Pos; + if(!GameServer()->GetNearestAirPos(QuadData.m_Pos[j], &Pos, MaxSpawnPointOffset)) + continue; + + m_vSpawnPoints.push_back(Pos); + } + } + + m_vQuads.push_back(QuadData); + } +} diff --git a/src/game/server/foxnet/components/zones/hidenseek.h b/src/game/server/foxnet/components/zones/hidenseek.h new file mode 100644 index 00000000000..b13b3e39fde --- /dev/null +++ b/src/game/server/foxnet/components/zones/hidenseek.h @@ -0,0 +1,144 @@ +#ifndef GAME_SERVER_FOXNET_COMPONENTS_ZONES_HIDENSEEK_H +#define GAME_SERVER_FOXNET_COMPONENTS_ZONES_HIDENSEEK_H + +#include "zone.h" + +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +class CHideAndSeekZone : public IZone +{ + enum class ESubType : uint8_t + { + Area = 0, + Spawn, + Hidden, + }; + + enum class EState + { + BadMap, + WaitingForPlayers, + Warmup, + DoWarmup, + Playing, + Finished, + } m_State = EState::WaitingForPlayers; + + enum class EWinState + { + None, + Hiders, + Seeker, + }; + + int64_t m_FinishedDelay = 0; + + int64_t m_WarmUpTime = 0; + + int m_SeekTimeTotal = 0; + int m_SeekTimeRemaining = 0; // In Ticks + + int m_GameInfoTimeLimit = 0; + int m_GameInfoRoundStartTick = 0; + + class CClientData + { + public: + int64_t m_JoinedAt = 0; + int64_t m_LastMovement = 0; // to prevent afks + + bool m_MarkedAfk = false; + + bool m_Alive = false; + + vec2 m_LastKnownPos = vec2(0, 0); // only gets set if Ghost Mode is off + int m_GhostCooldown = 0; // In Ticks + int m_GhostDuration = 0; // In Ticks + + int m_NumHiddenTicks = 0; // In Ticks + + bool m_InHiddenZone = false; + + bool m_IsSeeker = false; + int m_GunReloadTimer = 0; + int m_NumKills = 0; + + int m_NumWins = 0; + + void Reset() + { + m_JoinedAt = 0; + m_LastMovement = 0; + m_Alive = false; + m_LastKnownPos = vec2(0, 0); + m_GhostCooldown = 0; + m_GhostDuration = 0; + m_NumHiddenTicks = 0; + m_InHiddenZone = false; + m_IsSeeker = false; + m_GunReloadTimer = 0; + m_NumKills = 0; + } + + } m_aClientData[MAX_CLIENTS]; + + void ClientTick(int ClientId); + + // when m_State gets set to Playing, this function is called to move players and set them up for the game + void StartGame(); + void EndGame(EWinState WinState); + + std::vector m_vSpawnPoints; + + int m_SeekerTuneZone = -1; + int m_HiderTuneZone = -1; + int m_GhostTuneZone = -1; + bool InitTuning(); + + void SetGhost(int ClientId, int Duration); + + std::vector m_vCandidates; + int GetNumCandidates(); + bool IsCandidate(int ClientId) const; + vec2 GetRandomSpawnPos(); + int GetClosestHiderId(int SeekerId); + bool TryReplaceAfkSeeker(int ClientId); + + void SetDead(int ClientId, bool SendKillMsg = true); + +public: + CHideAndSeekZone(CGameContext *pGameContext, size_t MapIndex) : + IZone(pGameContext, MapIndex) {} + + void Init(CMapItemLayerQuads *pQuadsLayer) override; + void OnTick() override; + + void OnClientDrop(int ClientId, const char *pReason) override; + + void OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) override; + + int ShowOthers(CPlayer *pPlayer) override; + bool CanUseCommand(CPlayer *pPlayer, const char *pCommand) override; + bool CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) override; + bool CanSnapCharacter(CCharacter *pChr, int SnappingClient) override; + bool CanDropWeapon(CCharacter *pChr, int Weapon) override; + void OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) override; + bool OnCharacterFire(int ClientId, int Weapon) override; + void OnCharacterHammerHit(int ClientId, int Target) override; + bool SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) override; + + void OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) override; +}; + +#endif // GAME_SERVER_FOXNET_COMPONENTS_ZONES_HIDENSEEK_H \ No newline at end of file diff --git a/src/game/server/foxnet/components/zones/roulette.cpp b/src/game/server/foxnet/components/zones/roulette.cpp index dea16204945..6e90ef1d0fc 100644 --- a/src/game/server/foxnet/components/zones/roulette.cpp +++ b/src/game/server/foxnet/components/zones/roulette.cpp @@ -40,38 +40,45 @@ void CRouletteZone::OnTick() } } } - - for(const CQuadData &QuadData : Quads()) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(QuadData.m_SubType != (uint8_t)ESubType::Area) + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; + if(!pPlayer || !pPlayer->GetCharacter()) + continue; + if(pPlayer->MultiMapIdx() != (int)MultiMapIndex()) + continue; + CCharacter *pChr = pPlayer->GetCharacter(); + if(!pChr->IsAlive()) continue; - for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) - { - CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; - if(!pPlayer || !pPlayer->GetCharacter()) - continue; - if(pPlayer->MultiMapIdx() != (int)MultiMapIndex()) - continue; - CCharacter *pChr = pPlayer->GetCharacter(); - if(!pChr->IsAlive()) - continue; + pChr->m_InsideQuadFreeze = false; + if(pChr->Core()->m_IsInFreeze) + continue; - pChr->m_InsideQuadFreeze = false; - if(pChr->Core()->m_IsInFreeze) + bool InArea = false; + for(const CQuadData &QuadData : Quads()) + { + if(QuadData.m_SubType != (uint8_t)ESubType::Area) continue; vec2 Points[4] = {QuadData.m_Pos[0], QuadData.m_Pos[1], QuadData.m_Pos[3], QuadData.m_Pos[2]}; if(InsideQuad(pChr->GetPos(), Points, vec2(0, 0))) { - pPlayer->SetArea(EArea::Roulette); - } - else - { - if(pPlayer->m_Area == EArea::Roulette) - pPlayer->SetArea(EArea::Game); + InArea = true; + break; } } + + if(InArea) + { + if(pPlayer->m_Area != EArea::Roulette) + pPlayer->SetArea(EArea::Roulette); + } + else + { + if(pPlayer->m_Area == EArea::Roulette) + pPlayer->SetArea(EArea::Game); + } } } diff --git a/src/game/server/foxnet/components/zones/roulette.h b/src/game/server/foxnet/components/zones/roulette.h index 9a5d2660678..acb262a387a 100644 --- a/src/game/server/foxnet/components/zones/roulette.h +++ b/src/game/server/foxnet/components/zones/roulette.h @@ -11,13 +11,6 @@ #include #include -enum class ESubType : uint8_t -{ - Area = 0, - Wheel, - BetOption, -}; - class CBetQuadData { public: @@ -29,6 +22,13 @@ class CBetQuadData class CRouletteZone : public IZone { + enum class ESubType : uint8_t + { + Area = 0, + Wheel, + BetOption, + }; + bool m_CreatedWheel = false; std::vector m_vBetQuads; diff --git a/src/game/server/foxnet/components/zones/zone.cpp b/src/game/server/foxnet/components/zones/zone.cpp index d8b97bb76e6..4b359cc002e 100644 --- a/src/game/server/foxnet/components/zones/zone.cpp +++ b/src/game/server/foxnet/components/zones/zone.cpp @@ -17,6 +17,7 @@ #include #include #include +#include void IZone::GetAnimationTransform(int MultiMapIndex, float GlobalTime, int Env, vec2 &Position, float &Angle) const { @@ -173,6 +174,11 @@ CCollision *IZone::Collision() const return GameServer()->Collision(MultiMapIndex()); } +IServer *IZone::Server() const +{ + return GameServer()->Server(); +} + void IZone::UpdateCache() { const double Time = GameServer()->m_pController->GetTime(); diff --git a/src/game/server/foxnet/components/zones/zone.h b/src/game/server/foxnet/components/zones/zone.h index e31fe2fa1ab..94be4c17d15 100644 --- a/src/game/server/foxnet/components/zones/zone.h +++ b/src/game/server/foxnet/components/zones/zone.h @@ -3,6 +3,8 @@ #include +#include + #include #include #include @@ -11,8 +13,11 @@ #include class CGameContext; +class IServer; class CQuad; class CMapItemLayerQuads; +class CPlayer; +class CCharacter; class IZone { @@ -34,6 +39,7 @@ class IZone std::vector m_vQuads; CGameContext *GameServer() const { return m_pGameContext; } + IServer *Server() const; CCollision *Collision() const; [[nodiscard]] const std::vector &Quads() const { return m_vQuads; } @@ -47,6 +53,23 @@ class IZone virtual void Init(CMapItemLayerQuads *pQuadsLayer); virtual void OnTick() {} + virtual void OnClientDrop(int ClientId, const char *pReason) {}; + + virtual void OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) {} + + virtual int ShowOthers(CPlayer *pPlayer) { return -1; } + virtual bool CanUseCommand(CPlayer *pPlayer, const char *pCommand) { return true; } + virtual bool CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) { return true; } + virtual bool CanSnapCharacter(CCharacter *pChr, int SnappingClient) { return true; } + virtual bool CanDropWeapon(CCharacter *pChr, int Weapon) { return true; } + virtual void OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) {} + virtual void OnCharacterSpawn(int ClientId, vec2 Pos) {} + virtual bool OnCharacterFire(int ClientId, int Weapon) { return true; } + virtual void OnCharacterHammerHit(int ClientId, int Target) {} + virtual bool SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) { return true; } + + virtual void OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) {} + virtual ~IZone() = default; }; diff --git a/src/game/server/foxnet/components/zones/zonemanager.cpp b/src/game/server/foxnet/components/zones/zonemanager.cpp index 4d139e15a17..66374cff0f4 100644 --- a/src/game/server/foxnet/components/zones/zonemanager.cpp +++ b/src/game/server/foxnet/components/zones/zonemanager.cpp @@ -4,6 +4,7 @@ #include "collidable.h" #include "death.h" #include "freeze.h" +#include "hidenseek.h" #include "roulette.h" #include "unfreeze.h" #include "zone.h" @@ -20,6 +21,7 @@ #include #include #include +#include IZone *CZoneManager::FindZoneByMapIndex(EZoneType Type, size_t MultiMapIdx) { @@ -54,6 +56,15 @@ void CZoneManager::OnMapLoad(size_t MultiMapIdx) m_avpZones[(int)EZoneType::Roulette].push_back(pGroupZone); } } + else if(!str_comp(aGroupName, "#HideNSeek")) + { + pGroupZone = FindZoneByMapIndex(EZoneType::HideNSeek, MultiMapIdx); + if(pGroupZone == nullptr) + { + pGroupZone = new CHideAndSeekZone(GameServer(), MultiMapIdx); + m_avpZones[(int)EZoneType::HideNSeek].push_back(pGroupZone); + } + } for(int LayerIndex = 0; LayerIndex < pGroup->m_NumLayers; LayerIndex++) { @@ -97,13 +108,11 @@ void CZoneManager::OnMapLoad(size_t MultiMapIdx) if(!str_comp("QHook", aLayerName)) { CCollidableZone *pZone = new CCollidableZone(GameServer(), MultiMapIdx, true); - // pZone->Init(pTilemap); we get the pointer from Collision() m_avpZones[(int)EZoneType::Hookable].push_back(pZone); } else if(!str_comp("QUnHook", aLayerName)) { CCollidableZone *pZone = new CCollidableZone(GameServer(), MultiMapIdx, true); - // pZone->Init(pTilemap); we get the pointer from Collision() m_avpZones[(int)EZoneType::Unhookable].push_back(pZone); } else if(!str_comp("QCfrm", aLayerName)) @@ -145,4 +154,175 @@ void CZoneManager::OnTick() pZone->OnTick(); } } +} + +void CZoneManager::OnClientDrop(int ClientId, const char *pReason) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnClientDrop(ClientId, pReason); + } + } +} + +void CZoneManager::OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnGameInfoSnap(ClientId, pGameInfoObj, pGameInfoEx); + } + } +} + +int CZoneManager::ShowOthers(CPlayer *pPlayer) +{ + int ShowOthers = -1; + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + int ZoneShowOthers = pZone->ShowOthers(pPlayer); + if(ZoneShowOthers != -1) + ShowOthers = ZoneShowOthers; + } + } + return ShowOthers; +} + +bool CZoneManager::CanUseCommand(CPlayer *pPlayer, const char *pCommand) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->CanUseCommand(pPlayer, pCommand)) + return false; + } + } + return true; +} + +bool CZoneManager::CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->CanSpectateId(pPlayer, pTarget)) + return false; + } + } + return true; +} + +bool CZoneManager::CanSnapCharacter(CCharacter *pChr, int SnappingClient) +{ + bool Allowed = true; + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->CanSnapCharacter(pChr, SnappingClient)) + Allowed = false; + } + } + return Allowed; +} +bool CZoneManager::CanDropWeapon(CCharacter *pChr, int Weapon) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->CanDropWeapon(pChr, Weapon)) + return false; + } + } + return true; +} + + +void CZoneManager::OnCharacterSpawn(int ClientId, vec2 Pos) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnCharacterSpawn(ClientId, Pos); + } + } +} +void CZoneManager::OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnCharacterDie(ClientId, Killer, Weapon, SendKillMsg); + } + } +} +bool CZoneManager::OnCharacterFire(int ClientId, int Weapon) +{ + bool Allowed = true; + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->OnCharacterFire(ClientId, Weapon)) + Allowed = false; + } + } + return Allowed; +} +void CZoneManager::OnCharacterHammerHit(int ClientId, int Target) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnCharacterHammerHit(ClientId, Target); + } + } +} +bool CZoneManager::SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) +{ + // Doesn't need bool Allowed, if SetMask returns false the ClientId wont be snapped + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + if(!pZone->SetMask(ClientId, MultiMapIdx, Team, ExceptId, Asker, VersionFlags, Flags)) + return false; + } + } + return true; +} + +void CZoneManager::OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) +{ + for(int i = 0; i < (int)EZoneType::Num; i++) + { + auto &vZones = m_avpZones[i]; + for(IZone *pZone : vZones) + { + pZone->OnPlayerSnap(pPlayer, SnappingClient, pClientInfo, pTeam, pLatency, pScore); + } + } } \ No newline at end of file diff --git a/src/game/server/foxnet/components/zones/zonemanager.h b/src/game/server/foxnet/components/zones/zonemanager.h index 5013356df96..d45a4edcc46 100644 --- a/src/game/server/foxnet/components/zones/zonemanager.h +++ b/src/game/server/foxnet/components/zones/zonemanager.h @@ -5,6 +5,8 @@ #include +#include + #include #include #include @@ -28,6 +30,23 @@ class CZoneManager : public CServerComponent void OnMapLoad(size_t MapIdx) override; void OnMapUnload(size_t MapIdx) override; void OnTick() override; + + void OnClientDrop(int ClientId, const char *pReason) override; + + void OnGameInfoSnap(int ClientId, CNetObj_GameInfo *pGameInfoObj, CNetObj_GameInfoEx *pGameInfoEx) override; + + int ShowOthers(CPlayer *pPlayer) override; + bool CanUseCommand(CPlayer *pPlayer, const char *pCommand) override; + bool CanSpectateId(CPlayer *pPlayer, CPlayer *pTarget) override; + bool CanSnapCharacter(CCharacter *pChr, int SnappingClient) override; + bool CanDropWeapon(CCharacter *pChr, int Weapon) override; + void OnCharacterSpawn(int ClientId, vec2 Pos) override; + void OnCharacterDie(int ClientId, int Killer, int Weapon, bool SendKillMsg) override; + bool OnCharacterFire(int ClientId, int Weapon) override; + void OnCharacterHammerHit(int ClientId, int Target) override; + bool SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, int Asker, int VersionFlags, int Flags) override; + + void OnPlayerSnap(CPlayer *pPlayer, int SnappingClient, CNetObj_ClientInfo *pClientInfo, int *pTeam, int *pLatency, int *pScore) override; }; #endif // GAME_SERVER_FOXNET_COMPONENTS_ZONES_ZONEMANAGER_H diff --git a/src/game/server/foxnet/entities/roulette.cpp b/src/game/server/foxnet/entities/roulette.cpp index 03c4bf1b8a2..383fab31aec 100644 --- a/src/game/server/foxnet/entities/roulette.cpp +++ b/src/game/server/foxnet/entities/roulette.cpp @@ -93,10 +93,6 @@ bool CRoulette::AddClient(int ClientId, int BetAmount, const char *pBetOption) if(pPlayer->m_Area != EArea::Roulette) return false; - CCharacter *pChr = pPlayer->GetCharacter(); - if(pChr->Team() != TEAM_FLOCK) - return false; - if(!g_Config.m_SvAccounts) { GameServer()->SendChatTarget(ClientId, "Feature is disabled."); diff --git a/src/game/server/foxnet/gamecontext.cpp b/src/game/server/foxnet/gamecontext.cpp index e6a0b60c1a9..3b7c419a891 100644 --- a/src/game/server/foxnet/gamecontext.cpp +++ b/src/game/server/foxnet/gamecontext.cpp @@ -1563,4 +1563,47 @@ CTuningParams CGameContext::DDNetDefaultTuning() const Tune.m_ShotgunSpeed = 500; Tune.m_ShotgunSpeeddiff = 0; return Tune; +} + +bool CGameContext::GetNearestAirPos(vec2 Pos, vec2 *pOut, float Radius) +{ + const float Size = Radius * 0.5f; + const vec2 SizeVec2 = vec2(Size, Size); + + if(!Collision()->TestBox(Pos, SizeVec2)) + { + *pOut = Pos; + return true; + } + + static constexpr int SearchRadius = 12; + static constexpr float Step = 16.0f; + + float BestDist = std::numeric_limits::max(); + vec2 BestPos = Pos; + + for(int y = -SearchRadius; y <= SearchRadius; y++) + { + for(int x = -SearchRadius; x <= SearchRadius; x++) + { + vec2 Candidate = Pos + vec2(x * Step, y * Step); + if(Collision()->TestBox(Pos, SizeVec2)) + continue; + + float Dist = distance(Pos, Candidate); + if(Dist < BestDist) + { + BestDist = Dist; + BestPos = Candidate; + } + } + } + + if(BestDist < std::numeric_limits::max()) + { + *pOut = BestPos; + return true; + } + + return false; } \ No newline at end of file diff --git a/src/game/server/foxnet/player.cpp b/src/game/server/foxnet/player.cpp index e339c7e36e4..872920860bd 100644 --- a/src/game/server/foxnet/player.cpp +++ b/src/game/server/foxnet/player.cpp @@ -1,3 +1,4 @@ +#include "component.h" #include "cosmetics/dot_trail.h" #include "cosmetics/epic_circle.h" #include "cosmetics/halo.h" @@ -149,10 +150,8 @@ void CPlayer::ExpireItems() pCfg->m_Remove(*this, *pCfg, -1); } Entry = CInventoryEntry(); - char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "Item '%s' has expired!", pName); + SendChatFmt("Item '%s' has expired!", pName); GameServer()->m_AccountManager.RemoveItem(Acc()->m_aUsername, pName); - SendChat(aBuf); } } } @@ -1018,7 +1017,10 @@ int CPlayer::NumDDraceHudRows() Rows++; if(pChr->Core()->m_Solo || pChr->Core()->m_CollisionDisabled || pChr->Core()->m_Passive || pChr->Core()->m_HookHitDisabled || pChr->Core()->m_HammerHitDisabled || pChr->Core()->m_ShotgunHitDisabled || - pChr->Core()->m_GrenadeHitDisabled || pChr->Core()->m_LaserHitDisabled) + pChr->Core()->m_GrenadeHitDisabled || pChr->Core()->m_LaserHitDisabled || + !pChr->GetCurrentTuning()->m_PlayerHammering || + !pChr->GetCurrentTuning()->m_PlayerHooking || + !pChr->GetCurrentTuning()->m_PlayerCollision) Rows++; if(pChr->Teams()->IsPractice(pChr->Team()) || pChr->Teams()->TeamLocked(pChr->Team()) || pChr->Core()->m_DeepFrozen || pChr->Core()->m_LiveFrozen) Rows++; @@ -1073,9 +1075,18 @@ void CPlayer::SendBroadcastHud(const std::vector &pMessages, int Of SendBroadcast(aBuf); } -void CPlayer::SendChat(const char *pText) +void CPlayer::SendChat(const char *pMsg) { - GameServer()->SendChatTarget(GetCid(), pText); + GameServer()->SendChatTarget(GetCid(), pMsg); +} +void CPlayer::SendChatFmt(const char *pFmt, ...) +{ + char aBuf[1024]; + va_list args; + va_start(args, pFmt); + str_format_v(aBuf, sizeof(aBuf), pFmt, args); + va_end(args); + SendChat(aBuf); } void CPlayer::SendAreaMotd(EArea Area) @@ -1117,6 +1128,22 @@ void CPlayer::SendAreaMotd(EArea Area) "3x dozens: 3x\n" "Green [Zero]: 10x\n" "\n" + "[Press Tab to hide]"; + break; + case EArea::HideAndSeek: + Msg.m_pMessage = + "[Viewable in Server info tab]\n" + "\n" + "\n" + "-- Hɪᴅᴇ ᴀɴᴅ Sᴇᴇᴋ --\n" + "\n" + "Sᴇᴇᴋᴇʀ:\n" + "Find all seekers and hammer them, shooting your gun will point to the closest hidden player.\n" + "\n" + "Hɪᴅᴇʀ:\n" + "Dark Areas completely hide you from the seeker, hammering will put you in ghost mode for a short time which allows you to run away\n" + "\n" + "Depending on the map, entities will or will not work\n" "\n" "[Press Tab to hide]"; break; @@ -1277,3 +1304,14 @@ bool CPlayer::SendToMap(int Idx) return true; } + +int CPlayer::GetShowOthers() +{ + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + int Value = pComponent->ShowOthers(this); + if(Value != -1) + return Value; + } + return m_ShowOthers; +} \ No newline at end of file diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 975760a1f2a..42df2482845 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -2427,6 +2427,12 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, con pPlayer->m_aLastCommands[pPlayer->m_LastCommandPos] = Now; pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4; + for(CServerComponent *pComponent : m_vpComponents) + { + if(!pComponent->CanUseCommand(pPlayer, pMsg->m_pMessage + 1)) + return; + } + Console()->SetFlagMask(CFGFLAG_CHAT); { CClientChatLogger Logger(this, ClientId, log_get_scope_logger()); diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index fea6d5a4ab3..af697c0d3c4 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -329,10 +329,9 @@ class CGameContext : public IGameServer CTuningParams *TuningList(size_t MultiMapIdx) { if(MultiMapIdx >= m_vMultiMaps.size()) - return &m_vMultiMaps[DefaultMapIndex]->m_aTuningList[0]; - return &m_vMultiMaps[MultiMapIdx]->m_aTuningList[0]; + return m_vMultiMaps[DefaultMapIndex]->m_aTuningList; + return m_vMultiMaps[MultiMapIdx]->m_aTuningList; } - // FoxNet> IAntibot *Antibot() { return m_pAntibot; } CTeeHistorian *TeeHistorian() { return &m_TeeHistorian; } @@ -803,8 +802,8 @@ class CGameContext : public IGameServer const char *MapName(int ClientId); std::deque> m_vMultiMaps; // index 0 is default map -private: std::vector m_vpComponents; +private: class CDamageIndEffects { @@ -1063,6 +1062,8 @@ class CGameContext : public IGameServer bool CanUseCmd(int ClientId, const char *pCmd); CTuningParams DDNetDefaultTuning() const; + + bool GetNearestAirPos(vec2 Pos, vec2 *pOut, float Radius); // FoxNet> }; diff --git a/src/game/server/gamecontroller.cpp b/src/game/server/gamecontroller.cpp index 44aff4768df..63fce39dd71 100644 --- a/src/game/server/gamecontroller.cpp +++ b/src/game/server/gamecontroller.cpp @@ -709,8 +709,11 @@ void IGameController::Snap(int SnappingClient) else m_QuadStartTick = m_RoundStartTick; pGameInfoObj->m_RoundStartTick = m_QuadStartTick; - // FoxNet> + + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + pComponent->OnGameInfoSnap(SnappingClient, pGameInfoObj, pGameInfoEx); + // FoxNet> if(Server()->IsSixup(SnappingClient)) { protocol7::CNetObj_GameData *pGameData = Server()->SnapNewItem(0); diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 9b162d5cd65..a54fa38ab50 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -350,6 +350,10 @@ void CPlayer::Snap(int SnappingClient) int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientId]; Latency += m_ExtraPing; int Score = GameServer()->m_pController->SnapPlayerScore(SnappingClient, this); + int Team = m_Team; + + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + pComponent->OnPlayerSnap(this, SnappingClient, pClientInfo, &Team, &Latency, &Score); if(!Server()->IsSixup(SnappingClient)) { @@ -361,11 +365,11 @@ void CPlayer::Snap(int SnappingClient) pPlayerInfo->m_Score = Score; pPlayerInfo->m_Local = (int)(m_ClientId == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD)); pPlayerInfo->m_ClientId = TranslatedId; - pPlayerInfo->m_Team = m_Team; + pPlayerInfo->m_Team = Team; if(SnappingClientVersion < VERSION_DDNET_INDEPENDENT_SPECTATORS_TEAM) { // In older versions the SPECTATORS TEAM was also used if the own player is in PAUSE_PAUSED or if any player is in PAUSE_SPEC. - pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientId != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS; + pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientId != SnappingClient) && m_Paused < PAUSE_SPEC ? Team : TEAM_SPECTATORS; } } else @@ -383,7 +387,7 @@ void CPlayer::Snap(int SnappingClient) pPlayerInfo->m_Latency = Latency; } - if(m_ClientId == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused)) + if(m_ClientId == SnappingClient && (Team == TEAM_SPECTATORS || m_Paused)) { if(!Server()->IsSixup(SnappingClient)) { @@ -411,7 +415,7 @@ void CPlayer::Snap(int SnappingClient) if(m_ClientId == SnappingClient) { // send extended spectator info even when playing, this allows demo to record camera settings for local player - const int SpectatingClient = ((m_Team != TEAM_SPECTATORS && !m_Paused) || m_SpectatorId < 0 || m_SpectatorId >= MAX_CLIENTS) ? TranslatedId : m_SpectatorId; + const int SpectatingClient = ((Team != TEAM_SPECTATORS && !m_Paused) || m_SpectatorId < 0 || m_SpectatorId >= MAX_CLIENTS) ? TranslatedId : m_SpectatorId; const CPlayer *pSpecPlayer = GameServer()->m_apPlayers[SpectatingClient]; if(pSpecPlayer) @@ -425,7 +429,7 @@ void CPlayer::Snap(int SnappingClient) pDDNetSpectatorInfo->m_Deadzone = pSpecPlayer->m_CameraInfo.m_Deadzone; pDDNetSpectatorInfo->m_FollowFactor = pSpecPlayer->m_CameraInfo.m_FollowFactor; - if(pSpecPlayer->m_EnableSpectatorCount && SpectatingClient == TranslatedId && SnappingClient != SERVER_DEMO_CLIENT && m_Team != TEAM_SPECTATORS && !m_Paused) + if(pSpecPlayer->m_EnableSpectatorCount && SpectatingClient == TranslatedId && SnappingClient != SERVER_DEMO_CLIENT && Team != TEAM_SPECTATORS && !m_Paused) { CNetObj_SpectatorCount *pSpectatorCount = Server()->SnapNewItem(0); if(!pSpectatorCount) @@ -479,6 +483,13 @@ void CPlayer::Snap(int SnappingClient) pDDNetPlayer->m_Flags |= EXPLAYERFLAG_PAUSED; IGameController::CFinishTime PlayerTime = GameServer()->m_pController->SnapPlayerTime(SnappingClient, this); + + if(SnappingClient != SERVER_DEMO_CLIENT && pSnapPlayer && + pSnapPlayer->m_Area == EArea::HideAndSeek && m_Area == EArea::HideAndSeek) + { + PlayerTime = IGameController::CFinishTime::Unset(); + } + pDDNetPlayer->m_FinishTimeSeconds = PlayerTime.m_Seconds; pDDNetPlayer->m_FinishTimeMillis = PlayerTime.m_Milliseconds; @@ -495,7 +506,7 @@ void CPlayer::Snap(int SnappingClient) if(SnappingClient != SERVER_DEMO_CLIENT) { - ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(m_ClientId) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused())); + ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(m_ClientId) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->GetShowOthers() == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused())); } if(ShowSpec) @@ -930,6 +941,12 @@ void CPlayer::SetSpectatorId(int Id) CPlayer *pSpectator = Id >= 0 ? GameServer()->m_apPlayers[Id] : nullptr; if(pSpectator && !Server()->IsRconAuthed(GetCid())) { + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + if(!pComponent->CanSpectateId(this, pSpectator)) + return; + } + if(pSpectator->m_Vanish && !m_Vanish) { SendChat("Invalid spectator id used"); @@ -1030,10 +1047,9 @@ void CPlayer::ProcessScoreResult(CScorePlayerResult &Result) } case CScorePlayerResult::PLAYER_TIMECP: GameServer()->Score()->PlayerData(m_ClientId)->SetBestTimeCp(Result.m_Data.m_Info.m_aTimeCp); - char aBuf[128], aTime[32]; + char aTime[32]; str_time_float(Result.m_Data.m_Info.m_Time.value(), ETimeFormat::HOURS_CENTISECS, aTime, sizeof(aTime)); - str_format(aBuf, sizeof(aBuf), "Showing the checkpoint times for '%s' with a race time of %s", Result.m_Data.m_Info.m_aRequestedPlayer, aTime); - SendChat(aBuf); + SendChatFmt("Showing the checkpoint times for '%s' with a race time of %s", Result.m_Data.m_Info.m_aRequestedPlayer, aTime); break; } } diff --git a/src/game/server/player.h b/src/game/server/player.h index 3a0af6656de..34bf828a1a0 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -40,6 +40,7 @@ enum class EArea { Game, Roulette, + HideAndSeek, Num }; @@ -439,8 +440,6 @@ class CPlayer void LootBoxTick(); - void SendBroadcast(const char *pText); - class CLootBoxData { public: @@ -573,9 +572,13 @@ class CPlayer } m_BroadcastData; int NumDDraceHudRows(); + void SendBroadcast(const char *pText); + void SendBroadcastHud(const std::vector &pMessages, int Offset = -1); void ClearBroadcast() { SendBroadcast(""); } - void SendChat(const char *pText); + + void SendChat(const char *pMsg); + [[gnu::format(printf, 2, 3)]] void SendChatFmt(const char *pFmt, ...); float m_PredMargin; void Repredict(int PredMargin) { m_PredMargin = PredMargin / 10.0; } @@ -595,6 +598,8 @@ class CPlayer int MultiMapIdx() const { return m_MultiMapIndex; } std::vector m_vReceivedConditionals; // conditional chat commands + + int GetShowOthers(); // FoxNet> }; #endif diff --git a/src/game/server/teams.cpp b/src/game/server/teams.cpp index 0533a1a7aa5..d86333cc6d5 100644 --- a/src/game/server/teams.cpp +++ b/src/game/server/teams.cpp @@ -1482,6 +1482,12 @@ bool CGameTeams::SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, if(MultiMapIdx != pClient->MultiMapIdx() && !g_Config.m_SvMultimapAllowInteraction) return false; + for(CServerComponent *pComponent : GameServer()->m_vpComponents) + { + if(!pComponent->SetMask(ClientId, MultiMapIdx, Team, ExceptId, Asker, VersionFlags, Flags)) + return false; + } + if(!(pClient->GetTeam() == TEAM_SPECTATORS || pClient->IsPaused())) { // Not spectator if(ClientId != Asker) @@ -1489,12 +1495,12 @@ bool CGameTeams::SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, if(!pClientChr) return false; // Player is currently dead const bool SpawnSolo = pClientChr->m_SpawnSolo || (pAskerChr && pAskerChr->m_SpawnSolo); // Spawn solo mimics SHOW_OTHERS_ONLY_TEAM - if(pClient->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM || SpawnSolo) + if(pClient->GetShowOthers() == SHOW_OTHERS_ONLY_TEAM || SpawnSolo) { if(m_Core.Team(ClientId) != Team && m_Core.Team(ClientId) != TEAM_SUPER) return false; // In different teams } - else if(pClient->m_ShowOthers == SHOW_OTHERS_OFF) + else if(pClient->GetShowOthers() == SHOW_OTHERS_OFF) { if(!(Flags & IGNORE_SOLO)) { @@ -1515,12 +1521,12 @@ bool CGameTeams::SetMask(int ClientId, int MultiMapIdx, int Team, int ExceptId, if(!Character(pClient->SpectatorId())) return false; // Player is currently dead const bool SpawnSolo = Character(pClient->SpectatorId())->m_SpawnSolo || (pAskerChr && pAskerChr->m_SpawnSolo); // Spawn solo mimics SHOW_OTHERS_ONLY_TEAM - if(pClient->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM || SpawnSolo) + if(pClient->GetShowOthers() == SHOW_OTHERS_ONLY_TEAM || SpawnSolo) { if(m_Core.Team(pClient->SpectatorId()) != Team && m_Core.Team(pClient->SpectatorId()) != TEAM_SUPER) return false; // In different teams } - else if(pClient->m_ShowOthers == SHOW_OTHERS_OFF) + else if(pClient->GetShowOthers() == SHOW_OTHERS_OFF) { if(!(Flags & IGNORE_SOLO)) { diff --git a/src/game/tuning.h b/src/game/tuning.h index 35392fadf81..a170762a0ab 100644 --- a/src/game/tuning.h +++ b/src/game/tuning.h @@ -76,4 +76,6 @@ MACRO_TUNING_PARAM(PortalgunFireDelay, portalgun_fire_delay, 100, "Delay of usin MACRO_TUNING_PARAM(MovingTiles, moving_tiles, 0, "Whether to use Moving Tiles") MACRO_TUNING_PARAM(TeleGrenade, tele_grenade, 0, "Whether to use Tele Grenade") + +MACRO_TUNING_PARAM(PlayerHammering, player_hammering, 1, "Whether this player can hammer others") // FoxNet> From 141bc74b7fb9912f04d100a6cde8e6cf2b7668b6 Mon Sep 17 00:00:00 2001 From: qxdFox Date: Sat, 4 Apr 2026 14:27:14 +0200 Subject: [PATCH 2/2] save client infos on map change --- src/engine/server/server.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 915c8d57207..6d06012e78b 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -341,7 +341,6 @@ void CServer::CClient::Reset() m_RedirectDropTime = 0; FreeClientOverrideMap(*this); - ResetContent(); } CServer::CServer() @@ -1330,6 +1329,7 @@ int CServer::NewClientNoAuthCallback(int ClientId, void *pUser) pThis->Antibot()->OnEngineClientJoin(ClientId); // m_aClients[ClientId].ResetContent(); pThis->SendFoxnetInfo(ClientId); // FoxNet> @@ -1372,6 +1372,7 @@ int CServer::NewClientCallback(int ClientId, void *pUser, bool Sixup) pThis->SendConnLoggingCommand(OPEN_SESSION, pThis->ClientAddr(ClientId)); #endif // m_aClients[ClientId].ResetContent(); pThis->SendFoxnetInfo(ClientId); // FoxNet> return 0; @@ -4921,6 +4922,7 @@ void CServer::CClient::ResetContent() { FreeClientOverrideMap(*this); str_copy(m_aCustomClient, "DDNet"); + m_aClientMessage[0] = '\0'; m_QuietJoin = false; m_HighBandwidth = true; }