using System;
namespace UnityEngine.Localization.Tables
{
///
/// A Key generator that uses the current time in ms with the machine id to generate a
/// unique value every time. This means it is safe for multiple users to add entries to the same table without
/// suffering from conflicts due to the entries using the same key but having different values.
///
/// The implementation is based on this article https://www.callicoder.com/distributed-unique-id-sequence-number-generator/
///
/// The Key is made up of the following components:
///
/// -
/// Sequence Number
/// 12 Bits(0 - 11)
/// A local counter per machine that starts at 0 and is incremented by 1 for each new id request that is made during the same millisecond.
/// The value is limited to 12 bytes so can contain 4095 items before the ids for this millisecond are exhausted and the id generator
/// must wait until the next millisecond before it can continue.
///
///
/// -
/// Machine Id
/// 10 Bits(12-21)
/// The Id of the machine. By default, in the Editor, this value is generated
/// from the machines network interface physical address however it can also be set to a user provided value. There is enough space for 1024 machines.
///
///
/// -
/// Epoch Timestamp.
/// 41 Bits(22-63)
/// A timestamp using a custom epoch which is the time the class was created.
/// The maximum timestamp that can be represented is 69 years from the custom epoch, at this point the Key generator will have exhausted its possible unique Ids.
///
///
///
/// -
/// Signed Bit
/// 1 Bit(64)
/// The signed bit is unused by the ID generator. If you wish to add custom Id values then using the signed bit and adding negative ids will avoid conflicts.
///
///
[Serializable]
public class DistributedUIDGenerator : IKeyGenerator
{
// Configured machine id - 10 bits (gives us up to 1024 machines)
const int kMachineIdBits = 10;
// Sequence number - 12 bits (A local counter per machine that rolls over every 4096)
// The sequence number is used to generate multiple ids per millisecond. This means we can generate
// 4095 ids per ms and must then wait until the next ms before we can continue generating ids.
const int kSequenceBits = 12;
static readonly int kMaxNodeId = (int)(Mathf.Pow(2, kMachineIdBits) - 1);
static readonly int kMaxSequence = (int)(Mathf.Pow(2, kSequenceBits) - 1);
///
/// The name of the EditorPrefs that is used to store the machine id.
///
public const string MachineIdPrefKey = "KeyGenerator-MachineId";
[SerializeField, HideInInspector]
long m_CustomEpoch = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long m_LastTimestamp = -1;
long m_Sequence;
int m_MachineId;
///
/// The custom epoch used to generate the timestamp.
///
public long CustomEpoch => m_CustomEpoch;
///
/// The Id of the current machine. By default, in the Editor, this value is generated
/// from the machines network interface physical address however it can also be set to a user provided value.
/// There is enough space for 1024 unique machines.
/// Set value will be clamped in the range 1-1023.
/// The value is not serialized into the asset but stored into the EditorPrefs(Editor only).
///
public int MachineId
{
get
{
if (m_MachineId == 0)
{
m_MachineId = GetMachineId();
}
return m_MachineId;
}
set
{
m_MachineId = Mathf.Clamp(value, 1, kMaxNodeId);
#if UNITY_EDITOR
UnityEditor.EditorPrefs.SetInt(MachineIdPrefKey, m_MachineId);
#endif
}
}
///
/// Create a default instance which uses the current time as the the machines
/// physical address as .
///
public DistributedUIDGenerator()
{
}
///
/// Creates an instance with a defined .
///
/// The custom epoch is used to calculate the timestamp by taking the
/// current time and subtracting the .
/// The value is then stored in 41 bits giving it a maximum time of 69 years.
public DistributedUIDGenerator(long customEpoch)
{
m_CustomEpoch = customEpoch;
}
///
/// Returns the next Id using the current time, machine id and sequence number.
///
///
public long GetNextKey()
{
var currentTimestamp = TimeStamp();
Debug.Assert(currentTimestamp >= m_LastTimestamp, "Invalid system clock. Current time is less than previous time.");
// If we are generating another id in the same millisecond then we need to increment the sequence
// or wait till the next millisecond if we have exhausted our sequences.
if (currentTimestamp == m_LastTimestamp)
{
m_Sequence = (m_Sequence + 1) & kMaxSequence;
if (m_Sequence == 0)
{
// Sequence Exhausted, wait till next millisecond.
currentTimestamp = WaitNextMillis(currentTimestamp);
}
}
else
{
// reset sequence to start with zero for the next millisecond.
m_Sequence = 0;
}
m_LastTimestamp = currentTimestamp;
long id = currentTimestamp << (kMachineIdBits + kSequenceBits);
id |= (uint)MachineId << kSequenceBits;
id |= m_Sequence;
return id;
}
long TimeStamp() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - m_CustomEpoch;
static int GetMachineId()
{
#if UNITY_EDITOR
var id = UnityEditor.EditorPrefs.GetInt(MachineIdPrefKey, 0);
if (id != 0)
{
return id;
}
foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{
if (nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up)
{
var address = nic.GetPhysicalAddress().ToString();
return address.GetHashCode() & kMaxNodeId;
}
}
#endif
return Random.Range(0, kMaxNodeId);
}
// Block and wait till next millisecond
long WaitNextMillis(long currentTimestamp)
{
while (currentTimestamp == m_LastTimestamp)
{
System.Threading.Thread.Sleep(1);
currentTimestamp = TimeStamp();
}
return currentTimestamp;
}
}
}