--- name: modernuo-networking description: > Trigger when creating or modifying packets, working with NetState, SpanWriter, SpanReader, or implementing network protocol handlers. --- # ModernUO Networking & Packets ## When This Activates - Creating new packets (outgoing or incoming) - Modifying existing packet handlers - Working with `NetState`, `SpanWriter`, `SpanReader` - Implementing network protocol features ## Key Rules 1. **Outgoing packets**: Static `Create*` method fills `Span`, extension `Send*` method on `NetState` 2. **Incoming packets**: Register with function pointers in `Configure()` 3. **Use `stackalloc`** for small fixed-size outgoing packets 4. **Always check `ns.CannotSendPackets()`** before sending 5. **Big-endian by default** -- use `WriteLE()` for little-endian ## Outgoing Packet Pattern ### Step 1: Create Extension Method on NetState ```csharp public static class OutgoingMyPackets { public const int MyPacketLength = 12; public static void SendMyPacket(this NetState ns, Serial target, int value) { if (ns.CannotSendPackets()) return; var buffer = stackalloc byte[MyPacketLength].InitializePacket(); CreateMyPacket(buffer, target, value); ns.Send(buffer); } public static void CreateMyPacket(Span buffer, Serial target, int value) { if (buffer[0] != 0) // Already initialized check return; var writer = new SpanWriter(buffer); writer.Write((byte)0xBF); // Packet ID writer.Write((ushort)12); // Length writer.Write((ushort)0x99); // Sub-command writer.Write(target); // Serial (4 bytes) writer.Write((short)value); // Value (2 bytes) } } ``` ### Step 2: Call from Game Code ```csharp mobile.NetState.SendMyPacket(target.Serial, 42); // Or for all nearby players: foreach (var ns in mobile.GetClientsInRange(18)) { ns.SendMyPacket(target.Serial, 42); } ``` ### Variable-Length Outgoing Packets ```csharp public static void SendMyDynamicPacket(this NetState ns, string name) { if (ns.CannotSendPackets()) return; var writer = new SpanWriter(stackalloc byte[64]); // Or use pooled buffer for large packets writer.Write((byte)0x99); // Packet ID writer.Write((ushort)0); // Placeholder for length writer.WriteBigUniNull(name); // Unicode string with null terminator writer.WritePacketLength(); // Fill in actual length ns.Send(writer.Span); } ``` ## Incoming Packet Pattern ### Step 1: Register Handler in Configure() ```csharp public static class IncomingMyPackets { public static unsafe void Configure() { IncomingPackets.Register(0x99, 12, true, &MyPacketHandler); // ^ID ^len ^inGameOnly ^handler // len=0 for variable-length packets } public static void MyPacketHandler(NetState state, SpanReader reader) { var from = state.Mobile; if (from == null) return; var targetSerial = (Serial)reader.ReadUInt32(); var value = reader.ReadInt16(); var target = World.FindMobile(targetSerial); if (target != null) { // Process packet } } } ``` ### Encoded Packet Registration ```csharp public static unsafe void Configure() { IncomingPackets.RegisterEncoded(0x28, true, &GuildGumpRequest); // ^subID ^inGame ^handler } public static void GuildGumpRequest(NetState state, IEntity target, EncodedReader reader) { // Handle encoded packet } ``` ## SpanWriter Reference ```csharp // Constructors var writer = new SpanWriter(Span buffer); var writer = new SpanWriter(stackalloc byte[64]); var writer = new SpanWriter(int capacity, bool resize = false); // Integer writes (big-endian by default) writer.Write(bool value); writer.Write(byte value); writer.Write(sbyte value); writer.Write(short value); // Big-endian writer.Write(ushort value); // Big-endian writer.Write(int value); // Big-endian writer.Write(uint value); // Big-endian writer.Write(long value); writer.Write(Serial serial); // 4 bytes, big-endian // Little-endian variants writer.WriteLE(short value); writer.WriteLE(ushort value); writer.WriteLE(int value); writer.WriteLE(uint value); // String writes writer.WriteAscii(string value); writer.WriteAsciiNull(string value); // Null-terminated writer.WriteAscii(string value, int fixedLength); writer.WriteLatin1(string value); writer.WriteLatin1Null(string value); writer.WriteBigUni(string value); // UTF-16 big-endian writer.WriteBigUniNull(string value); writer.WriteLittleUni(string value); // UTF-16 little-endian writer.WriteLittleUniNull(string value); writer.WriteUTF8(string value); writer.WriteUTF8Null(string value); // Utilities writer.Write(ReadOnlySpan data); writer.Clear(int count); // Write zeros writer.Seek(int offset, SeekOrigin origin); writer.WritePacketLength(); // Fill in length at position 1-2 writer.EnsureCapacity(int capacity); writer.Dispose(); // Return pooled buffer if any // Properties writer.Position; // Current write position writer.Capacity; // Buffer size writer.Span; // ReadOnlySpan of written data ``` ## SpanReader Reference ```csharp // Constructor var reader = new SpanReader(ReadOnlySpan data); // Integer reads (big-endian by default) reader.ReadByte(); reader.ReadBoolean(); // byte > 0 reader.ReadSByte(); reader.ReadInt16(); // Big-endian reader.ReadUInt16(); // Big-endian reader.ReadInt32(); // Big-endian reader.ReadUInt32(); // Big-endian reader.ReadInt64(); reader.ReadUInt64(); // Little-endian variants reader.ReadInt16LE(); reader.ReadUInt16LE(); reader.ReadUInt32LE(); // String reads reader.ReadAscii(int fixedLength = -1); reader.ReadAsciiSafe(int fixedLength = -1); // Filters control chars reader.ReadLatin1(int fixedLength = -1); reader.ReadLatin1Safe(int fixedLength = -1); reader.ReadBigUni(int fixedLength = -1); reader.ReadBigUniSafe(int fixedLength = -1); reader.ReadLittleUni(int fixedLength = -1); reader.ReadUTF8(int fixedLength = -1); // Utilities reader.Seek(int offset, SeekOrigin origin); reader.Read(Span destination); // Properties reader.Position; // Current read position reader.Length; // Total data length reader.Remaining; // Bytes remaining ``` ## Player-Facing Message APIs For chat / system messages / overhead text, use the convenience methods on `Mobile` and `Item` rather than building packets manually. They handle stackalloc sizing, spatial queries, and visibility filtering, and each has a `ref RawInterpolatedStringHandler` overload for zero-allocation interpolation. ### Mobile ```csharp // Self-message mob.SendMessage(int hue, ROS text); mob.SendLocalizedMessage(int number, ROS args = default, int hue = 0x3B2); mob.SendAsciiMessage(int hue, ROS text); // Speech (broadcast in range) mob.Say(ROS text); // SpeechHue mob.Emote(ROS text); // EmoteHue mob.Whisper(ROS text); // WhisperHue, short range mob.Yell(ROS text); // YellHue, long range // Overhead (3 visibility variants × text/localized × affix) mob.PublicOverheadMessage(MessageType, int hue, bool ascii, ROS text, ...); mob.PrivateOverheadMessage(MessageType, int hue, int number, ROS args, NetState); mob.LocalOverheadMessage(MessageType, int hue, bool ascii, ROS text); mob.NonlocalOverheadMessage(MessageType, int hue, int number, ROS args = default); ``` ### Item ```csharp item.PublicOverheadMessage(MessageType, int hue, bool ascii, ROS text); item.PublicOverheadMessage(MessageType, int hue, int number, ROS args = default); item.SendLocalizedMessageTo(Mobile to, int number, ROS args = default); item.SendLocalizedMessageTo(Mobile to, int number, int hue, ROS args = default); item.SendMessageTo(Mobile to, ROS text, int hue = 0x3B2); ``` ### Zero-allocation interpolation ```csharp // Compiler picks the handler overload — no string allocated mob.SendMessage($"You have {gold:N0} gold"); mob.Say($"Hello, {target.Name}!"); item.SendLocalizedMessageTo(player, cliloc, $"{a}\t{b}"); ``` Several call-site shapes (ternaries with interpolated branches, `.ToString()` inside holes, pre-built `var msg = $"..."` locals, etc.) silently defeat handler binding and allocate a `string`. See `dev-docs/string-handling.md` § "Interpolation Anti-Patterns" for the list and fixes. For lowercase output, use `:L`: ```csharp mob.SendMessage($"You earned a {rank:L} trophy!"); // "gold" not "Gold" ``` ### Implementation note The 7 spatial-broadcast variants (`Public/NonlocalOverheadMessage` × text/localized/affix) route through generic `OutgoingMessagePackets.Broadcast*` helpers parameterized over a `private readonly struct` filter that encapsulates the visibility predicate. The `where TFilter : struct, IBroadcastFilter` constraint specializes per filter type and keeps dispatch zero-alloc. If you add a new spatial-message API, add a filter struct and call the existing helper rather than duplicating the loop. --- ## Common Packet Patterns ### Sound Effect ```csharp ns.SendSoundEffect(0x1E5, target); ``` ### Mobile Animation ```csharp ns.SendMobileAnimation(mobile.Serial, action, frameCount, repeatCount, forward, repeat, delay); ``` ### Damage ```csharp ns.SendDamage(target.Serial, amount); ``` ## Anti-Patterns - **Not checking `CannotSendPackets()`**: Player may have disconnected - **Using `new byte[]` for packets**: Use `stackalloc` for fixed-size, `SpanWriter` for variable - **Wrong endianness**: UO protocol is big-endian; only use `WriteLE` when spec requires it - **Not calling `InitializePacket()`**: Required for `Create*` pattern with static buffer reuse ## Real Examples - Mobile packets: `Projects/Server/Network/Packets/OutgoingMobilePackets.cs` - Item packets: `Projects/Server/Network/Packets/OutgoingItemPackets.cs` - Damage packets: `Projects/Server/Network/Packets/OutgoingDamagePackets.cs` - Effect packets: `Projects/Server/Network/Packets/OutgoingEffectPackets.cs` - Incoming registration: `Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs` - Movement handler: `Projects/UOContent/Network/Packets/IncomingMovementPackets.cs` - SpanWriter: `Projects/Server/Buffers/SpanWriter.cs` - SpanReader: `Projects/Server/Buffers/SpanReader.cs` ## See Also - `dev-docs/networking-packets.md` - Complete networking documentation - `dev-docs/claude-skills/modernuo-threading.md` - Network I/O threading context