using System.Security.Cryptography; using System.Text; namespace NeonSprawl.Server.Game.Contracts; /// Id normalization for contract instance stores (NEO-146). public static class ContractInstanceIds { /// Trim + lowercase; empty when input is null/whitespace. public static string NormalizePlayerId(string? playerId) { var trimmed = playerId?.Trim(); if (string.IsNullOrEmpty(trimmed)) { return string.Empty; } return trimmed.ToLowerInvariant(); } /// Trim + lowercase; empty when input is null/whitespace. public static string NormalizeContractInstanceId(string? contractInstanceId) => NormalizePlayerId(contractInstanceId); /// Trim + lowercase; empty when input is null/whitespace. public static string NormalizeTemplateId(string? templateId) => NormalizePlayerId(templateId); /// Trim; empty when input is null/whitespace (seed bucket casing preserved). public static string NormalizeSeedBucket(string? seedBucket) { var trimmed = seedBucket?.Trim(); return string.IsNullOrEmpty(trimmed) ? string.Empty : trimmed; } /// Composite store key for + . public static string MakeInstanceKey(string? playerId, string? contractInstanceId) { var p = NormalizePlayerId(playerId); var i = NormalizeContractInstanceId(contractInstanceId); if (p.Length == 0 || i.Length == 0) { return string.Empty; } return $"{p}\0{i}"; } /// /// Deterministic contract instance id from normalized seed inputs (NEO-147). /// Format: ci_{32 lowercase hex} from SHA-256 of {playerId}|{templateId}|{seedBucket}. /// public static string MakeDeterministicInstanceId(string? playerId, string? templateId, string? seedBucket) { var p = NormalizePlayerId(playerId); var t = NormalizeTemplateId(templateId); var b = NormalizeSeedBucket(seedBucket); if (p.Length == 0 || t.Length == 0 || b.Length == 0) { return string.Empty; } var payload = $"{p}|{t}|{b}"; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload)); return $"ci_{Convert.ToHexStringLower(hash.AsSpan(0, 16))}"; } }