| | |
| | | # Storm: Overview |
| | | |
| | | **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. |
| | | |
| | | ## Routers |
| | | In the `us-sdkClientStorm` field for the game's [Space](../03_ubisoft_services/03_structure.md#spaces) parameters, a set of **router provisioning URLs** is provided: |
| | | |
| | | ```json |
| | | { |
| | | "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: |
| | | |
| | | ### Endpoint |
| | | |
| | | ``` |
| | | GET /v1/router/19 |
| | | ``` |
| | | |
| | | **Host:** `ncsa-storm.ubi.com` |
| | | |
| | | ### Headers |
| | | |
| | | | Header | Value | |
| | | |---------------|---------------------| |
| | | | Authorization | `Ubi_v1 t={ticket}` | |
| | | |
| | | ### Example Response |
| | | |
| | | ```json |
| | | { |
| | | "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-protocol) |
| | | |
| | | ## Storm Protocol |
| | | |
| | | Storm appears to use a mix of **positive acknowledgements** (ACKs), and **negative acknowledgements** (nACKs). |
| | | |
| | | ### Ping Packet |
| | | | 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](#enum-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` | |
| | | |
| | | ### Unknown Handshake Packet 1 |
| | | |
| | | > [!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](#enum-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` | |
| | | |
| | | ### Enum: `Opcode` |
| | | |
| | | Size: `u8` |
| | | |
| | | | Name | Description | Value | |
| | | |---------|-----------------------------------------|--------| |
| | | | Ping | Used for determining round-trip latency | `0x10` | |
| | | | Unknown | Unknown | `0xe0` | |
| | | |
| | | ## Detecting Best Region |
| | | When multiple regional routers are provided in the [`routerProvisioningUrl` field](#routers), 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](#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](#handshake). |
| | | |
| | | ## 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](#heartbeats) |
| | | |
| | | ## 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. |
| | | |
| | | ## Disconnect |
| | | 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 |
| | | ``` |