159 lines
4.6 KiB
C#
159 lines
4.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace RobotAndDonkey.Game.Utils;
|
|
|
|
public static class SeedString
|
|
{
|
|
/// <summary>
|
|
/// Encode an int seed to a short, human-friendly string.
|
|
/// </summary>
|
|
/// <param name="seed">Any 32-bit signed integer.</param>
|
|
/// <param name="includeChecksum">Append 1 char to detect typos.</param>
|
|
/// <param name="group">Insert '-' every 4 chars for readability.</param>
|
|
public static string ToString(int seed, bool includeChecksum = true, bool group = false)
|
|
{
|
|
var u = unchecked((uint)seed);
|
|
|
|
// Convert to base-32 (most-significant digit first)
|
|
if (u == 0)
|
|
return includeChecksum ? "0" + CheckChar(0) : "0";
|
|
|
|
var digits = new List<char>(8);
|
|
while (u > 0)
|
|
{
|
|
var v = (int)(u & 31);
|
|
digits.Add(Alphabet[v]);
|
|
u >>= 5;
|
|
}
|
|
|
|
digits.Reverse();
|
|
|
|
var sb = new StringBuilder(digits.Count + (includeChecksum ? 1 : 0) + (group ? digits.Count / 4 : 0));
|
|
for (var i = 0; i < digits.Count; i++)
|
|
{
|
|
if (group && i > 0 && i % 4 == 0)
|
|
sb.Append('-');
|
|
sb.Append(digits[i]);
|
|
}
|
|
|
|
if (includeChecksum)
|
|
sb.Append(CheckChar(unchecked((uint)seed)));
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to parse a seed string back to the original int.
|
|
/// </summary>
|
|
/// <param name="code">Case-insensitive; hyphens/spaces/underscores ignored. O=0, I/L=1 accepted.</param>
|
|
/// <param name="seed">Decoded seed.</param>
|
|
/// <param name="requireChecksum">If true, only accept strings with a valid trailing checksum.</param>
|
|
public static bool TryParse(string code, out int seed, bool requireChecksum = false)
|
|
{
|
|
seed = 0;
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
return false;
|
|
|
|
var t = Normalize(code);
|
|
if (t.Length == 0)
|
|
return false;
|
|
|
|
// If checksum is required (or present), prefer validating it:
|
|
if (t.Length >= 2)
|
|
{
|
|
if (TryParsePayload(t.AsSpan(0, t.Length - 1), out var u) && TryMap(t[^1], out var chk) && chk == Check5(u))
|
|
{
|
|
seed = unchecked((int)u);
|
|
return true;
|
|
}
|
|
|
|
if (requireChecksum)
|
|
return false;
|
|
}
|
|
|
|
// Fallback: treat entire string as payload without checksum.
|
|
if (TryParsePayload(t.AsSpan(), out var uv))
|
|
{
|
|
seed = unchecked((int)uv);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse or throw (optionally requiring checksum).
|
|
/// </summary>
|
|
public static int Parse(string code, bool requireChecksum = false)
|
|
{
|
|
return TryParse(code, out var seed, requireChecksum) ? seed : throw new FormatException("Invalid seed string.");
|
|
}
|
|
|
|
// ---------- publics ----------
|
|
|
|
private static Dictionary<char, int> BuildMap()
|
|
{
|
|
var d = new Dictionary<char, int>(64);
|
|
for (var i = 0; i < Alphabet.Length; i++)
|
|
d[Alphabet[i]] = i;
|
|
|
|
// Forgiving mappings
|
|
d['O'] = d['0']; // O -> 0
|
|
d['I'] = d['1']; // I -> 1
|
|
d['L'] = d['1']; // L -> 1
|
|
return d;
|
|
}
|
|
|
|
private static string Normalize(string s)
|
|
{
|
|
var sb = new StringBuilder(s.Length);
|
|
foreach (var ch in s)
|
|
{
|
|
if (ch == '-' || ch == '_' || char.IsWhiteSpace(ch))
|
|
continue;
|
|
sb.Append(char.ToUpperInvariant(ch));
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static bool TryParsePayload(ReadOnlySpan<char> span, out uint u)
|
|
{
|
|
u = 0;
|
|
if (span.Length == 0)
|
|
return false;
|
|
if (span.Length > 7)
|
|
return false; // 7 base32 digits cover full 32-bit range (5 bits per digit)
|
|
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
if (!TryMap(span[i], out var v))
|
|
return false;
|
|
u = (u << 5) | (uint)v;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryMap(char c, out int v)
|
|
{
|
|
return CharToVal.TryGetValue(char.ToUpperInvariant(c), out v);
|
|
}
|
|
|
|
// 5-bit checksum: multiplicative hash then take top 5 bits.
|
|
private static int Check5(uint u)
|
|
{
|
|
return (int)((u * 0x9E3779B1u) >> 27);
|
|
}
|
|
|
|
private static char CheckChar(uint u)
|
|
{
|
|
return Alphabet[Check5(u)];
|
|
}
|
|
|
|
// Crockford Base32 (no I, L, O, U)
|
|
private const string Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
private static readonly Dictionary<char, int> CharToVal = BuildMap();
|
|
} |