Skip to content
Giorgio edited this page May 21, 2022 · 6 revisions

Hoo boy.

<rant>

Just like Riot, Epic use XMPP to send and receive presences. However, unlike Riot, their code is a mess.

They probably made their presence system in a hurry around when Fortnite was starting to blow up, and since they update the game every 2 weeks, they haven't really had the time to go back and clean up the code. Same goes for their authentication, did you know there are 8 different ways to generate an access token?

Honestly, it makes me appreciate the fact Riot managed to keep their code somewhat clean despite League being out since 2009 (although to be fair, VALORANT came out in 2020). Compare this to Epic Games, whose presence code dates back to at most ~2016 according to Asriel.

No wonder Fortnite is a buggy mess. The more you work with Epic's services, the more you realize their code is a pile of spaghetti. Sure it works, but I can't imagine what it's like to add any feature while wrestling with the code some other dev probably wrote in 5 minutes a year ago.

A few examples, if I may:

  • In the Fortnite presences, there is a variable "bFellToDeath" which represents whether the player died of fall damage. Not only is this a completely random thing to send, it doesn't send anything else about whether the player is alive or not, so if the player died to another player we have no way of knowing. But at least we know if they die of fall damage.
  • You're supposed to connect to Epic's XMPP using WebSockets. But if you happen to connect using a simple TLS stream, you will be greeted with another separate but fully functional XMPP server, with the exception that it always rejects your authentication. I spent multiple days trying to figure out why on earth I couldn't authenticate. Thank you Epic.
  • The Epic Games Launcher doesn't use XMPP to transmit friend presences anymore, it uses STOMP instead (which is why you can't DM friends anymore). However, the launcher still starts and maintains an XMPP connection, that it then proceeds to not use. To quote Asriel, "EGL keeps XMPP because their codebase had it hardcoded in. They don't use XMPP for anything".
  • In the STW game files, the Mythic Storm King is called "DUDEBRO". And the Battle Royale version is called "DADBRO". (ok admittedly this one's kinda funny. Source: Λssassinツ and Zagrion in the discord).

I was hoping that with the switch to Unreal Engine 5, they would have a chance to somewhat rewrite the code from scratch, or at least take a step back and restructure the whole thing. It doesn't seem to be the case.

</rant>

With that out of the way, here is how to connect to Epic's XMPP services.

Update

The Epic Games launcher no longer receives rich presences over XMPP, but over STOMP instead.

From my initial research, the connection happens over a WebSocket to connect.ol.epicgames.com, and the authentication seems to happen using cookies.

Unfortunately, the WebSockets API doesn't let you set custom headers, including cookies. The solution would then be for me to either find and link an alternate WebSocket library that supports headers, or make one myself.

Fortunately, the Fortnite game itself still uses XMPP, so full Fortnite presences are still sent over XMPP, as well as what other games your friends are playing. Unfortunately, that doesn't include those games' rich presence data.

Prerequisites

As mentioned above, Epic has 8 different ways of obtaining an access token, so pick and choose whichever one suits your use case best, although I personally recommend auth code if you are logged in on your browser, or exchange code if you are logged in on the launcher. You can use any of these to obtain both an access token that lasts 8 hours, and a refresh token that lasts 23 days that can be used to generate access tokens.

Make sure you include &token_type=eg1 in the auth URL to get a JWT instead of whatever else it gives you by default.

When authenticating, you will have to choose an auth client, which basically represents which platform you are "authenticating" from. The popular choice among the Epic dev community seems to be fortniteIOSGameClient (Fortnite on iOS), although I opted to use launcherAppClient2 (the desktop Epic Games Launcher) so that I am guaranteed to receive presences of other games than Fortnite.

Connecting to the XMPP server

The XMPP server address is wss://xmpp-service-prod.ol.epicgames.com.

To connect and recieve presence updates, send these strings one by one, waiting for a response from the server between each one:

<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" version="1.0" xml:lang="en" to="prod.ol.epicgames.com"/>
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">[AUTH STRING]</auth>
<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" version="1.0" xml:lang="en" to="prod.ol.epicgames.com"/>,
<iq xmlns="jabber:client" type="set" id="4f3712da-95d0-43cd-b5de-c039e88c18c9"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>V2:launcher:WIN::</resource></bind></iq>
<presence xmlns="jabber:client" id="638e9399-7760-435d-a351-c168f06bc7fa"/>

Note: all your friends will see you as online and in the launcher.

Make sure to replace [AUTH STRING] with the auth string.

The auth string

The auth string is made up of the account ID you're authenticating with, and the access token. You can extract the account ID from the token by getting the sub, make sure to remove the eg1~ at the beginning.

Once you got these, the auth string is simply the account ID and token separated using the null character:

[NULL][account ID][NULL][token]

Keeping the connection alive

You need to ping the server every minute or so to keep the connection alive.

<iq xmlns="jabber:client" id="acbeabf8-b04b-4e94-a044-6d6b8f04514e" type="get"><ping xmlns="urn:xmpp:ping"/></iq>

Weirdly enough, if you simply send a whitespace character every minute (just like for Riot), Epic will silently stop sending you presence updates, but will still keep the connection alive. I'm sure there's a good reason for that.

What presences look like

Games without rich presence

When launching a game on Epic Games, two presence messages are sent, the game opening and the launcher closing. Here is what they look like:

<presence from="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[short session id]"  to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]" xmlns="jabber:client"><status>{"Status":"","bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"","Properties":{"OverrideAppId_s":"33956bcb55d4452d8c47e16b94e294bd"}}</status><delay stamp="2069-10-20T12:34:56.789Z" xmlns="urn:xmpp:delay"/></presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:launcher:   ::[longer session id]" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]" type="unavailable">   <status>{            "bIsPlaying":false,"bIsJoinable":false, "hasVoiceSupport":false,"SessionId":"","Properties":{                                                    }}</status>                                                                </presence>

