MHServerEmu Progress Report: September 2025
It’s party time. P-A-R-T-Why? Because I gotta!
Parties
The long awaited Party system is finally here, and now you can play most of the content in the game with your friends. While the implementation of the Party system itself took only a few weeks, it really is the culmination of the two months of backend work that preceded it, including overhauling how instancing works and implementing the Community system, which we covered in previous reports.
Like other social features, the Party system breaks the boundaries of individual game instances to connect players across them. One thing that makes parties special is that they are “owned” by the server rather than individual players: the player who started a party may leave it and log out, but the party will continue to exist for as long as it still has any members. Because of this, authoritative representations of parties need to exist in one of the services that run for as long as server itself does. This is where the confusion begins.
As a reminder, there are two services that the client remains connected to for the entire duration of a session, no matter what game instance it is currently being hosted by: the Player Manager and the Grouping Manager. Originally, all party functionality was handled by the appropriately named Grouping Manager, but there was a pretty major change in 2017. It appears that when Gazillion was integrating their social functionality into the console online services, PlayStation Network and Xbox Live, they decided to move party functionality from the Grouping Manager to the Player Manager. As a result, the Grouping Manager became little more than a chat service, while the Player Manager ended up being effectively the primary service that orchestrated the entire game.
One example of a minor point of confusion this change caused is the API used for party requests. In versions of the game prior to the Biggest Update Ever (BUE) the client did party requests using a number of separate network messages:
message NetMessageRequestPartyJoinPortal {
}
message NetMessageDeclineGroupInvite {
}
message NetMessageLeaveGroup {
}
message NetMessageChangeGroupLeader {
required uint64 targetMemberId = 1;
}
message NetMessageBootPlayer {
required uint64 targetMemberId = 1;
}
message NetMessageDisbandGroup {
}
message NetMessageGroupChangeType {
required GroupType type = 1;
}
message NetMessageGroupChangeTypeConfirmResponse {
required uint64 playerId = 1;
required bool accept = 2;
}
In BUE this was changed into what is essentially a subprotocol, and all requests were unified into a single structure called PartyOperationPayload
:
message PartyOperationPayload {
required uint64 requestingPlayerDbId = 1;
required string requestingPlayerName = 2;
optional uint64 requestingPlayerPsnAccountId = 3;
optional uint64 targetPlayerDbId = 4;
optional string targetPlayerName = 5;
required GroupingOperationType operation = 6;
optional string psnSessionId = 7;
optional uint64 difficultyTierProtoId = 8;
}
message NetMessagePartyOperationRequest {
required PartyOperationPayload payload = 1;
}
However, you may notice that the operation
enumeration is still referred to as a “grouping” operation:
enum GroupingOperationType {
eGOP_InvitePlayer = 1;
eGOP_AcceptInvite = 2;
eGOP_DeclineInvite = 3;
eGOP_JoinPartyWithPSNSessionId = 4;
eGOP_LeaveParty = 5;
eGOP_DisbandParty = 6;
eGOP_ServerNotification = 7;
eGOP_KickPlayer = 8;
eGOP_ChangeLeader = 9;
eGOP_ConvertToRaid = 10;
eGOP_ConvertToRaidAccept = 11;
eGOP_ConvertToRaidDecline = 12;
eGOP_ConvertToParty = 13;
eGOP_ChangeDifficulty = 14;
}
This is because this enumeration actually comes from the pre-BUE version of the party API: it was simply moved from GroupingManager.proto
to CommonMessages.proto
. The functionality itself is mostly the same, but untangling subtle API nuances like this took a little bit of time.
It gets messier when it comes to replicating the authoritative data from the Player Manager to everything that needs to use it. Here is the big picture overview of what it looks like with a party of two members in two game instances:

