UUIDv1 for SQF - official-antistasi-community/A3-Antistasi GitHub Wiki
If viewing in Visual Studio Code, press ctrl
+shift
+v
Source: A Universally Unique IDentifier (UUID) URN Namespace
UUID β Universally Unique Identifier
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timeLow | counter |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timeMid |version| timeHigh |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| clockSequence | node (0β1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2β5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Name | Definition |
---|---|
UUID Layout |
timeLow counter "-" timeMid "-" version timeHigh "-" variant clockSequence "-" node
|
Printed UUID | "01234567-89ab-cdef-0123-456789abcdef" |
timeLow | Least significant bits of UTC time |
timeMid | Middle significant bits of UTC time |
timeHigh | Most significant bits of UTC time |
version |
0001 for time and MAC address based version |
var |
01 variant for this UUID layout/type |
clockSequence | Random initial value (0β16383) that is increments on change. |
node | Any connected network card's MAC address (6 octets) |
Revision 1 β 4 September 2021 β Caleb S. Serafin
Table of Contents
Layout
Field Definitions
βPrinted UUID
βtime
βcounter
βclockSequence
βorigin
βnode
Cached and Saved Values
βWithin Runtime
βIn profileNamespace
Algorithms for Producing the Node
βOrigin 0
βOrigin 1
βOrigin 2
Generating a UUID after initialisation
βExample algorithm
UUID Pooling
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|version| timeHigh | timeMid |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timeLow | counter |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| clockSequence | origin| node (0β1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2β5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Name | Definition |
---|---|
UUID Layout |
version timeHigh "-" timeMid "-" timeLow counter "-" variant clockSequence "-" src node
|
Printed UUID | "cdef-89ab-01234567-0123-456789abcdef" |
SQF stored UUID | 5 elements between 0β65535, 2 elements between 0β16777215 |
timeHigh | Most significant bits of UTC time |
timeMid | Middle significant bits of UTC time |
timeLow | Least significant bits of UTC time |
version |
0001 for time and MAC address based version |
var |
01 variant for this UUID layout/type |
clockSequence | Random initial value (0β16383) that is increments on change. |
origin | Origin of node data and algorithm used. |
node | 44 bits that attempt to uniquely represent the node. |
Node β A machine in a network.
To reflect the changes in Endian-ness, the printed version is divided into 4-4-8-4-12 chunks of hexadecimal.
SQF stored UUID optimises the storage of the node by storing it in 2 chunks of 2^24 instead of 3 chunks of 2^16.
The same cannot be applied to other fields, without adding the additional cost of string selects when formatting it with dashes. timeLow and counter cannot be combined as floats do not have 32 bits of accuracy, but only 24.
Unlike RFC 4122, the time is ordered in littleEndian. This makes it sortable and easy to select ranges without an intermediary conversion.
Time will not be directly comparable with Unix time as bit packing will take place to convert 7 discrete time values, into 3 discrete time values.
Within SQF it would be an unnecessary expense to convert the 7 discrete time values into 3 continuous segments of a time value.
Bit packing is as follows:
- timeHigh (2^12) = (year mod 341) β’ 12 + (month - 1)
- timeMid (2^16) = ((day - 1) β’ 24 + hour) β’ 60 + minute
- timeLow (2^16) = second β’ 1000 + millisecond
The counter is initialised with a random value.
It is incremented every time a UUID is requested.
It will loop around to 0 at 2^16.
In order for the counter to be exhausted, a UUID will need to be created every 15 nanoseconds. For comparison, on an empty map, merely adding two numbers takes roughly 250 nanoseconds. SQF would require a CPU whose clock frequency is measured in the Terahertz to run a mission and exhaust UUID creation.
This is in addition to the specification provided by RFC 4122.
After clockSequence is loaded, the clockSequence in profileNamespace will be randomised. At the end of runtime, the clockSequence will be saved to profileNamespace. profileNamespace will be flushed after each operation.
If a node crashes, and then reloads, the system will detect this, and reset its clockSequence to a random value. If another instance of the same profileNamespace runs in parallel, it will detect this, and reset its clockSequence to a random value. In the parallel case: the order of saving will not matter.
Crash Example: instance 1 loads, instance 1 crashes, instance 1 resets.
Parallel Example: instance1 loads, instance2 loads reset, instance2 saves, instance3 loads from 2. instance1 saves, instance4 loads from 1, instance5 loads reset,
Specifies the consumed source and algorithm version to produce the node.
-
0000
:0
: steam64 -
0001
:1
: clientOwner β¨ initialSystemTimeUTC β¨ serverName -
0010
:2
: clientOwner β¨ serverName -
0011.*
: future definitions
β¨ β In this document it will represent arbitrary combining two elements.
Arbitrary β Undetermined; No assigned specific value or method.
The node can be treated as 6 octets. The most significant octet's bits are replaced by the origin code.
MAC address is not available. It is replaced by mostly stable sources:
- Clients will use
origin 0
. This is regenerated if the steam64 changes (swapping profile.vars) - Dedicated servers will use
origin 1
. This is regenerated if the serverName changes. - Headless clients will use
origin 1
. This is regenerated if the serverName changes.
Problems and Solutions:
- Steam64s are not available before the player object has been initialised on clients. This is mitigated by caching the steam64 in the profileNamespace.
- If the steam64 is not available in the profileNamespace when a UUID is requested before the player object exists, the client will use
origin 2
. The initialSystemTimeUTC is not used as this node will only need to be valid for an extremely short time period, therefore the time segments will be sufficiently close. - A clientOwner collision in origin 1 would be very likely due to only 16 IDs. Therefore, headless clients are manually assigned IDs by the server to ensure there is no collision. This means that when it's not cached and before it's assigned, HCs will use
origin 2
.
- counter (scalar)
- clockSequence (scalar)
- origin β¨ node (array)
- variant β¨ clockSequence β¨ origin β¨ node (array)
- systemTimeUTC (array)
- clockSequence (scalar)
- origin (scalar)
- origin β¨ node (array)
- steam64 (string)
- serverName (string)
- headlessClientID (scalar)
- The client's steam64 will be fetched.
- A least significant chunk (LSC) of 7 digits will be extracted from digits 0β6 (rightβleft)
- A most significant chunk (MSC) of 7 digits will be extracted from digits 7β13 (rightβleft)
- Each will be parsed into a scalar.
- MSC will be limited between
0
β7825791
((2^20 + (2^24 + 10^7))
). - The MSC will have
6777216
((2^44 - 10^7)
) subtracted from it. If it is smaller than this amount, then only its current value will be taken. - The amount taken from MSC will be added to LSC.
- The return value will be
[MSC, LSC]
. (Origin 0's code is not added as it is 0)
Limiting a scalar to a range value will be done with the x modulo y operation rather than 0 max x min y.
- days and hours will be extracted from systemTimeUTC at generation.
- days will be decremented and limited between
0
β9
. - timePart will be equal to
(days * 24 + hours)
(~2^8
). This is based on the idea that most servers will update their mission around a week (especially servers that they copy another server's name). - serverName will be hashed using
hashValue
. This results a 11 character string of base64. NOTE: the last character will have a lower range than the rest, so only 10 full characters. - Each of first 8 characters will be converted from base64, this will be an array of 8 items
0
β63
. - 4 buckets will be created (0β3).
- The 8 values will be combined into 4 buckets. Each pair of values will be combined by
item1<<6 + item2
. - Each bucket will be limited between
0
β255
(2^8
). - headlessClientID will be limited between
1
β15
(2^4
). This is assigned manually from the server. Server uses 0. Theses are statically mapped by HC name. - origin code is 1.
- The return value will be
[origin<<20 + headlessClientID<<16 + time<<8 + bucket#0, bucket#1<<16 + bucket#2<<8 + bucket#3]
.
<<
,>>
β Bit shifts are implemented by multiplying by a positive or negative power of 2.
#
β Select element from a zero-based list.
- serverName will be hashed using
hashValue
. This results a 11 character string of base64. NOTE: the last character will have a lower range than the rest, so only 10 full characters. - Each of first 8 characters will be converted from base64, this will be an array of 8 items
0
β63
. - 4 buckets will be created (0β3).
- The 8 values will be combined into 4 buckets. Each pair of values will be combined by
item1<<6 + item2
. - Each bucket will be limited between
0
β255
(2^8
). - clientOwner will be limited between
0
β4095
(2^12
). - origin code is 2.
- The return value will be
[origin<<20 + clientOwner<<8 + bucket#0, bucket#1<<16 + bucket#2<<8 + bucket#3]
.
- Get systemTimeUTC as year, month, day, hour, minute, second, millisecond.
- timeHigh (
2^12
) =(year mod 341) * 12 + (month - 1)
- timeMid (
2^16
) =((day - 1) * 24 + hour) * 60 + minute
- timeLow (
2^16
) =second * 1000 + millisecond
- timeHigh =
timeHigh + 4096
(Add version 1 (1 * 2^12)) - START UNSCHEDULED
- count =
counter
(get from global) - Increment global counter
- END UNSCHEDULED
- UUID =
[timeHigh, timeMid, timeLow, count]
- UUID append cachedClockSequenceAndNode
- return UUID
The caller will also be able to request for a UUID directly in string form. In this case, the saved string of cachedClockSequenceAndNode can be used, saving the time of formatting 16 hexadecimal digits.
UUID get methods can utilise pooling to reduce the overhead of fetching single or bulk UUIDs. The pool can be increased periodically by a background process. If the pool is empty, fresh UUIDs be generated. Callers can also request a fresh UUID if so desired (For time recording purposes). Requesting a fresh UUID will not require the pool to be dropped.
Pulling items from the pool does not require an unscheduled scope, because SQF's deleteAt
both gets and removes an item from an array. Therefore the process of popping a UUID is atomic. The UUID pooling frequency, increaseAmount, and maxSize can be manually tweaked based on mission UUID usage.
Atomic β An operation that cannot be broken up into smaller parts. Cannot be suspended mid-execution.