(I've added spaces to make it easier to read and compare the presences)

The part after the from= and the to= is called the Jabber ID, or JID. Here's the format, as far as I can tell:

[account id]@prod.ol.epicgames.com/V2:["launcher" or app ID]:[platform (WIN) or nothing]::[session ID]

The interesting presence is the one sent by the game. It has:

  • WIN as its platform
  • a <delay> indicating the time at which it was sent
  • a Status in the JSON, usually empty for regular games
  • an OverrideAppId_s containing the game's namespace

A few things to note:

  • I'm not sure what "session id" is, but it seems to change every session, so that's what I'm assuming it is
  • The JSON contains false for bIsPlaying. That's because bIsPlaying only refers to whether the user is playing Fortnite specifically. Same goes for bIsJoinable, bHasVoiceSupport and SessionId.

Oh and the launcher closing message has type="unavailable".

Namespaces

In typical Epic fashion, all games on the Epic Games store have not one, not two, but at least three different IDs per game, and probably more I'm not aware of.

One of them is the namespace of the game, and the way I've found of getting the name of the game from the namespace is by using Epic's GraphQL, more specifically the searchStoreQuery. This is what the Epic Games Store search used up until a while ago, so I don't think it will work on unlisted games, and I'm not sure how long the GQL service will be up if the store doesn't use it anymore. Then again, Epic do like keeping old servers online for no reason...

Another possible way according to FNBRUnreleased is using this endpoint:

https://catalog-public-service-prod.ol.epicgames.com/catalog/api/shared/namespace/[namespace]/items?count=1000

Although for the Among Us namespace it returns "Among Us General Audience", whatever the hell that is.

Rocket League

Some games, typically the more high-profile ones (GTA V, Paladins, Satisfactory...), don't behave like the other games. One such game is Rocket League, which sends three presences instead of two:

<presence from="[account id]@prod.ol.epicgames.com/V2:launcher                        :WIN::[launcher session id]"                to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[launcher session id]" xmlns="jabber:client"><status>{"Status":"",         "bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"","Properties":{"OverrideAppId_s":"9773aa1aa54f4f7b80e44bef04986cea"}}</status><delay stamp="2021-10-12T19:22:12.519Z" xmlns="urn:xmpp:delay"/></presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:launcher                        :   ::[rocket league session id]"           to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[launcher session id]" type="unavailable">   <status>{                     "bIsPlaying":false,"bIsJoinable":false,"hasVoiceSupport":false,"SessionId":"","Properties":{}}</status>                                                                                                                     </presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:fghi4567OXA1CdeeCuAmUWMhmfiO3EAl:   ::[different rocket league session id]" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[launcher session id]" type="available">     <status>{"Status":"Main Menu","bIsPlaying":false,"bIsJoinable":false,"hasVoiceSupport":false,"SessionId":"","Properties":{}}</status>                                                                                                                     </presence>

So:

  • The first one appears to be the launcher indicating Rocket League was launched (notice the OverrideAppId_s)
  • The second one is the launcher closing, but contains a Rocket League style session id (they are longer and have dashes) that is never used again
  • The third one is from Rocket League itself

During the user's play session, you will receive presences formatted like the third one:

  • The app id is fghi4567OXA1CdeeCuAmUWMhmfiO3EAl instead of launcher
  • type="available"
  • The rich presence Status in the JSON