When a player is added to a game instance, this game instance needs to know if the player is in a party, so the Player Manager sends this data in a structure called PartyInfo
:
message PartyMemberInfo {
required uint64 playerDbId = 1;
required string playerName = 2;
repeated uint64 boosts = 3;
optional uint64 consoleAccountId = 4;
optional uint64 secondaryConsoleAccountId = 5;
optional string secondaryPlayerName = 6;
}
message PartyInfo {
required uint64 groupId = 1;
required GroupType type = 2;
required uint64 leaderDbId = 3;
required uint64 difficultyTierProtoId = 4;
optional string groupPSNSessionId = 5;
repeated PartyMemberInfo members = 6;
optional string voiceChatFrontendIPAddr = 7;
optional string voiceChatFrontendPort = 8;
}
The game instance uses this data to either construct a new local representation of a party or update an existing one. It is also relayed to the client, so that it can construct or update its own representation of the same party. The representation in the game instance is shared by all party members present in the instance, and it is destroyed when there are no longer any members in the instance.
When the game instance representation changes, it notifies the local Player
entities representing members present in the instance. Each Player
entity has a Community
instance bound to it, which has a circle dedicated to holding current party members (see the previous report for more information on how the Community system works). The client relies on the Party circle to display hero icons for party members, and the server needs it to determine active party filters - conditions required to trigger scoring events for certain party-related achievements (e.g. do something in a party with specific heroes).
Replicating the party representation itself and the party community circle is not enough for the client. For it to “understand” that it is currently in a party, the party’s id needs to be replicated separately using the RepVar
system. This is a legacy replication system that was used more extensively in older versions of the game, like 1.10. Over time RepVar
fields were gradually replaced with regular protobuf messages, but there are still some places where they remain, such as this one. The client uses the id replicated in this way mainly for interaction validation, such as not allowing the player to go through portals belonging to other parties or preventing the use of prestige consumables while in a party.
But wait, there is more. In order for the client to be able to display the health meters of party members in the same game instance, it needs to have the Player
entities of party members replicated to it using the Party
channel of the Area of Interest system, and there is an issue with that. The ownership relation between a Player
entity and an Avatar
entity is established via the latter’s InvLoc
. In other words, the Avatar
being played is stored in the Player
’s AvatarInPlay
inventory. Due to how the Area of Interest system is implemented, changing an entity’s InvLoc
destroys and recreates the entity client-side. This causes problems when you invite an Avatar
that is already present in your client’s Area of Interest to a party: partying up adds the Player
entity of the invitee to your client’s Area of Interest, but the current Avatar
entity is already replicated to your client without the ownership relation established, because the owning Player
entity was not yet exposed to your client when it happened. To solve this edge case, Gazillion introduced an additional protobuf message called NetMessageInventoryMove
that is used to move an already replicated entity to the newly replicated owner’s inventory client-side.
The end result of all of this is that replicated party data is scattered across many different representations in various game instances and clients hosted by them. All of it can very easily get out of sync and cause all sorts of weird issues that appear to be “random”. Debugging this was also annoying because everything had to be done with two or more instances of the game client running. Despite all the difficulties on the way there, the Party system is now functional and stable.
0.8.0 Plans
With the Party system implemented and version 0.7.0 released, we are now in the final stretch of development. The remaining systems, like supergroups and PvP, are more like subsets of existing systems, such as Community and MetaGame respectively. This means that version 0.8.0, which we currently plan to release in December, is going to be the final major release before 1.0.0.
As you may know if you have read our FAQs, we plan to do a wipe in 1.0.0 by making existing saved data incompatible. For people who want to continue playing using their 0.x accounts on a self-hosted server, we will be releasing one final update, 0.8.1, which will include all the bug fixes and optimization changes that we are going to do between 0.8.0 and 1.0.0. We will share more details about this later.
Once 1.0.0 is out, we will be shifting our focus towards supporting older versions of the game. As we have mentioned previously, the highest priority for this is 1.48 from late 2016, which is the final pre-BUE version. Although it is not going to be “officially” supported until after 1.0.0, I have some news to share regarding early progress that has been made.
Pre-BUE Progress
I was feeling burnt out after months of working on social features, so I decided to do something fun for a change. While I had a reasonably good plan for supporting pre-BUE versions of the game based on the proof-of-concept server from 2024, it was still mostly theoretical. I was relatively confident that it would not take long to get some version of the fully-featured server working with 1.48, so I decided to take a small break from 1.52 and give it a go.
There are two things you need to do to make the server compatible with a different version of the game:
-
Update the network protocol. This means generating new
NetMessage
classes from.proto
schemas extracted from the client and changing the code that references message fields that no longer exist. One example of this are messages that reference avatars by index: they were modified to support couch co-op on consoles, but the concept of avatar indices did not exist pre-BUE, so everything that references it needs to be removed. -
Update prototype structure definitions. This means adjusting all the
Prototype
classes to match the old data. For example, the difficulty slider feature did not exist pre-BUE, but there was still the concept of difficulty inherent to the region. To accomodate this, theTuningPrototype
class needs to be renamed toDifficultyPrototype
, and its fields need to be adjusted. This has a cascading effect on all gameplay code that references the changed prototype classes and their fields.
Most of the data structure changes can be automated with code generation, so what remains to be done manually is fixing hundreds of compile errors from code referencing non-existing fields. Because 1.52 and 1.48 are actually not that different under the hood, fixing all the compile errors took me just a single day. With that I was able to go back to December 2016:
All the old content and features were there, including the point-based power system:
However, just getting it to work is the “easy” part. Playing the game reveals all kinds of subtle issues, including powers disappearing from the action bar, power points not being awarded on level up, some of the bosses not spawning, and many more. I have spent another day fixing some of the more glaring problems, but getting everything to a polished state would require implementing various missing features and investigating the remaining inconsistencies between the server and the older client version. The overall scope of the required work is similar to a single quarterly server release.
I have published a separate branch containing the current work-in-progress 1.48 code. There are no nightly builds and no instructions for setting it up, but if you are feeling adventurous you can try getting it up and running. I will most likely continue doing a little bit of work on it as a side project, but it will not be the primary focus until 1.0.0 is out early next year.
And with this another Progress Report comes to an end. See you next time!