Skip to content

Commit 80b30e8

Browse files
committed
performance: optimize packet broadcasting by caching entity state snapshots
- Add snapshot caching for entity stats, vitals, and player equipment - Skip redundant broadcasts when state hasn't changed since last send - Clear snapshots when entities leave map/layer/instance - Batch vital updates to only include entities with actual changes - Equipment broadcasts now only send when items visually change This reduces network bandwidth and server CPU during high-entity scenarios (raids, events, crowded maps) by eliminating unnecessary/duped tx of packets to proximity clients while preserving first-send guarantees and correctness. Changes are backward-compatible; clients see no behavioral difference, only faster network synchronization and lower latency impact from packets broadcasting. Signed-off-by: Arufonsu <17498701+Arufonsu@users.noreply.github.com>
1 parent a73e5c7 commit 80b30e8

1 file changed

Lines changed: 121 additions & 13 deletions

File tree

Intersect.Server.Core/Networking/PacketSender.cs

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Intersect.Framework.Core.GameObjects.PlayerClass;
1313
using Intersect.Framework.Core.GameObjects.Resources;
1414
using Intersect.Framework.Core.GameObjects.Variables;
15-
using Intersect.Framework.Core.Network.Packets.Security;
1615
using Intersect.Framework.Core.Security;
1716
using Intersect.GameObjects;
1817
using Intersect.Models;
@@ -27,7 +26,6 @@
2726
using Intersect.Server.General;
2827
using Intersect.Server.Localization;
2928
using Intersect.Server.Maps;
30-
using Intersect.Utilities;
3129
using Microsoft.Extensions.Logging;
3230
using Newtonsoft.Json;
3331

@@ -53,6 +51,19 @@ public static void ResetMetrics()
5351
SentBytes = 0;
5452
}
5553

54+
//Tracks last sent stats per entity so we can skip redundant packet updates.
55+
private sealed record EntityStatsSnapshot(int[] Stats);
56+
57+
private static readonly Dictionary<Guid, EntityStatsSnapshot> _lastEntityStatsSnapshot = new Dictionary<Guid, EntityStatsSnapshot>();
58+
59+
// Tracks last sent vitals per entity for map‑wide batch updates.
60+
private sealed record EntityVitalSnapshot(long[] Vitals, long[] MaxVitals, long CombatTimeRemaining);
61+
62+
private static readonly Dictionary<Guid, EntityVitalSnapshot> _lastEntityVitalSnapshot = new Dictionary<Guid, EntityVitalSnapshot>();
63+
64+
// Tracks last sent equipment item ids per player.
65+
private static readonly Dictionary<Guid, Guid[]> _lastPlayerEquipmentSnapshot = new Dictionary<Guid, Guid[]>();
66+
5667
//PingPacket
5768
public static void SendPing(Client client, bool request = true)
5869
{
@@ -640,24 +651,28 @@ public static void SendNpcAggressionTo(Player player, Npc npc)
640651
public static void SendEntityLeaveMap(Entity en, Guid leftMap)
641652
{
642653
SendDataToMapInstance(leftMap, en.MapInstanceId, new EntityLeftPacket(en.Id, en.GetEntityType(), en.MapId));
654+
ClearEntitySnapshotCache(en);
643655
}
644656

645657
//EntityLeftPacket
646658
public static void SendEntityLeave(Entity en)
647659
{
648660
SendDataToProximityOnMapInstance(en.MapId, en.MapInstanceId, new EntityLeftPacket(en.Id, en.GetEntityType(), en.MapId));
661+
ClearEntitySnapshotCache(en);
649662
}
650663

651664
//EntityLeftPacket
652665
public static void SendEntityLeaveLayer(Entity en, Guid mapInstanceId)
653666
{
654667
SendDataToProximityOnMapInstance(en.MapId, mapInstanceId, new EntityLeftPacket(en.Id, en.GetEntityType(), en.MapId));
668+
ClearEntitySnapshotCache(en);
655669
}
656670

657671
//EntityLeftPacket
658672
public static void SendEntityLeaveInstanceOfMap(Entity en, Guid mapId, Guid mapInstanceId)
659673
{
660674
SendDataToProximityOnMapInstance(mapId, mapInstanceId, new EntityLeftPacket(en.Id, en.GetEntityType(), en.MapId));
675+
ClearEntitySnapshotCache(en);
661676
}
662677

663678
//EntityLeavePacket
@@ -772,6 +787,16 @@ public static void CacheGameDataPacket()
772787
CachedGameDataPacket = new GameDataPacket(gameObjects.ToArray(), CustomColors.Json());
773788
}
774789