Oh, and when exiting Rocket League, there are also three different presences sent:

  • One with the Rocket League app ID and type="unavailable" (but still with a Status)
  • One from the launcher with an empty Status and a <delay>
  • One from the launcher with type="available"

When in-game, the status is usually something like Doubles in Mannfield. That being said, one of my friends has presences that look something like InGame 0:1 [2:45 remaining] and InGame 2:2 [Overtime 0:15]. Either it's a gradual rollout thing where Psyonix are testing the feature on a small number of players, or (more likely) it's just BakkesMod.

Fortnite

Since Epic likes to 1-up itself, when launching Fortnite there are 5 messages sent:

<presence from="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[launcher session ID]"             xmlns="jabber:client" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]"><status>{"Status":"",                            "bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"",                         "Properties":{"OverrideAppId_s":"fn"                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    }}</status><delay stamp="2069-10-20T01:23:45.678Z" xmlns="urn:xmpp:delay"/></presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:launcher:   ::[long session ID with dashes]"     type="unavailable"    to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]"><status>{                                        "bIsPlaying":false,"bIsJoinable":false, "hasVoiceSupport":false,"SessionId":"",                         "Properties":{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          }}</status>                                                                </presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:Fortnite:WIN::[short but consistent session ID]" xmlns="jabber:client" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]"><status>{"Status":"",                            "bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"","ProductName":"Fortnite","Properties":{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          }}</status><delay stamp="2069-10-20T01:23:45.678Z" xmlns="urn:xmpp:delay"/></presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:Fortnite:WIN::[short but consistent session ID]" xmlns="jabber:client" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]"><status>{"Status":"",                            "bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"","ProductName":"Fortnite","Properties":{"party.joininfodata.286331153_j":{"sourceId":"[account id]","sourceDisplayName":"Giorgiо","sourcePlatform":"WIN","partyId":"40ac75438f54460fadc4fbb2f3571fae","partyTypeId":286331153,"key":"k","appId":"Fortnite","buildId":"1:3:18141325","partyFlags":4,"notAcceptingReason":33,"pc":1}                                                                                                                                                                                                                                                                }}</status><delay stamp="2069-10-20T01:23:45.678Z" xmlns="urn:xmpp:delay"/></presence>
<presence from="[account id]@prod.ol.epicgames.com/V2:Fortnite:WIN::[short but consistent session ID]" xmlns="jabber:client" to="[account id]@prod.ol.epicgames.com/V2:launcher:WIN::[session id]"><status>{"Status":"Battle Royale Lobby - 1 / 16","bIsPlaying":false,"bIsJoinable":false,"bHasVoiceSupport":false,"SessionId":"","ProductName":"Fortnite","Properties":{"party.joininfodata.286331153_j":{"sourceId":"[account id]","sourceDisplayName":"Giorgiо","sourcePlatform":"WIN","partyId":"40ac75438f54460fadc4fbb2f3571fae","partyTypeId":286331153,"key":"k","appId":"Fortnite","buildId":"1:3:18141325","partyFlags":6,"notAcceptingReason":0, "pc":1},"FortBasicInfo_j":{"homeBaseRating":0},"FortLFG_I":"0","FortPartySize_i":1,"FortSubGame_i":1,"InUnjoinableMatch_b":false,"FortGameplayStats_j":{"state":"","playlist":"None","numKills":0,"bFellToDeath":false},"SocialStatus_j":{"attendingSocialEventIds":[]}}}</status><delay stamp="2069-10-20T01:23:45.678Z" xmlns="urn:xmpp:delay"/></presence>

Let's break this down:

  • The first two are the launcher launching the game and closing itself, same as above
  • The third is Fortnite connecting to the XMPP server, but the game is still loading so it has no data yet
  • The fourth is Fortnite creating a party, but it's still not finished loading the game
  • The fifth is the game fully loaded and the player finally in the Lobby

A few things that set Fortnite presences apart from other games:

  • Fortnite's namespace is just fn, as seen in OverrideAppId_s
  • Fortnite's app ID is "Fortnite"
  • The JSON has much more data

Finally some data for us to play with.

Fortnite's presence

Let's format the last JSON to allow us to take a better look:

{
  "Status": "Battle Royale Lobby - 1 / 16",
  "bIsPlaying": false,
  "bIsJoinable": false,
  "bHasVoiceSupport": false,
  "SessionId": "",
  "ProductName": "Fortnite",
  "Properties": {
    "party.joininfodata.286331153_j": {
      "sourceId": "[account id]",
      "sourceDisplayName": "Giorgiо",
      "sourcePlatform": "WIN",
      "partyId": "40ac75438f54460fadc4fbb2f3571fae",
      "partyTypeId": 286331153,
      "key": "k",
      "appId": "Fortnite",
      "buildId": "1:3:18141325",
      "partyFlags": 6,
      "notAcceptingReason": 0,
      "pc": 1
    },
    "FortBasicInfo_j": {
      "homeBaseRating": 0
    },
    "FortLFG_I": "0",
    "FortPartySize_i": 1,
    "FortSubGame_i": 1,
    "InUnjoinableMatch_b": false,
    "FortGameplayStats_j": {
      "state": "",
      "playlist": "None",
      "numKills": 0,
      "bFellToDeath": false
    },
    "SocialStatus_j": {
      "attendingSocialEventIds": []
    }
  }
}

