rbx-dom
rbx-dom copied to clipboard
Implement UniqueId type
UniqueId is a property type that is currently present only in place files. It has distinct formats for both rbxl and rbxlx.
Sample property chunk for rbxl format, in hexadecimal format:
| 00 00 00 00 08 00 00 00 55 6e 69 71 75 65 49 64 |........UniqueId|
| 1f 00 01 b7 86 02 4e 93 e1 e1 1e 4c 04 09 cf 82 |......N....L....|
| 88 |.|
Sample of equivalent value for rbxlx:
<UniqueId name="UniqueId">708f260204e7c144024e93e10001b786</UniqueId>
The first bytes describe the property as usual:
00 00 00 00 Class ID (0)
08 00 00 00 Length of property name (8)
55 6e 69 71 75 65 49 64 Property name ("UniqueId")
1f Property type
The type ID for UniqueId is 0x1F.
As usual, the remaining bytes represent an array of values. For this type, the array is interlaced by 16 bytes.
The sample contains one value:
00 01 b7 86 02 4e 93 e1 e1 1e 4c 04 09 cf 82 88
For proper conversion between rbxlx, this must be interpreted as a struct:
{
Index: u32
Time: u32
Random: i64
}
The Index and Time fields are interpreted as unsigned 32-bit integers, encoded in big-endian. The Random field is similar to the encoding of the int64 type: a signed 64-bit integer in big-endian and with rotated encoding.
In the rbxlx format, this value is encoded by converting each field to bytes in big-endian, then encoding to hexadecimal. In relation to the binary format, the fields are written in reverse order:
Index Time Random
bin 0001b786 024e93e1 e11e4c0409cf8288
Random Time Index
xml 708f260204e7c144 024e93e1 0001b786
Decoding is simply applying this process in reverse.
Note: The Random field looks different between formats because rbxl uses rotated encoding:
Random (hex) Random (binary)
bin e11e4c0409cf8288 1110000100011110010011000000010000001001110011111000001010001000
xml 708f260204e7c144 0111000010001111001001100000001000000100111001111100000101000100
The following archive contains rbxl and rbxlx files with instances that have equivalent UniqueId values. These can be used to verify the correctness of implementations:
After a brief discussion with a Roblox engineer on this, it's been made clear that these are meant to be just GUIDs for Instances (it will be used to make packages usable, among other things). I'm not sure if it's meaningful to separate them internally beyond splitting the number to correct the differences between file formats.
I'd love some additional feedback on this, but my current plan is to instead implement this as a simple wrapper for u128 and have functions to convert from the xml format to the binary format and vice versa. I don't see a reason why someone would want to access the individual fields that make up the number (the time field is unfortunately not a timestamp).
I think it's a lot easier to reason about as a struct. Had the binary format of UIDs matched the XML format, then the type could easily have been represented as a u128 or constant-length sequence of bytes. But because of the seemingly complicated conversion between binary and XML, it's pretty clear that Roblox uses a struct internally. We all want the UID to be an opaque hunk, but that isn't how it played out, and it's too late to change things. Any attempts to hide what it actually is now will only make things more difficult down the line.
Some extra bits:
- Time is a timestamp! It just has an epoch set at 2021-01-01 00:00:00.
- The Random field is almost certainly a 64-bit signed integer, due to how its encoded in the binary format. The sign bit is always 0, suggesting that random 63-bit positive integers are generated.
- This comment has more information, particularly how UIDs are generated.
Wow, I really don't like that epoch even though it makes perfect sense to use it. I'd not sure what I'd assumed the time field was if not a real timestamp (in hindsight I think I've just been willfully ignoring its significance in hopes of ignoring the struct entirely), but confirming that it's a real one does change things somewhat because now we have to treat UniqueId as a struct.
The hope was to obscure the implementation details on Roblox's end since it seemed superfluous to the whole thing but with a timestamp embedded it really doesn't make sense to ignore it, especially when the time comes for us to make our own. I suppose complexity is the price we pay for correctness in the end...
This was closed by #271!