diff --git a/CMakeLists.txt b/CMakeLists.txt
index ac9bb20326..157b42d358 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -82,9 +82,9 @@ set(OPENMSX_VERSION "1.6")
set(OPENMSX_URL "https://github.com/OpenRCT2/OpenMusic/releases/download/v${OPENMSX_VERSION}/openmusic.zip")
set(OPENMSX_SHA1 "ba170fa6d777b309c15420f4b6eb3fa25082a9d1")
-set(REPLAYS_VERSION "0.0.87")
+set(REPLAYS_VERSION "0.0.89")
set(REPLAYS_URL "https://github.com/OpenRCT2/replays/releases/download/v${REPLAYS_VERSION}/replays.zip")
-set(REPLAYS_SHA1 "6061B53DE346BD853BB997E635AC7374B1A7D2F0")
+set(REPLAYS_SHA1 "089CB8EEA76A98028367FDDE72675E9309AB9036")
option(FORCE32 "Force 32-bit build. It will add `-m32` to compiler flags.")
option(WITH_TESTS "Build tests")
diff --git a/distribution/changelog.txt b/distribution/changelog.txt
index 4b3f544237..c41e9826ad 100644
--- a/distribution/changelog.txt
+++ b/distribution/changelog.txt
@@ -2,6 +2,7 @@
------------------------------------------------------------------------
- Feature: [#24468] [Plugin] Add awards to plugin API.
- Feature: [#24702] [Plugin] Add bindings for missing cheats (forcedParkRating, ignoreRidePrice, makeAllDestructible).
+- Change: [#25967] Security guards now only walk slowly in crowded areas.
- Fix: [#24598] Cannot load .park files that use official legacy footpaths by accident.
0.4.24 (2025-07-05)
diff --git a/openrct2.deps.targets b/openrct2.deps.targets
index 9c2bb75b5f..8f428cde8d 100644
--- a/openrct2.deps.targets
+++ b/openrct2.deps.targets
@@ -224,8 +224,8 @@
b1b1f1b241d2cbff63a1889c4dc5a09bdf769bfb
https://github.com/OpenRCT2/OpenMusic/releases/download/v1.6/openmusic.zip
ba170fa6d777b309c15420f4b6eb3fa25082a9d1
- https://github.com/OpenRCT2/replays/releases/download/v0.0.87/replays.zip
- 6061B53DE346BD853BB997E635AC7374B1A7D2F0
+ https://github.com/OpenRCT2/replays/releases/download/v0.0.89/replays.zip
+ 089CB8EEA76A98028367FDDE72675E9309AB9036
diff --git a/src/openrct2/entity/Staff.cpp b/src/openrct2/entity/Staff.cpp
index 2e81d1353e..faf690a9c1 100644
--- a/src/openrct2/entity/Staff.cpp
+++ b/src/openrct2/entity/Staff.cpp
@@ -889,7 +889,7 @@ void Staff::EntertainerUpdateNearbyPeeps() const
continue;
int16_t z_dist = std::abs(z - guest->z);
- if (z_dist > 48)
+ if (z_dist > kTileRadius / 2)
continue;
int16_t x_dist = std::abs(x - guest->x);
@@ -1644,14 +1644,58 @@ bool Staff::UpdatePatrollingFindSweeping()
return false;
}
+bool Staff::SecurityGuardPathIsCrowded() const
+{
+ // Iterate over tiles within a 3-tile radius (96 units)
+ constexpr auto kTileRadius = 3;
+ constexpr auto kLookupRadius = kCoordsXYStep * kTileRadius;
+ constexpr auto kSecurityPathCrowdedThreshold = 20;
+
+ int16_t guestCount = 0;
+
+ for (int32_t tileX = x - kLookupRadius; tileX <= x + kLookupRadius; tileX += kCoordsXYStep)
+ {
+ for (int32_t tileY = y - kLookupRadius; tileY <= y + kLookupRadius; tileY += kCoordsXYStep)
+ {
+ for (auto* guest : EntityTileList({ tileX, tileY }))
+ {
+ if (guest->x == kLocationNull)
+ continue;
+
+ int16_t zDist = std::abs(z - guest->z);
+ if (zDist > kTileRadius / 2)
+ continue;
+
+ int16_t xDist = std::abs(x - guest->x);
+ if (xDist > kLookupRadius)
+ continue;
+
+ int16_t yDist = std::abs(y - guest->y);
+ if (yDist > kLookupRadius)
+ continue;
+
+ if (!guest->IsActionWalking())
+ continue;
+
+ guestCount++;
+ if (guestCount >= kSecurityPathCrowdedThreshold)
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
void Staff::Tick128UpdateStaff()
{
if (AssignedStaffType != StaffType::Security)
return;
- PeepAnimationGroup newAnimationGroup = PeepAnimationGroup::Alternate;
- if (State != PeepState::Patrolling)
- newAnimationGroup = PeepAnimationGroup::Normal;
+ // Alternate between walking animations based on crowd size
+ auto newAnimationGroup = PeepAnimationGroup::Normal;
+ if (State == PeepState::Patrolling && SecurityGuardPathIsCrowded())
+ newAnimationGroup = PeepAnimationGroup::Alternate;
if (AnimationGroup == newAnimationGroup)
return;
@@ -1665,6 +1709,7 @@ void Staff::Tick128UpdateStaff()
auto& objManager = GetContext()->GetObjectManager();
auto* animObj = objManager.GetLoadedObject(AnimationObjectIndex);
+ // NB: security staff have two animations groups: one regular, and one slow-walking
PeepFlags &= ~PEEP_FLAGS_SLOW_WALK;
if (animObj->IsSlowWalking(newAnimationGroup))
{
diff --git a/src/openrct2/entity/Staff.h b/src/openrct2/entity/Staff.h
index 301b9c51db..b9e7bd0c43 100644
--- a/src/openrct2/entity/Staff.h
+++ b/src/openrct2/entity/Staff.h
@@ -112,6 +112,7 @@ private:
Direction HandymanDirectionRandSurface(uint8_t validDirections) const;
void EntertainerUpdateNearbyPeeps() const;
+ bool SecurityGuardPathIsCrowded() const;
uint8_t GetValidPatrolDirections(const CoordsXY& loc) const;
Direction HandymanDirectionToNearestLitter() const;
diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp
index bf50997b2a..3c971727ab 100644
--- a/src/openrct2/network/NetworkBase.cpp
+++ b/src/openrct2/network/NetworkBase.cpp
@@ -49,7 +49,7 @@ using namespace OpenRCT2;
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
-constexpr uint8_t kNetworkStreamVersion = 0;
+constexpr uint8_t kNetworkStreamVersion = 1;
const std::string kNetworkStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kNetworkStreamVersion);