790+
private static void ClearEntitySnapshotCache(Entity en)
791+
{
792+
if (en != null)
793+
{
794+
_lastEntityStatsSnapshot.Remove(en.Id);
795+
_lastEntityVitalSnapshot.Remove(en.Id);
796+
_lastPlayerEquipmentSnapshot.Remove(en.Id);
797+
}
798+
}
799+
775800
/// <summary>
776801
/// Sends a global chat message to every user online.
777802
/// </summary>
@@ -945,22 +970,56 @@ public static EntityVitalsPacket GenerateEntityVitalsPacket(Entity en)
945970
//EntityVitalsPacket
946971
public static void SendMapEntityVitalUpdate(MapController map, Entity[] entities, Guid mapInstanceId)
947972
{
948-
// Generate a list of vitals to send to our users!
973+
if (map == null || entities == null || entities.Length == 0)
974+
{
975+
return;
976+
}
977+
949978
var data = new List<EntityVitalData>();
979+
950980
foreach (var entity in entities)
951981
{
952-
data.Add(new EntityVitalData()
982+
if (entity == null)
953983
{
954-
Id = entity.Id,
955-
Type = entity.GetEntityType(),
956-
Vitals = entity.GetVitals(),
957-
MaxVitals = entity.GetMaxVitals(),
958-
CombatTimeRemaining = entity.CombatTimer - Timing.Global.Milliseconds
959-
});
984+
continue;
985+
}
986+
987+
var vitals = entity.GetVitals();
988+
var maxVitals = entity.GetMaxVitals();
989+
var combatRemaining = entity.CombatTimer - Timing.Global.Milliseconds;
990+
991+
var newSnapshot = new EntityVitalSnapshot(vitals, maxVitals, combatRemaining);
992+
993+
if (_lastEntityVitalSnapshot.TryGetValue(entity.Id, out var oldSnapshot))
994+
{
995+
if (oldSnapshot.Vitals.SequenceEqual(newSnapshot.Vitals) &&
996+
oldSnapshot.MaxVitals.SequenceEqual(newSnapshot.MaxVitals) &&
997+
oldSnapshot.CombatTimeRemaining == newSnapshot.CombatTimeRemaining)
998+
{
999+
// Nothing relevant changed, skip this entity.
1000+
continue;
1001+
}
1002+
}
1003+
1004+
_lastEntityVitalSnapshot[entity.Id] = newSnapshot;
1005+
1006+
data.Add(
1007+
new EntityVitalData
1008+
{
1009+
Id = entity.Id,
1010+
Type = entity.GetEntityType(),
1011+
Vitals = vitals,
1012+
MaxVitals = maxVitals,
1013+
CombatTimeRemaining = combatRemaining
1014+
}
1015+
);
9601016
}
9611017

962-
// Send the data to the surroundings!
963-
SendDataToProximityOnMapInstance(map.Id, mapInstanceId, new MapEntityVitalsPacket(map.Id, data.ToArray()));
1018+
// Only send if at least one entity actually changed.
1019+
if (data.Count > 0)
1020+
{
1021+
SendDataToProximityOnMapInstance(map.Id, mapInstanceId, new MapEntityVitalsPacket(map.Id, data.ToArray()));
1022+
}
9641023
}
9651024

9661025
public static void SendMapEntityStatusUpdate(MapController map, Entity[] entities, Guid mapInstanceId)
@@ -989,7 +1048,26 @@ public static void SendEntityStats(Entity en)
9891048
return;
9901049
}
9911050

992-
SendDataToProximityOnMapInstance(en.MapId, en.MapInstanceId, GenerateEntityStatsPacket(en), null, TransmissionMode.Any);
1051+
// Build current stats array (same logic as GenerateEntityStatsPacket)
1052+
var stats = new int[Enum.GetValues<Stat>().Length];
1053+
for (var i = 0; i < stats.Length; i++)
1054+
{
1055+
stats[i] = en.Stat[i].Value();
1056+
}
1057+
1058+
var newSnapshot = new EntityStatsSnapshot(stats);
1059+
1060+
if (_lastEntityStatsSnapshot.TryGetValue(en.Id, out var oldSnapshot))
1061+
{
1062+
if (oldSnapshot.Stats.SequenceEqual(newSnapshot.Stats))
1063+
{
1064+
// No visible stat changes; skip the packet update entirely.
1065+
return;
1066+
}
1067+
}
1068+
1069+
_lastEntityStatsSnapshot[en.Id] = newSnapshot;
1070+
SendDataToProximityOnMapInstance(en.MapId, en.MapInstanceId, new EntityStatsPacket(en.Id, en.GetEntityType(), en.MapId, stats), null, TransmissionMode.Any);
9931071
}
9941072

9951073
//EntityVitalsPacket
@@ -1295,6 +1373,36 @@ public static void SendPlayerEquipmentTo(Player forPlayer, Player en)
12951373
//EquipmentPacket
12961374
public static void SendPlayerEquipmentToProximity(Player en)
12971375
{
1376+
if (en == null)
1377+
{
1378+
return;
1379+
}
1380+
1381+
var slots = Options.Instance.Equipment.Slots.Count;
1382+
var equipment = new Guid[slots];
1383+
1384+
for (var i = 0; i < slots; i++)
1385+
{
1386+
if (en.Equipment[i] == -1 || en.Items[en.Equipment[i]].ItemId == Guid.Empty)
1387+
{
1388+
equipment[i] = Guid.Empty;
1389+
}
1390+
else
1391+
{
1392+
equipment[i] = en.Items[en.Equipment[i]].ItemId;
1393+
}
1394+
}
1395+
1396+
if (_lastPlayerEquipmentSnapshot.TryGetValue(en.Id, out var last))
1397+
{
1398+
if (last.Length == equipment.Length && last.SequenceEqual(equipment))
1399+
{
1400+
// Nothing changed, skip entirely.
1401+
return;
1402+
}
1403+
}
1404+
1405+
_lastPlayerEquipmentSnapshot[en.Id] = equipment;
12981406
SendDataToProximityOnMapInstance(en.MapId, en.MapInstanceId, GenerateEquipmentPacket(null, en), null, TransmissionMode.Any);
12991407
SendPlayerEquipmentTo(en, en);
13001408
}

0 commit comments

Comments
 (0)