Storm is Ubisoft's proprietary matchmaking engine. The engine itself is game-agnostic, meaning all Storm-based games connect to single central regional servers which co-ordinate games between players.
There is very little information online about the Storm matchmaking service.
In the us-sdkClientStorm field for the game's Space parameters, a set of router provisioning URLs is provided: { "parameters": { "us-sdkClientStorm": { "fields": { "detectConfig": "EnableDebug=false;ValidateDetect=true", "detectProvisioningUrl": "https://ncsa-storm.ubi.com/v1/natdetect", "routerProvisioningUrl": "https://apac-storm.ubi.com/v1/router;https://emea-storm.ubi.com/v1/router;https://ncsa-storm.ubi.com/v1/router;https://ap-southeast-2-storm.ubi.com/v1/router", "matchmakingSandboxName": "SM_PC_LNCH_A", "traversalProvisioningUrl": "https://ncsa-storm.ubi.com/v1/nattraversal", "matchmakingSandboxSpaceId": "4c29e04e-14d2-4245-a3c4-bf4703461119" }, "relatedPopulation": null } } }
The URLs provided each provider an endpoint with a versioning number at the end. For example, Hyperscape makes requests to: https://apac-storm.ubi.com/v1/router/19.
This is an authenticated route, meaning a Ubisoft ticket must be provided in the Authorization header.
Here is an example request:
GET /v1/router/19
Host: ncsa-storm.ubi.com
| Header | Value |
|---|---|
| Authorization | Ubi_v1 t={ticket} |
{
"router": "52.207.248.146:12000"
}
The client can then make UDP requests to this router at the specified port, packets which are structured according to the Storm Protocol
Storm appears to use a mix of positive acknowledgements (ACKs), and negative acknowledgements (nACKs).
| Name | Description | Type |
|---|---|---|
| Connection ID | The ID of the sender. At this stage null. |
u16 |
| Unknown | Unknown | 20 bytes |
| Opcode | Packet opcode, 0x10 for ping |
Opcode |
| Nonce | Incrementing nonce. First 3 bytes are seemingly random, last byte is incrementing | u32 |
| App ID | Some sort of ID. Client sends the application ID for Hyperscape, server responds with an unknown UUID. |
UUID |
| Unknown | Unknown | u8 |
[!IMPORTANT]
This is a possible structure, but highly unlikely due to strange type sizes. This is derived from seeing which bytes are static, and which seem to change on each connection.
| Name | Description | Type |
|---|---|---|
| Connection ID | The ID of the sender. At this stage null. |
u16 |
| Unknown | Unknown | 20 bytes |
| Opcode | Packet opcode, 0xe0 for this message |
Opcode |
| Unknown | Unknown | u16 |
| Nonce | Some sort of incrementing nonce | u32 |
| App ID | Some sort of ID. Client sends the application ID for Hyperscape. |
UUID |
| Unknown | Unknown | UUID |
| Unknown | Unknown | u32 |
| Unknown | Unknown | 20 bytes |
| Unknown | Unknown | u8 |
| Unknown | Unknown | 3 bytes |
| Unknown | Unknown | 26 bytes |
| Unknown | Unknown | u16 |
| Unknown | Unknown | 7 bytes |
| Unknown | Unknown | 13 bytes |
| Unknown | Unknown | 10 bytes |
| Unknown | Unknown | u16 |
OpcodeSize: u8
| Name | Description | Value |
|---|---|---|
| Ping | Used for determining round-trip latency | 0x10 |
| Unknown | Unknown | 0xe0 |
When multiple regional routers are provided in the routerProvisioningUrl field, the game sends exactly 5 packets to calculate the average round-trip latency. The lowest latency server is then selected.
This stage is not required. If only one regional router is provided, then it moves straight to the handshake.
Here is an example conversation:
[C -> S] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb00 69e55abd5aca4eeebe94ce3944c72e73 c4
[S -> C] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb00 63aba5df1da34eab863359bb2f53cd13 00
[C -> S] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb01 69e55abd5aca4eeebe94ce3944c72e73 c4
[S -> C] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb01 63aba5df1da34eab863359bb2f53cd13 00
[C -> S] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb02 69e55abd5aca4eeebe94ce3944c72e73 c4
[S -> C] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb02 63aba5df1da34eab863359bb2f53cd13 00
[C -> S] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb03 69e55abd5aca4eeebe94ce3944c72e73 c4
[S -> C] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb03 63aba5df1da34eab863359bb2f53cd13 00
[C -> S] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb04 69e55abd5aca4eeebe94ce3944c72e73 c4
[S -> C] 0000 0000000000000000000000000000000000000000 10 00000000 1e38eb04 63aba5df1da34eab863359bb2f53cd13 00
If the router server fails to reply to these packets, the client re-sends them with a different starting nonce.
Once a router has been selected, the client then performs what is presumably a handshake.
This appears to be some sort of handshake sequence for connecting.
[C -> S] 77b2 0000000000000000000000000000000000000000 e0 0003 c210 2013 69e55abd5aca4eeebe94ce3944c72e73 3d1e94062594475797cdfb247c1730c0 00130000 0c0ba989e03c0574f2a0 a6 032e09 81500003ffff0c2b0307802a29 0e86 61800000040002 5d8a0c0a5ddaca0b4a5e00d010 989959581d 9c00
[S -> C] 2450 0000000000000000000000000000000000000000 e0 0000 0020 2013 63aba5df1da34eab863359bb2f53cd13 ffffffffffffffffffffffffffffffff 00130001 0c0ba989e03c0574f2a0 b2 032e09 582280c85dc10a00 455455 61800000040000 1e0ff0c2b0307802a2 90e860
[C -> S] 2450 0000000000000000000000000000000000000000 e0 0003 e230 2013 69e55abd5aca4eeebe94ce3944c72e73 3d1e94062594475797cdfb247c1730c0 00130000 0c0ba989e03c0574f2a0 a6 032e09 81500003ffff0c2b0307802a29 0e86 61800000040002 5d8a0c0a5ddaca0b4a5e00d010 989959581d 9d0022aa2a80
Note how the connection IDs swap -- the client sends the server's connection ID, and the server sends the client's connection ID.
After the initial handshake, both the client and server start sending heartbeats
[C -> S] 2450 0000000000000000000000000000000000000000 e0 0000 d6 60
[S -> C] 77b2 0000000000000000000000000000000000000000 e0 0000 00 60
[C -> S] 2450 0000000000000000000000000000000000000000 e0 0000 d6 60
[S -> C] 77b2 0000000000000000000000000000000000000000 e0 0000 00 60
... repeated ...
These heartbeats are sent by the client and server independently of eachother approximately every 10 seconds, but are not acknowledged. The protocol relies on the negative presence of these packets to indicate disconnection. This seems to happen after 60 seconds of no heartbeat.
When the client closes the game, the client sends 8 disconnect packets each exactly the same. This is possibly done to ensure the highest chance of arrival, however the server does not appear to take much notice.
[C -> S]
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
6723 0000000000000000000000000000000000000000 e0 0001 664c2b0307802a291bd25300cff05b ffffffff c3
[S -> C]
... heartbeats for 60 seconds ...
3b6a 0000000000000000000000000000000000000000 e0 0000 004a6019fe0b6158183c015148de93 ffffffff c3
3b6a 0000000000000000000000000000000000000000 e0 0000 004a6019fe0b6158183c015148de93 ffffffff c3
3b6a 0000000000000000000000000000000000000000 e0 0000 004a6019fe0b6158183c015148de93 ffffffff c3
3b6a 0000000000000000000000000000000000000000 e0 0000 004a6019fe0b6158183c015148de93 ffffffff c3