This project enables play on existing legacy WoW emulation cores using the modern clients. It serves as a translation layer, converting all network traffic to the appropriate format each side can understand.
There are 4 major components to the application:
- The modern BNetServer to which the client initially logs into.
- The legacy AuthClient which will in turn login to the remote authentication server (realmd).
- The modern WorldServer to which the game client will connect once a realm has been selected.
- The legacy WorldClient which communicates with the remote world server (mangosd).
HermesProxy translates between modern WoW Classic clients and legacy private server emulators.
These are the Blizzard WoW Classic client versions you can use:
| Version | Expansion | Build Range | Notes |
|---|---|---|---|
| 1.14.0 | Classic Era | 39802 - 40618 | Season of Mastery |
| 1.14.1 | Classic Era | 40487 - 42032 | |
| 1.14.2 | Classic Era | 41858 - 42597 | |
| 2.5.2 | TBC Classic | 39570 - 41510 | |
| 2.5.3 | TBC Classic | 41402 - 42598 |
These are the private server versions HermesProxy can connect to:
| Version | Expansion | Build | Server Software |
|---|---|---|---|
| 1.12.1 | Vanilla | 5875 | CMaNGOS, VMaNGOS, etc. |
| 1.12.2 | Vanilla | 6005 | CMaNGOS, VMaNGOS, etc. |
| 1.12.3 | Vanilla | 6141 | CMaNGOS, VMaNGOS, etc. |
| 2.4.3 | TBC | 8606 | CMaNGOS, etc. |
The proxy automatically selects the best legacy version based on your client:
| Modern Client | Connects To |
|---|---|
| 1.14.x | 1.12.x (Vanilla) |
| 2.5.x | 2.4.3 (TBC) |
Note: Keep Optimize Network for Speed enabled (it's under System -> Network), otherwise you will get kicked every now and then.
- Edit the app's config to specify the exact versions of your game client and the remote server, along with the address.
- Go into your game folder, in the Classic or Classic Era subdirectory, and edit WTF/Config.wtf to set the portal to 127.0.0.1.
- Download Arctium Launcher into the main game folder, and then run it
with
--staticseed --version=ClassicErafor vanilla or--staticseed --version=Classicfor TBC. - Start the proxy app and login through the game with your usual credentials.
HermesProxy provides some internal chat commands:
| Command | Description |
|---|---|
!qcomplete <questId> |
Manually marks a quest as already completed (useful for quest helper addons) |
!quncomplete <questId> |
Unmarks a quest as completed |
| Argument | Description |
|---|---|
--config <filename> |
Specify a config file (default: HermesProxy.config) |
--set <key>=<value> |
Override a specific config value at runtime |
--no-version-check |
Disable the check for newer versions on startup |
Examples:
# Use a custom config file
HermesProxy --config MyServer.config
# Override server address
HermesProxy --set ServerAddress=logon.example.com
# Override multiple values
HermesProxy --set ServerAddress=logon.example.com --set ServerPort=3725
# Combine config file with overrides
HermesProxy --config Production.config --set DebugOutput=trueConfiguration is stored in HermesProxy.config (XML format). All settings can also be overridden via --set command line arguments.
| Setting | Default | Description |
|---|---|---|
ServerAddress |
127.0.0.1 |
Address of the legacy server (what you'd use in SET REALMLIST) |
ServerPort |
3724 |
Port of the legacy authentication server |
ExternalAddress |
127.0.0.1 |
Your IP address for others to connect (for hosting) |
| Setting | Default | Valid Values |
|---|---|---|
ClientBuild |
40618 (1.14.0) |
40618 (1.14.0)41794 (1.14.1)42597 (1.14.2)40892 (2.5.2)42328 (2.5.3) |
ServerBuild |
auto |
auto5875 (1.12.1)8606 (2.4.3) |
ClientSeed |
179D3DC3235629D07113A9B3867F97A7 |
32-character hex string (16 bytes) |
All ports must be in the range 1-65535.
| Setting | Default | Description |
|---|---|---|
BNetPort |
1119 |
Port for BNet/Portal server (use this in your Config.wtf) |
RestPort |
8081 |
Port for the REST API server |
RealmPort |
8084 |
Port for the realm server |
InstancePort |
8086 |
Port for the instance server |
| Setting | Default | Valid Values | Description |
|---|---|---|---|
ReportedOS |
OSX |
OSX, Win, etc. |
OS identifier sent to the legacy server |
ReportedPlatform |
x86 |
x86, x64 |
Platform identifier sent to legacy server |
| Setting | Default | Description |
|---|---|---|
DebugOutput |
false |
Print additional debug information to console |
PacketsLog |
true |
Save each session's packets to files in PacketsLog directory |
SpanStatsLog |
false |
Log statistics about Span-based packet serialization (for profiling) |
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="ClientBuild" value="40618" />
<add key="ServerBuild" value="auto" />
<add key="ServerAddress" value="logon.example.com" />
<add key="ServerPort" value="3724" />
<add key="BNetPort" value="1119" />
<add key="DebugOutput" value="false" />
<add key="PacketsLog" value="true" />
</appSettings>
</configuration>HermesProxy has been extensively optimized to minimize latency and memory allocations in packet handling hot paths.
The packet serialization system uses Span<T> and ref struct types for zero-allocation packet writing and reading:
SpanPacketWriter vs ByteBuffer (Write Operations)
| Operation | ByteBuffer | SpanWriter | Speedup | Memory |
|---|---|---|---|---|
| WriteInt64 | 93.37 ns | 0.29 ns | ~317x | 80B → 0B |
| WriteVector3 | 102.99 ns | 0.68 ns | ~151x | 88B → 0B |
| WriteMixed | 109.30 ns | 1.29 ns | ~85x | 96B → 0B |
SpanPacketReader vs ByteBuffer (Read Operations)
| Operation | ByteBuffer | SpanReader | Speedup | Memory |
|---|---|---|---|---|
| ReadInt64 | 157.98 ns | 0.08 ns | ~1948x | 48B → 0B |
| ReadVector3 | 178.31 ns | 0.75 ns | ~238x | 48B → 0B |
| ReadCString | 294.61 ns | 23.51 ns | ~12.5x | 104B → 56B |
The core ByteBuffer class has been refactored for improved performance:
- ArrayPool-based buffer management reduces GC pressure
- Direct
BinaryPrimitivesusage eliminates BinaryReader/BinaryWriter overhead MemoryStream.ToArray()optimization forGetData():
| Buffer Size | Original | Optimized | Speedup |
|---|---|---|---|
| Small | 46.87 ns | 10.31 ns | ~4.5x |
| Medium | 649.49 ns | 70.88 ns | ~9.2x |
| Large | 36,383.19 ns | 4,234.92 ns | ~8.6x |
- Enum Conversions: Cached name-based mappings replace
Enum.Parse(typeof(T), x.ToString())pattern (8-25x speedup, 95% memory reduction) - Opcode Lookups:
FrozenDictionaryfor O(1) opcode resolution - WowGuid: Refactored to value-type record structs eliminating heap allocations
- NetworkThread: O(1) socket removal with
ConcurrentQueue - BnetTcpSession: Zero-allocation buffer management with
Span<T> - Movement Handlers: Fixed monster/pet movement zig-zag at tile boundaries
This section tracks the progress of implementing ISpanWritable interface for server packets to enable zero-allocation span-based packet writing.
| Metric | Count |
|---|---|
| Packets WITH ISpanWritable | 272 |
| Packets WITHOUT ISpanWritable | 49 |
| Total ServerPackets | 321 |
| Coverage | 84.7% |
Click to expand full list
PongWaitQueueFinishResumeComms
AuctionHelloResponseAuctionClosedNotification(uses AuctionPacketHelpers for ItemInstance)AuctionOwnerBidNotification(uses AuctionPacketHelpers for ItemInstance)AuctionWonNotification(uses AuctionPacketHelpers for ItemInstance)AuctionOutbidNotification(uses AuctionPacketHelpers for ItemInstance)
BattlegroundInitBattlegroundPlayerLeftOrJoinedAreaSpiritHealerTimePvPCreditPlayerSkinned
CreateCharDeleteCharLoginVerifyWorldCharacterLoginFailedLogoutResponseLogoutCompleteLogoutCancelAckLogXPGainPlayedTimeInspectHonorStatsResultClassicInspectHonorStatsResultTBCGenerateRandomCharacterNameResult(bounded string - player name)CharacterRenameResult(bounded string - player name)
STextEmoteChatPlayerNotfound(bounded string - player name)
ClientCacheVersion
AttackSwingErrorSAttackStartSAttackStopCancelCombatAIReactionPartyKillLog
CanDuelResultDuelRequestedDuelCountdownDuelCompleteDuelInBoundsDuelOutOfBoundsDuelWinner(bounded string - 2 player names)
GameObjectDespawnGameObjectResetStateGameObjectCustomAnimFishNotHookedFishEscaped
GroupUninviteReadyCheckStartedReadyCheckResponseReadyCheckCompletedSendRaidTargetUpdateSingleSendRaidTargetUpdateAll(capped 8 raid markers)SummonRequestMinimapPingRandomRollGroupDecline(bounded string - player name)GroupNewLeader(bounded string - player name)PartyCommandResult(bounded string - player name)
GuildCommandResult(bounded string - guild/player name)GuildSendRankChangeGuildEventDisbandedGuildEventRanksUpdatedGuildEventTabAddedGuildEventBankMoneyChangedGuildEventTabTextChangedPlayerTabardVendorActivatePlayerSaveGuildEmblemGuildBankRemainingWithdrawMoneyGuildEventPlayerJoined(bounded string - player name)GuildEventPlayerLeft(bounded string - player names)GuildEventNewLeader(bounded string - 2 player names)GuildEventPresenceChange(bounded string - player name)GuildInviteDeclined(bounded string - player name)
UpdateInstanceOwnershipUpdateLastInstanceInstanceResetInstanceResetFailedResetFailedNotifyInstanceSaveCreatedRaidGroupOnlyRaidInstanceMessage
SetProficiencyBuySucceededBuyFailedSellResponseReadItemResultFailedReadItemResultOKSocketGemsSuccessDurabilityDamageDeathItemCooldownItemEnchantTimeUpdateEnchantmentLogItemPushResult(uses ItemPacketHelpers for ItemInstance)InventoryChangeFailure(conditional fields based on BagResult)
LootReleaseResponseLootMoneyNotifyCoinRemovedLootRemovedLootRollsCompleteMasterLootCandidateList(capped 40 raid members)LootList(optional fields)LootResponse(capped 16 items, 4 currencies)
NotifyReceivedMailMailCommandResult
BindPointUpdatePlayerBoundServerTimeOffsetTutorialFlagsCorpseReclaimDelayTimeSyncRequestWeatherPktStartLightningStormLoginSetTimeSpeedAreaTriggerMessageDungeonDifficultySetInitialSetupCorpseLocationDeathReleaseLocStandStateUpdateExplorationExperiencePlayMusicPlaySound(version-specific)PlayObjectSound(version-specific)TriggerCinematicStartMirrorTimerPauseMirrorTimerStopMirrorTimerConquestFormulaConstantsSeasonInfoInvalidatePlayerZoneUnderAttack
MonsterMove(capped 64 waypoints, real usage: 1-2 points)MoveUpdateMoveTeleport(optional Vehicle + TransportGUID)TransferPending(optional Ship + TransferSpellID)TransferAbortedNewWorldMoveSplineSetSpeedMoveSetSpeedMoveUpdateSpeedMoveSplineSetFlagMoveSetFlagMoveSetCollisionHeightMoveKnockBackMoveUpdateKnockBackSuspendTokenResumeTokenControlUpdate
GossipComplete(version-specific)BinderConfirmShowBankTrainerBuyFailedRespecWipeConfirmSpiritHealerConfirm
PetitionSignResultsTurnInPetitionResultPetitionRenameGuildResponse(bounded string - guild name)
PetClearSpellsPetActionSoundPetStableResult
QueryTimeResponseQueryPetNameResponse(bounded string - pet name + declined names)
QuestGiverQuestFailedQuestUpdateStatusQuestUpdateAddCreditQuestUpdateAddCreditSimpleQuestPushResult
SetFactionVisible
ConnectionStatus
CancelAutoRepeatSpellPrepareCastFailedPetCastFailedSpellFailureSpellFailedOtherCooldownEventClearCooldownCooldownCheatSpellDelayedSpellChannelStart(optional InterruptImmunities + HealPrediction)SpellChannelUpdateSpellInstakillLogPlaySpellVisualKitTotemCreatedResurrectRequest(bounded string - player name)LearnedSpells(capped 8 spells)UnlearnedSpells(capped 8 spells)SendUnlearnSpells(capped 8 spells)SpellCooldownPkt(capped 64 cooldowns)SendSpellHistory(capped 64 entries)SendSpellCharges(capped 16 entries)SpellEnergizeLog(optional SpellCastLogData, capped 10 power entries)SpellDamageShield(optional SpellCastLogData, capped 10 power entries)EnvironmentalDamageLog(optional SpellCastLogData, capped 10 power entries)SpellHealLog(optional SpellCastLogData + ContentTuning, high-frequency)SpellNonMeleeDamageLog(optional SpellCastLogData + ContentTuning, high-frequency)SetSpellModifier(capped 8 modifiers × 8 data entries, high-frequency)
ArenaTeamCommandResult(bounded string - team + player names)ArenaTeamInvite(bounded string - team + player names)
TaxiNodeStatusPktNewTaxiPathActivateTaxiReplyPkt
PowerUpdate(capped 16 power types)
UpdateWorldState
These packets cannot implement ISpanWritable due to dynamic lists or complex data structures that cannot determine MaxSize at compile time.
Dynamic Lists / Complex Data
| Packet | File | Blocking Field(s) |
|---|---|---|
AuctionListMyItemsResult |
AuctionPackets.cs | List<AuctionItem> |
AuctionListItemsResult |
AuctionPackets.cs | List<AuctionItem> |
PVPMatchStatisticsMessage |
BattleGroundPackets.cs | List<PVPMatchPlayerStatistics> |
EnumCharactersResult |
CharacterPackets.cs | List<CharacterListEntry> Characters |
GetAccountCharacterListResult |
CharacterPackets.cs | List<AccountCharacterListEntry> |
InspectResult |
CharacterPackets.cs | Multiple lists (talents, glyphs, items) |
AttackerStateUpdate |
CombatPackets.cs | Complex damage info with lists |
PartyUpdate |
GroupPackets.cs | List<PartyPlayerInfo> PlayerList |
PartyMemberPartialState |
GroupPackets.cs | Complex nested state |
PartyMemberFullState |
GroupPackets.cs | Complex nested state |
QueryGuildInfoResponse |
GuildPackets.cs | Multiple rank names |
GuildRoster |
GuildPackets.cs | List<GuildRosterMemberData> |
GuildRanks |
GuildPackets.cs | List<GuildRankData> |
GuildBankQueryResults |
GuildPackets.cs | List<GuildBankItemInfo> |
GuildBankLogQueryResults |
GuildPackets.cs | List<GuildBankLogEntry> |
DBReply |
HotfixPackets.cs | Dynamic data blob |
AvailableHotfixes |
HotfixPackets.cs | List<HotfixRecord> |
HotfixConnect |
HotfixPackets.cs | List<HotfixRecord> |
HotFixMessage |
HotfixPackets.cs | List<HotfixData> |
MailListResult |
MailPackets.cs | List<MailListEntry> |
GossipMessagePkt |
NPCPackets.cs | List<GossipOption>, List<GossipQuest> |
VendorInventory |
NPCPackets.cs | List<VendorItem> |
PetSpells |
PetPackets.cs | List<uint> ActionButtons, List<PetSpellCooldown> |
QueryQuestInfoResponse |
QueryPackets.cs | Complex quest data with lists |
QueryCreatureResponse |
QueryPackets.cs | Variable-length strings |
QueryGameObjectResponse |
QueryPackets.cs | Variable-length strings |
QueryPageTextResponse |
QueryPackets.cs | Variable-length text |
WhoResponsePkt |
QueryPackets.cs | List<WhoEntry> |
QuestGiverQuestDetails |
QuestPackets.cs | Multiple lists (rewards, objectives) |
QuestGiverQuestListMessage |
QuestPackets.cs | List<GossipQuest> |
QuestGiverRequestItems |
QuestPackets.cs | List<QuestObjectiveCollect> |
QuestGiverOfferRewardMessage |
QuestPackets.cs | Multiple reward lists |
QuestGiverQuestComplete |
QuestPackets.cs | Optional reward display |
DisplayToast |
QuestPackets.cs | Multiple switch-based writes |
AuraUpdate |
SpellPackets.cs | List<AuraInfo> Auras |
SpellStart |
SpellPackets.cs | Complex spell cast data |
SpellGo |
SpellPackets.cs | Complex spell cast data |
SpellPeriodicAuraLog |
SpellPackets.cs | List<SpellLogEffect> |
FeatureSystemStatus |
SystemPackets.cs | Complex with many optional fields |
FeatureSystemStatusGlueScreen |
SystemPackets.cs | Complex with version-dependent lists |
TradeUpdated |
TradePackets.cs | List<TradeItem> |
UpdateObject |
UpdatePackets.cs | Complex object update data |
Complex Authentication / Session Packets
| Packet | File | Reason |
|---|---|---|
AuthResponse |
AuthenticationPackets.cs | Complex with optional sections |
ConnectTo |
AuthenticationPackets.cs | Connection data with addresses |
EnterEncryptedMode |
AuthenticationPackets.cs | Encryption keys |
BattlenetNotification |
SessionPackets.cs | Bnet protocol data |
BattlenetResponse |
SessionPackets.cs | Bnet protocol data |
ChangeRealmTicketResponse |
SessionPackets.cs | Auth ticket data |
Unbounded Strings / Other
| Packet | File | Reason |
|---|---|---|
PartyInvite |
GroupPackets.cs | Multiple unbounded strings (realm names, etc.) |
Based on spanstats.log analysis, several packets were allocating far more memory than needed. These have been optimized with reduced caps that still use fallback to Write() for rare oversized cases.
| Packet | Old MaxSize | New MaxSize | Reduction |
|---|---|---|---|
ContactList |
36,605 | ~2,933 | 92% |
UpdateAccountData |
16,419 | ~2,083 | 87% |
AllAccountCriteria |
13,060 | ~1,636 | 87% |
ChatPkt |
8,511 | ~1,087 | 87% |
SendKnownSpells |
4,617 | ~1,097 | 76% |
SetupCurrency |
3,844 | ~484 | 87% |
SetAllTaskProgress |
3,332 | ~420 | 87% |
LoadCUFProfiles |
2,048 | 256 | 88% |
MOTD |
2,065 | ~517 | 75% |
MonsterMove |
1,134 | ~366 | 68% |
LFGListUpdateBlacklist |
1,028 | ~132 | 87% |
ChannelNotifyJoined |
357 | ~165 | 54% |
MoveUpdate |
256 | 192 | 25% |
Parts of this project's code are based on CypherCore and BotFarm. I would like to extend my sincere thanks to these projects, as the creation of this app might have never happened without them. And I would also like to expressly thank Modox for all his work on reverse engineering the classic clients and all the help he has personally given me.
Stable Downloads: Releases