diff --git a/CS2MultiplayerMod/Game/Sync/Channels/City/StatisticsStateChannel.cs b/CS2MultiplayerMod/Game/Sync/Channels/City/StatisticsStateChannel.cs index d309aa5..a1c2aac 100644 --- a/CS2MultiplayerMod/Game/Sync/Channels/City/StatisticsStateChannel.cs +++ b/CS2MultiplayerMod/Game/Sync/Channels/City/StatisticsStateChannel.cs @@ -10,17 +10,31 @@ namespace CS2MultiplayerMod.Game.Sync.Channels { /// /// Replicates the cumulative life-event counters - deaths, births, move-ins, - /// move-aways, crime, mail - host -> clients, so both players' statistics panels show - /// the same numbers between full-world resyncs. + /// move-aways, crime, mail, transport passengers and cargo - host -> clients, so + /// both players' statistics panels show the same numbers between full-world resyncs. /// Mechanism: the host snapshots each counter's lifetime value and the client feeds it /// through the game's own event pipeline, the same path the deathcare/crime systems use, /// so the statistics buffers stay internally consistent and serializable. + /// Only event-accumulated lifetime totals ride here. Gauges the simulation rewrites + /// itself (population, money, happiness, current tourists) are deliberately excluded: + /// forcing those through the event queue fights the writer, the same reason the + /// population channel was retired. /// public sealed class StatisticsStateChannel : IStateChannel { public const byte Id = 10; public byte ChannelId => Id; + /// + /// Statistic parameter holding the event-accumulated lifetime total. All + /// counters synced here are lifetime totals at this parameter, which is why + /// the generic delta mechanism applies to every entry unchanged. + /// + private const int LifetimeParameter = 0; + + /// Upper bound for entries in one snapshot (the table holds 20). + private const int MaxEntriesPerSnapshot = 64; + private static readonly StatisticType[] Synced = { StatisticType.DeathRate, // "deaths" — cumulative count of citizen deaths @@ -31,6 +45,23 @@ public sealed class StatisticsStateChannel : IStateChannel StatisticType.EscapedArrestCount, StatisticType.CollectedMail, StatisticType.DeliveredMail, + // Transport ridership and cargo: lifetime boarding/load totals shown in the + // transport info summaries. Same event-accumulated shape as the counters + // above, read at parameter 0 (the total), so the generic delta mechanism + // applies unchanged. The payload stays self-describing (count + type + + // value), so peers without these entries simply exchange fewer of them. + StatisticType.PassengerCountBus, + StatisticType.PassengerCountSubway, + StatisticType.PassengerCountTram, + StatisticType.PassengerCountTrain, + StatisticType.PassengerCountTaxi, + StatisticType.PassengerCountAirplane, + StatisticType.PassengerCountShip, + StatisticType.PassengerCountFerry, + StatisticType.CargoCountTruck, + StatisticType.CargoCountTrain, + StatisticType.CargoCountShip, + StatisticType.CargoCountAirplane, }; private CityStatisticsSystem _stats; @@ -56,7 +87,7 @@ public bool Capture(EntityManager em, NetworkWriter writer) for (int i = 0; i < Synced.Length; i++) { writer.WriteByte((byte)Synced[i]); - writer.WriteLong(stats.GetStatisticValueLong(Synced[i], 0)); + writer.WriteLong(stats.GetStatisticValueLong(Synced[i], LifetimeParameter)); } return true; } @@ -71,13 +102,21 @@ public void Apply(EntityManager em, NetworkReader reader) { CityStatisticsSystem stats = Resolve(em); int count = reader.ReadByte(); + if (count < 0 || count > MaxEntriesPerSnapshot) + { + WarnOnce("apply", new System.IO.InvalidDataException( + "Implausible statistics entry count: " + count + ".")); + return; + } try { for (int i = 0; i < count; i++) { - var type = (StatisticType)reader.ReadByte(); + byte rawType = reader.ReadByte(); long hostValue = reader.ReadLong(); - long localValue = stats.GetStatisticValueLong(type, 0); + if (!System.Enum.IsDefined(typeof(StatisticType), (int)rawType)) continue; + var type = (StatisticType)rawType; + long localValue = stats.GetStatisticValueLong(type, LifetimeParameter); // Where this counter is headed: the value it will hold once the events already // queued are processed. Once the local value has caught up to that target the @@ -95,7 +134,7 @@ public void Apply(EntityManager em, NetworkReader reader) queue.Enqueue(new StatisticsEvent { m_Statistic = type, - m_Parameter = 0, + m_Parameter = LifetimeParameter, m_Change = delta, }); _inFlightTarget[type] = hostValue;