github Xian55/HermesProxy v4.5.0
Release v4.5.0 (2026-09-13)

2 hours ago

Source-generated packet dispatch

Every inbound packet used to find its handler through runtime reflection. This release replaces
that with tables built at compile time by a Roslyn source generator — 827 opcodes, in both
directions: the modern client's CMSGs and the legacy emulator's SMSGs.

Nothing changes in how you run it. No new configuration, no behaviour change, no wire change.
If the proxy worked for you on v4.4.29 it works the same way here, and you do not need to touch
your config.

What you may notice

Per-packet allocation on the inbound path is gone or down to the minimum the data requires, which
means less garbage-collection pressure over a long session. In a battleground against bots,
client-to-server allocation fell from ~18.3 KB per packet to ~1.5 KB, and GC pressure at a matched
packet rate went from 23 gen0 collections per minute to 4-8, with pause time at 0.02-0.09%.

That live figure is not all down to dispatch — a pooled-buffer disposal fix in the same release
accounts for most of it. The dispatch conversion itself is worth about 360 bytes per packet.

Measured in isolation, on an idle Apple M4:

packet shape before after
value-type fields 121.90 ns, 368 B 1.95 ns, 0 B
two strings 163.36 ns, 552 B 23.64 ns, 104 B
40-element array 239.21 ns, 1008 B 40.93 ns, 216 B

62-68x on packets made only of value types, with allocation reaching zero. Packets carrying
strings or lists reach 5.8-6.9x and stop at the floor the data itself costs — 104 B is the two
strings, 216 B is the list.

Two figures this release does not claim. Reflective dispatch was only 1.3% of CPU to begin
with, so this is an allocation and startup win rather than a throughput one. And on the legacy
(emulator-facing) side the per-packet cost was already effectively zero — 0.810 ns before against
0.767 ns after, which is noise. That half of the work bought a uniform dispatch shape and removed
a reflection scan that previously ran once per connection at startup, not faster packets.

Verified on

Played end to end on three client versions, each compared against a pre-change run on the same
backend, with no opcode newly unhandled and no handler exceptions:

client backend
3.4.3 WotLK Classic AzerothCore with playerbots, two full battlegrounds
1.14.2 Classic Era cMaNGOS vanilla
2.5.3 TBC Classic cMaNGOS TBC

Plus 2471 automated tests, including byte-level equivalence for every converted packet against a
frozen copy of the reader it replaced.

If you build on this codebase

This is why the minor version moved rather than the patch. PacketHandlerAttribute and both
reflective handler registries are removed. Anything that discovered handlers by attribute, or
subclassed ClientPacket to add one, needs updating:

  • Inbound client packets are now readonly record struct with a separate codec, and handlers are
    public static methods marked [HandlesCmsg(Opcode.X)] taking (in TPacket, in SessionContext).
  • Legacy emulator handlers stay instance methods on WorldClient but are marked [HandlesSmsg].
  • Version differences are declared as attribute data (AddedIn / RemovedIn) and resolved once
    when the table is built, instead of being re-evaluated inside the handler on every packet.
  • A new generated reference, docs/opcode-coverage.md, lists which opcodes are handled for which
    build and where packet layouts diverge between client versions.

Forks tracking master should expect conflicts in World/Server/PacketHandlers/ and
World/Client/PacketHandlers/; handler bodies were moved rather than rewritten, so the conflicts
are mostly relocation rather than logic changes.

What's Changed

Performance

  • Source-generated packet dispatch: 827 opcodes off runtime reflection by @Xian55 in #292

Full Changelog: v4.4.29...v4.5.0

Don't miss a new HermesProxy release

NewReleases is sending notifications on new releases.