A few initial notes:

  • If you are lazy and/or don't need detailed stats, the Status field still contains the formatted version of all this data shown in the launcher and in-game
  • I don't have Save The World, so I have no idea what the presences look like
  • bIsPlaying is still false, because it represents whether the person is in-game (as opposed to the lobby)
  • Similarly, bIsJoinable is only true when your friends can join the game you're currently in (such as in Creative mode)
  • bHasVoiceSupport is always false for me, even though my voice chat is enabled
  • SessionId is empty in the lobby, and a unique string for every game the player plays
  • Properties contains all the juicy information

Before analyzing Properties, it might be useful to compare the presence in the lobby to the presence in-game:

{
  "Status": "Playing Battle Royale - Solo - 67 Left",
  "bIsPlaying": true,
  "bIsJoinable": false,
  "bHasVoiceSupport": false,
  "SessionId": "954af13ef4e6485ca2a13cfe3871e26d",
  "ProductName": "Fortnite",
  "Properties": {
    "party.joininfodata.286331153_j": {
      "sourceId": "[account id]",
      "sourceDisplayName": "Giorgiо",
      "sourcePlatform": "WIN",
      "partyId": "968292f7f85241778426859ad48f330b",
      "partyTypeId": 286331153,
      "key": "k",
      "appId": "Fortnite",
      "buildId": "1:3:18141325",
      "partyFlags": 6,
      "notAcceptingReason": 0,
      "pc": 1
    },
    "FortBasicInfo_j": {
      "homeBaseRating": 0
    },
    "FortLFG_I": "0",
    "FortPartySize_i": 1,
    "FortSubGame_i": 1,
    "InUnjoinableMatch_b": true,
    "FortGameplayStats_j": {
      "state": "",
      "playlist": "None",
      "numKills": 0,
      "bFellToDeath": false
    },
    "SocialStatus_j": {
      "attendingSocialEventIds": []
    },
    "GamePlaylistName_s": "Playlist_DefaultSolo",
    "Event_PlayersAlive_s": "67",
    "Event_PartySize_s": "1",
    "Event_PartyMaxSize_s": "16",
    "ServerPlayerCount_i": 67
  }
}

Let's go over the different properties of, well, Properties:

  • Note the properties that finish in _i are integers, _s for strings, _b for booleans and _j for JSON objects
  • party.joininfodata.286331153_j: if the name seems arbitrary, it's because it is.
    • If the party is private, it will only contain {"bIsPrivate":true}
    • A plausible theory sent in the discord is that 286331153 most likely represents a Battle Royale/Creative party (as opposed to a STW one)
    • partyFlags is always 6, except during loading screens when it briefly switches to 4
    • No idea what "key": "k" is supposed to be
    • notAcceptingReason is 0 if the party is in fact accepting new members, and it's the only value I have encountered (except sometimes 33 while launching the game), since when the party is private the value is not sent at all
  • FortBasicInfo_j seems to be STW-related, and I haven't seen it change during testing
  • FortLFG_I probably stands for Looking For Group, and is probably an Xbox thing
  • FortSubGame_i is 1 for Battle Royale/Creative and probably 0 for STW
  • FortGameplayStats_j:
    • state is always empty, no idea what that is, probably used a long time ago
    • You can find list of playlists here
    • bFellToDeath is a prank by Epic Games to make fun of us by including such a pointless thing in their presence, and not including whether the guy is dead or not, which would be much more useful
  • SocialStatus_j: could be something to do with Party Royale, although my guess is that it shows what events the player is attending such as the Travis Scott concert, the Star Wars event, etc.

A few notes:

  • EventPlayersAlive_s and GamePlaylistName_s stay the same when leaving the game, and until another game is launched
  • If playing creative mode, bIsJoinable is true and there is additional property GameSessionJoinKey_s
  • There is no way to tell if someone in the lobby is in the queue or what mode they selected

Acknowledgements

  • fnbr.js's source code, written by Nils, who made my life 75% easier by having all the hard stuff already figured out
  • The EpicResearch repo, for documenting everything to do with Epic's authentication
  • The discord, join here!