Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Wildblood v0.5.7
plugins/Wildblood/Wildblood.Core.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using ExtraPotions.Wildblood.Bosses; using ExtraPotions.Wildblood.Config; using ExtraPotions.Wildblood.Conversion; using ExtraPotions.Wildblood.Integration; using ExtraPotions.Wildblood.Loot; using ExtraPotions.Wildblood.Stars; using ExtraPotions.Wildblood.Traits; using ExtraPotions.Wildblood.Transfers; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ExtraPotions")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (C) 2026 expDARE (ExtraPotions Meadworks)")] [assembly: AssemblyFileVersion("0.5.7.0")] [assembly: AssemblyInformationalVersion("0.5.7+fatal: not a git repository (or any of the parent directories): .git")] [assembly: AssemblyProduct("Wildblood.Core")] [assembly: AssemblyTitle("Wildblood.Core")] [assembly: AssemblyVersion("0.5.7.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ExtraPotions.Wildblood { public interface IDice { float Value(); } public static class DiceExtensions { public static bool Percent(this IDice dice, float chancePercent) { if (chancePercent <= 0f) { return false; } if (chancePercent >= 100f) { dice.Value(); return true; } return dice.Value() * 100f < chancePercent; } public static int Range(this IDice dice, int min, int maxExclusive) { if (maxExclusive <= min) { dice.Value(); return min; } float num = Math.Min(MathF.PI * 113f / 355f, Math.Max(0f, dice.Value())); return min + (int)Math.Floor(num * (float)(maxExclusive - min)); } public static int PickWeighted(this IDice dice, float[] weights) { if (weights == null) { throw new ArgumentNullException("weights"); } float num = 0f; foreach (float val in weights) { num += Math.Max(0f, val); } float num2 = Math.Min(MathF.PI * 113f / 355f, Math.Max(0f, dice.Value())) * num; if (num <= 0f) { return 0; } float num3 = 0f; int result = 0; for (int j = 0; j < weights.Length; j++) { if (!(weights[j] <= 0f)) { result = j; num3 += weights[j]; if (num2 < num3) { return j; } } } return result; } } public sealed class SeededDice : IDice { private readonly Random random; public SeededDice(int seed) { random = new Random(seed); } public float Value() { return (float)random.NextDouble(); } } public static class SpawnListPass { public static bool SkipOriginal(bool eventSpawns, bool areaBlocked) { return !eventSpawns && areaBlocked; } public static bool HordeGrows(bool eventSpawns, bool hasSpawners, bool enabled, float multiplier, bool listSkipped) { if (!listSkipped && !eventSpawns && hasSpawners && enabled) { return multiplier > 1f; } return false; } } public static class WildbloodCore { public const string PluginGuid = "com.meadworks.wildblood"; public const string ProductName = "Wildblood"; public const string ProductVersion = "0.5.7"; public const string Family = "Fang"; public const string TokenNamespace = "wildblood"; public const string TokenPrefix = "meadworks_wildblood_"; public const string SupportedGameVersion = "1.0.17"; } } namespace ExtraPotions.Wildblood.Visuals { public static class FlameVisualPolicy { public const float DefaultLife = 7f; public const float DefaultRadius = 1f; public static float BodyScale(float radius) { return Math.Max(0.5f, radius * 2f) * 0.4f; } public static bool BodyVisible(bool cloaked, bool cloning) { if (!cloaked) { return !cloning; } return false; } public static bool DrawDecals(bool flamesAvailable, float density) { if (flamesAvailable) { return Density(density) <= 0f; } return true; } private static float Density(float value) { return Math.Max(0f, Math.Min(1f, value)); } public static float PerPatchRate(float densityAdjustedRate, float density) { return densityAdjustedRate / Math.Max(Density(density), 0.01f) * 0.5f; } public static int Take(ref float owed, float rate, int patches, float elapsed, float density) { float num = (float)patches * elapsed * Density(density); owed = ((num > 0f) ? (owed + rate * num) : 0f); int num2 = (int)Math.Floor(owed); owed -= num2; return num2; } } } namespace ExtraPotions.Wildblood.Transfers { public interface IInventoryImageStore { bool IsBound(string source); byte[] Capture(string source); void Apply(string source, byte[] image); void Save(string source); } public interface IInventoryCheckpointStore { bool ReceiptConfirmed(string source); } public interface IInventoryDecisionObserver { void DecisionFlushed(InventoryDecisionRecord record); } public interface IInventoryRecoveryDiagnostics { void RecoveryRequired(string code); } public sealed class DurableInventoryParticipant { private readonly InventoryDecisionJournal journal; private readonly IInventoryImageStore store; private readonly Func<double> clock; public DurableInventoryParticipant(InventoryDecisionJournal journal, IInventoryImageStore store, Func<double> monotonicClock) { this.journal = journal ?? throw new ArgumentNullException("journal"); this.store = store ?? throw new ArgumentNullException("store"); clock = monotonicClock ?? throw new ArgumentNullException("monotonicClock"); foreach (InventoryDecisionRecord item in from r in journal.Snapshot() where r.State == InventoryDecision.Prepared select r) { journal.Transition(item, InventoryDecision.Aborted); } } public bool IsHeld(string source) { return journal.Snapshot().Any((InventoryDecisionRecord r) => r.Source == source && (r.State == InventoryDecision.Prepared || r.State == InventoryDecision.Decided || (r.State == InventoryDecision.Committed && store is IInventoryCheckpointStore inventoryCheckpointStore && !inventoryCheckpointStore.ReceiptConfirmed(source)))); } public InventoryDecision Prepare(string id, string source, string fingerprint, byte[] before, byte[] after, byte[]? metadata = null) { Expire(); InventoryDecisionRecord inventoryDecisionRecord = journal.Find(id); if (inventoryDecisionRecord != null) { if (!(inventoryDecisionRecord.Source == source) || !(inventoryDecisionRecord.Fingerprint == fingerprint) || !((ReadOnlySpan<byte>)inventoryDecisionRecord.Before).SequenceEqual((ReadOnlySpan<byte>)before) || !((ReadOnlySpan<byte>)inventoryDecisionRecord.After).SequenceEqual((ReadOnlySpan<byte>)after) || !((ReadOnlySpan<byte>)inventoryDecisionRecord.Metadata).SequenceEqual((ReadOnlySpan<byte>)(metadata ?? Array.Empty<byte>()))) { return InventoryDecision.Rejected; } return Result(inventoryDecisionRecord); } if (!journal.CanAdmit || IsHeld(source) || !store.IsBound(source) || !((ReadOnlySpan<byte>)store.Capture(source)).SequenceEqual((ReadOnlySpan<byte>)before)) { return InventoryDecision.Rejected; } try { journal.Prepare(id, source, fingerprint, clock() + 10.0, before, after, metadata); return InventoryDecision.Prepared; } catch { return InventoryDecision.Rejected; } } public InventoryDecision Status(string id) { Expire(); InventoryDecisionRecord inventoryDecisionRecord = journal.Find(id); if (inventoryDecisionRecord != null) { return Result(inventoryDecisionRecord); } return InventoryDecision.Rejected; } private InventoryDecision Result(InventoryDecisionRecord record) { if (record.State != InventoryDecision.Decided && (record.State != InventoryDecision.Committed || !(store is IInventoryCheckpointStore inventoryCheckpointStore) || inventoryCheckpointStore.ReceiptConfirmed(record.Source))) { return record.State; } return InventoryDecision.RecoveryRequired; } public InventoryDecision Commit(string id) { Expire(); InventoryDecisionRecord inventoryDecisionRecord = journal.Find(id); if (inventoryDecisionRecord == null) { return InventoryDecision.Rejected; } if (inventoryDecisionRecord.State == InventoryDecision.Aborted || (inventoryDecisionRecord.State == InventoryDecision.Committed && Result(inventoryDecisionRecord) == InventoryDecision.Committed)) { return inventoryDecisionRecord.State; } try { if (inventoryDecisionRecord.State == InventoryDecision.Prepared) { if (!store.IsBound(inventoryDecisionRecord.Source)) { journal.Transition(inventoryDecisionRecord, InventoryDecision.Decided); return Recovery("BindingUnavailable"); } if (!((ReadOnlySpan<byte>)store.Capture(inventoryDecisionRecord.Source)).SequenceEqual((ReadOnlySpan<byte>)inventoryDecisionRecord.Before)) { journal.Transition(inventoryDecisionRecord, InventoryDecision.Aborted); return InventoryDecision.Aborted; } journal.Transition(inventoryDecisionRecord, InventoryDecision.Decided); } if (!store.IsBound(inventoryDecisionRecord.Source)) { return Recovery("BindingUnavailable"); } (store as IInventoryDecisionObserver)?.DecisionFlushed(inventoryDecisionRecord); byte[] array = store.Capture(inventoryDecisionRecord.Source); if (((ReadOnlySpan<byte>)array).SequenceEqual((ReadOnlySpan<byte>)inventoryDecisionRecord.Before)) { store.Apply(inventoryDecisionRecord.Source, inventoryDecisionRecord.After); } else if (!((ReadOnlySpan<byte>)array).SequenceEqual((ReadOnlySpan<byte>)inventoryDecisionRecord.After)) { return Recovery("InventoryImageConflict"); } if (!store.IsBound(inventoryDecisionRecord.Source) || !((ReadOnlySpan<byte>)store.Capture(inventoryDecisionRecord.Source)).SequenceEqual((ReadOnlySpan<byte>)inventoryDecisionRecord.After)) { return Recovery("ImageNotApplied"); } store.Save(inventoryDecisionRecord.Source); journal.Transition(inventoryDecisionRecord, InventoryDecision.Committed); return InventoryDecision.Committed; } catch (Exception ex) { return Recovery("Persistence" + ex.GetType().Name); } } private InventoryDecision Recovery(string code) { (store as IInventoryRecoveryDiagnostics)?.RecoveryRequired(code); return InventoryDecision.RecoveryRequired; } public InventoryDecision Rollback(string id) { InventoryDecisionRecord inventoryDecisionRecord = journal.Find(id); if (inventoryDecisionRecord == null) { return InventoryDecision.Rejected; } if (inventoryDecisionRecord.State == InventoryDecision.Prepared) { try { journal.Transition(inventoryDecisionRecord, InventoryDecision.Aborted); } catch { return InventoryDecision.RecoveryRequired; } } return Result(inventoryDecisionRecord); } public void Expire() { foreach (InventoryDecisionRecord item in from r in journal.Snapshot() where r.State == InventoryDecision.Prepared && clock() >= r.Deadline select r) { journal.Transition(item, InventoryDecision.Aborted); } } } public enum InventoryDecision { Rejected, Prepared, Decided, Committed, Aborted, RecoveryRequired } public sealed class InventoryDecisionRecord { private readonly byte[] before; private readonly byte[] after; private readonly byte[] metadata; public string Id { get; } public string Source { get; } public string Fingerprint { get; } public double Deadline { get; } public InventoryDecision State { get; internal set; } public byte[] Before => (byte[])before.Clone(); public byte[] After => (byte[])after.Clone(); public byte[] Metadata => (byte[])metadata.Clone(); internal InventoryDecisionRecord(string id, string source, string fingerprint, double deadline, byte[] before, byte[] after, InventoryDecision state, byte[]? metadata = null) { Id = id; Source = source; Fingerprint = fingerprint; Deadline = deadline; this.before = (byte[])before.Clone(); this.after = (byte[])after.Clone(); this.metadata = ((metadata == null) ? Array.Empty<byte>() : ((byte[])metadata.Clone())); State = state; } } public sealed class InventoryDecisionJournal : IDisposable { private const int Magic = 1113018929; private const int MaxFrame = 4194304; private const long MaxFile = 268435456L; private readonly Dictionary<string, InventoryDecisionRecord> records = new Dictionary<string, InventoryDecisionRecord>(StringComparer.Ordinal); private readonly FileStream stream; private readonly int capacity; private bool disposed; private bool faulted; public bool CanAdmit { get { if (!disposed && !faulted && records.Count < capacity) { return stream.Length < 264241152; } return false; } } public InventoryDecisionJournal(string path, int capacity = 4096) : this(path, capacity, requireExisting: false) { } public InventoryDecisionJournal(string path, bool requireExisting) : this(path, 4096, requireExisting) { } public InventoryDecisionJournal(string path, int capacity, bool requireExisting) { if (capacity < 1 || capacity > 4096) { throw new ArgumentOutOfRangeException("capacity"); } this.capacity = capacity; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); stream = new FileStream(path, requireExisting ? FileMode.Open : FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); try { Read(); } catch { stream.Dispose(); throw; } } public InventoryDecisionRecord? Find(string id) { if (!records.TryGetValue(id, out InventoryDecisionRecord value)) { return null; } return value; } public IReadOnlyList<InventoryDecisionRecord> Snapshot() { return Array.AsReadOnly(records.Values.ToArray()); } public InventoryDecisionRecord Prepare(string id, string source, string fingerprint, double deadline, byte[] before, byte[] after, byte[]? metadata = null) { Text(id); Text(source); Text(fingerprint); if (!CanAdmit || records.ContainsKey(id) || double.IsNaN(deadline) || double.IsInfinity(deadline) || before == null || after == null || before.Length > 2080768 || after.Length > 2080768 || ((metadata != null) ? metadata.Length : 0) > 16384) { throw new InvalidOperationException("Journal admission denied."); } InventoryDecisionRecord inventoryDecisionRecord = new InventoryDecisionRecord(id, source, fingerprint, deadline, before, after, InventoryDecision.Prepared, metadata); Append(inventoryDecisionRecord); records.Add(id, inventoryDecisionRecord); return inventoryDecisionRecord; } public void Transition(InventoryDecisionRecord record, InventoryDecision next) { if (disposed || faulted || !records.TryGetValue(record.Id, out InventoryDecisionRecord value) || record != value) { throw new InvalidOperationException("Inactive journal record."); } if (!Allowed(record.State, next)) { throw new InvalidOperationException("Invalid durable inventory transition."); } if (next != record.State) { InventoryDecisionRecord record2 = new InventoryDecisionRecord(record.Id, record.Source, record.Fingerprint, record.Deadline, record.Before, record.After, next, record.Metadata); Append(record2); record.State = next; } } private static bool Allowed(InventoryDecision old, InventoryDecision next) { if (old != next && (old != InventoryDecision.Prepared || (next != InventoryDecision.Decided && next != InventoryDecision.Aborted))) { if (old == InventoryDecision.Decided) { return next == InventoryDecision.Committed; } return false; } return true; } private static void Text(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length > 512 || value.IndexOfAny(new char[3] { '\0', '\r', '\n' }) >= 0) { throw new InvalidDataException("Invalid journal identity."); } } private void Append(InventoryDecisionRecord record) { try { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(2); binaryWriter.Write(record.Id); binaryWriter.Write(record.Source); binaryWriter.Write(record.Fingerprint); binaryWriter.Write(record.Deadline); binaryWriter.Write((int)record.State); WriteImage(binaryWriter, record.Before); WriteImage(binaryWriter, record.After); WriteImage(binaryWriter, record.Metadata); } byte[] array = memoryStream.ToArray(); if (array.Length > 4194304 || stream.Length + array.Length + 40 > 268435456) { throw new IOException("Journal storage capacity reached."); } using (BinaryWriter binaryWriter2 = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true)) { binaryWriter2.Write(1113018929); binaryWriter2.Write(array.Length); binaryWriter2.Write(array); using (SHA256 sHA = SHA256.Create()) { binaryWriter2.Write(sHA.ComputeHash(array)); } binaryWriter2.Flush(); } stream.Flush(flushToDisk: true); } catch { faulted = true; throw; } } private static void WriteImage(BinaryWriter writer, byte[] image) { writer.Write(image.Length); writer.Write(image); } private static byte[] ReadImage(BinaryReader reader) { int num = reader.ReadInt32(); if (num < 0 || num > 2096128 || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException("Invalid inventory image."); } return reader.ReadBytes(num); } private void Read() { if (stream.Length > 268435456) { throw new InvalidDataException("Journal too large."); } long complete = 0L; using (BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true)) { while (stream.Position < stream.Length) { if (stream.Length - stream.Position < 8) { Truncate(complete); break; } if (binaryReader.ReadInt32() != 1113018929) { throw new InvalidDataException("Journal frame signature mismatch."); } int num = binaryReader.ReadInt32(); if (num < 0 || num > 4194304) { throw new InvalidDataException("Journal frame exceeds bounds."); } if (stream.Length - stream.Position < (long)num + 32L) { Truncate(complete); break; } byte[] buffer = binaryReader.ReadBytes(num); byte[] array = binaryReader.ReadBytes(32); using (SHA256 sHA = SHA256.Create()) { if (!((ReadOnlySpan<byte>)array).SequenceEqual((ReadOnlySpan<byte>)sHA.ComputeHash(buffer))) { throw new InvalidDataException("Journal frame checksum mismatch."); } } InventoryDecisionRecord inventoryDecisionRecord; try { using MemoryStream memoryStream = new MemoryStream(buffer); using BinaryReader binaryReader2 = new BinaryReader(memoryStream, Encoding.UTF8); int num2 = binaryReader2.ReadInt32(); if (num2 != 1 && num2 != 2) { throw new InvalidDataException("Unknown journal version."); } string text = binaryReader2.ReadString(); string text2 = binaryReader2.ReadString(); string text3 = binaryReader2.ReadString(); Text(text); Text(text2); Text(text3); double num3 = binaryReader2.ReadDouble(); if (double.IsNaN(num3) || double.IsInfinity(num3)) { throw new InvalidDataException("Invalid deadline."); } InventoryDecision inventoryDecision = (InventoryDecision)binaryReader2.ReadInt32(); if (inventoryDecision != InventoryDecision.Prepared && inventoryDecision != InventoryDecision.Decided && inventoryDecision != InventoryDecision.Committed && inventoryDecision != InventoryDecision.Aborted) { throw new InvalidDataException("Invalid decision state."); } byte[] before = ReadImage(binaryReader2); byte[] after = ReadImage(binaryReader2); byte[] array2 = ((num2 == 2) ? ReadImage(binaryReader2) : Array.Empty<byte>()); if (array2.Length > 16384) { throw new InvalidDataException("Journal metadata exceeds bounds."); } inventoryDecisionRecord = new InventoryDecisionRecord(text, text2, text3, num3, before, after, inventoryDecision, array2); if (memoryStream.Position != memoryStream.Length) { throw new InvalidDataException("Journal record has trailing bytes."); } } catch (EndOfStreamException innerException) { throw new InvalidDataException("Invalid complete journal frame.", innerException); } if (records.TryGetValue(inventoryDecisionRecord.Id, out InventoryDecisionRecord value)) { if (inventoryDecisionRecord.Source != value.Source || inventoryDecisionRecord.Fingerprint != value.Fingerprint || inventoryDecisionRecord.Deadline != value.Deadline || !((ReadOnlySpan<byte>)inventoryDecisionRecord.Before).SequenceEqual((ReadOnlySpan<byte>)value.Before) || !((ReadOnlySpan<byte>)inventoryDecisionRecord.After).SequenceEqual((ReadOnlySpan<byte>)value.After) || !((ReadOnlySpan<byte>)inventoryDecisionRecord.Metadata).SequenceEqual((ReadOnlySpan<byte>)value.Metadata) || !Allowed(value.State, inventoryDecisionRecord.State)) { throw new InvalidDataException("Conflicting journal history."); } } else if (records.Count >= capacity || inventoryDecisionRecord.State != InventoryDecision.Prepared) { throw new InvalidDataException("Invalid journal admission history."); } records[inventoryDecisionRecord.Id] = inventoryDecisionRecord; complete = stream.Position; } } stream.Position = stream.Length; } private void Truncate(long complete) { stream.SetLength(complete); stream.Position = complete; stream.Flush(flushToDisk: true); } public void Dispose() { if (!disposed) { disposed = true; stream.Dispose(); } } } public enum PouchDropDecision { Create, Created, Unknown, Unavailable } public enum PouchDropState { None, Marked, Created } public static class PouchDropKeys { public static string Entry(long sourceUser, uint sourceId, long stealer, int counter, string root, byte[] item) { if (item == null) { throw new ArgumentNullException("item"); } using SHA256 sHA = SHA256.Create(); using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write("wb.pouch.drop.v1"); binaryWriter.Write(sourceUser); binaryWriter.Write(sourceId); binaryWriter.Write(stealer); binaryWriter.Write(counter); binaryWriter.Write(root ?? ""); binaryWriter.Write(item.Length); binaryWriter.Write(item); } return Hex(sHA.ComputeHash(memoryStream.ToArray())); } internal static string Hex(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant(); } public static bool IsKey(string? value) { if (value != null && value.Length == 64) { return value.All((char c) => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); } return false; } } public sealed class PouchDropLedger : IDisposable { private const string Source = "wildblood.pouch.drop"; private const string CreatedSuffix = ".created"; private readonly string directory; private readonly int segmentCapacity; private readonly int maxSegments; private readonly List<InventoryDecisionJournal> segments = new List<InventoryDecisionJournal>(); private readonly Dictionary<string, InventoryDecisionRecord> records = new Dictionary<string, InventoryDecisionRecord>(StringComparer.Ordinal); public IReadOnlyList<string> Unresolved => records.Keys.Where((string k) => !k.EndsWith(".created", StringComparison.Ordinal) && !records.ContainsKey(k + ".created")).ToArray(); public PouchDropLedger(string directory, int segmentCapacity = 4096, int maxSegments = 64) { if (segmentCapacity < 2 || segmentCapacity > 4096) { throw new ArgumentOutOfRangeException("segmentCapacity"); } if (maxSegments < 1 || maxSegments > 1024) { throw new ArgumentOutOfRangeException("maxSegments"); } this.directory = directory; this.segmentCapacity = segmentCapacity; this.maxSegments = maxSegments; try { Directory.CreateDirectory(directory); for (int i = 0; i < maxSegments && File.Exists(SegmentPath(i)); i++) { Open(i); } if (segments.Count == 0) { Open(0); } } catch { Dispose(); throw; } } private string SegmentPath(int index) { return Path.Combine(directory, "drops-" + index.ToString("0000") + ".bin"); } private void Open(int index) { InventoryDecisionJournal inventoryDecisionJournal = new InventoryDecisionJournal(SegmentPath(index), segmentCapacity); segments.Add(inventoryDecisionJournal); foreach (InventoryDecisionRecord item in inventoryDecisionJournal.Snapshot()) { if (records.ContainsKey(item.Id)) { throw new InvalidDataException("Duplicate pouch drop decision."); } records.Add(item.Id, item); } } public PouchDropState State(string key) { if (records.ContainsKey(key + ".created")) { return PouchDropState.Created; } if (!records.ContainsKey(key)) { return PouchDropState.None; } return PouchDropState.Marked; } public byte[]? MarkedItem(string key) { if (!records.TryGetValue(key, out InventoryDecisionRecord value)) { return null; } return value.Before; } public string? CreatedIdentity(string key) { if (!records.TryGetValue(key + ".created", out InventoryDecisionRecord value)) { return null; } return Encoding.UTF8.GetString(value.Metadata); } public PouchDropDecision Begin(string key, bool markedElsewhere, byte[]? item = null) { if (!PouchDropKeys.IsKey(key)) { throw new ArgumentException("Pouch drop key.", "key"); } switch (State(key)) { case PouchDropState.Created: return PouchDropDecision.Created; case PouchDropState.Marked: return PouchDropDecision.Unknown; default: { if (markedElsewhere) { return PouchDropDecision.Unknown; } InventoryDecisionJournal inventoryDecisionJournal = Writable(); if (inventoryDecisionJournal == null) { return PouchDropDecision.Unavailable; } InventoryDecisionRecord value = inventoryDecisionJournal.Prepare(key, "wildblood.pouch.drop", key, 0.0, item ?? Array.Empty<byte>(), Array.Empty<byte>()); records.Add(key, value); return PouchDropDecision.Create; } } } public bool Complete(string key, string identity) { if (string.IsNullOrWhiteSpace(identity) || identity.Length > 128) { return false; } string text = CreatedIdentity(key); if (text != null) { return text == identity; } if (!records.ContainsKey(key)) { return false; } InventoryDecisionJournal inventoryDecisionJournal = Writable(); if (inventoryDecisionJournal == null) { return false; } InventoryDecisionRecord inventoryDecisionRecord = inventoryDecisionJournal.Prepare(key + ".created", "wildblood.pouch.drop", key, 0.0, Array.Empty<byte>(), Array.Empty<byte>(), Encoding.UTF8.GetBytes(identity)); records.Add(inventoryDecisionRecord.Id, inventoryDecisionRecord); return true; } private InventoryDecisionJournal? Writable() { InventoryDecisionJournal inventoryDecisionJournal = segments[segments.Count - 1]; if (inventoryDecisionJournal.CanAdmit) { return inventoryDecisionJournal; } if (segments.Count >= maxSegments) { return null; } Open(segments.Count); if (!segments[segments.Count - 1].CanAdmit) { return null; } return segments[segments.Count - 1]; } public void Dispose() { foreach (InventoryDecisionJournal segment in segments) { segment.Dispose(); } segments.Clear(); records.Clear(); } } public static class PouchDropMarker { public const int MaxKeys = 64; public static byte[] Encode(IEnumerable<string> keys) { List<string> list = keys.ToList(); if (list.Count > 64 || list.Any((string k) => !PouchDropKeys.IsKey(k)) || list.Distinct<string>(StringComparer.Ordinal).Count() != list.Count) { throw new InvalidDataException("Pouch drop marker."); } using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(1); binaryWriter.Write(list.Count); foreach (string item in list) { binaryWriter.Write(item); } } return memoryStream.ToArray(); } public static IReadOnlyList<string> Decode(byte[]? bytes) { if (bytes == null || bytes.Length == 0) { return Array.Empty<string>(); } if (bytes.Length > 4168) { throw new InvalidDataException("Pouch drop marker exceeds its bound."); } using BinaryReader binaryReader = new BinaryReader(new MemoryStream(bytes, writable: false), Encoding.UTF8); try { if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Pouch drop marker version."); } int num = binaryReader.ReadInt32(); if (num < 0 || num > 64) { throw new InvalidDataException("Pouch drop marker count."); } List<string> list = new List<string>(num); for (int i = 0; i < num; i++) { string text = binaryReader.ReadString(); if (!PouchDropKeys.IsKey(text) || list.Contains(text)) { throw new InvalidDataException("Pouch drop marker key."); } list.Add(text); } if (binaryReader.BaseStream.Position != bytes.Length) { throw new InvalidDataException("Pouch drop marker trailing data."); } return list; } catch (EndOfStreamException) { throw new InvalidDataException("Pouch drop marker truncated."); } } } public enum TheftStatusAnswer : byte { Unknown, Approved, Denied } public sealed class TheftAsk { public string Root { get; } public int Counter { get; } public double SentAt { get; } public string Fingerprint { get; } public byte[] Item { get; } public TheftAsk(string root, int counter, string fingerprint, byte[] item, double sentAt) { Root = root; Counter = counter; Fingerprint = fingerprint; Item = item; SentAt = sentAt; } } public sealed class TheftStatusQuery { public string Root { get; } public string Fingerprint { get; } public TheftStatusAnswer Answer { get; } public TheftStatusQuery(string root, string fingerprint, TheftStatusAnswer answer) { Root = root; Fingerprint = fingerprint; Answer = answer; } } public static class TheftWire { public const int Version = 2; public const int MaxItem = 1048576; public const double AskWindowSeconds = 5.0; public const double ReplyDeadlineSeconds = 10.0; private const byte Ask = 4; private const byte Status = 2; private const byte Answer = 3; public static bool Fresh(double sentAt, double now) { if (Finite(sentAt) && Finite(now) && now - sentAt <= 5.0) { return sentAt - now <= 5.0; } return false; } private static bool Finite(double value) { if (!double.IsNaN(value) && !double.IsInfinity(value)) { return value >= 0.0; } return false; } public static bool IsRoot(string? value) { return Hex(value, 32); } public static bool IsFingerprint(string? value) { return Hex(value, 64); } private static bool Hex(string? value, int length) { if (value != null && value.Length == length) { return value.All((char c) => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); } return false; } public static bool ItemMatches(string fingerprint, byte[] item) { if (item != null && item.Length != 0) { return TheftMetadata.Hash(item) == fingerprint; } return false; } public static byte[] EncodeAsk(string root, int counter, string fingerprint, byte[] item, double sentAt) { if (!IsRoot(root) || counter <= 0 || !IsFingerprint(fingerprint) || item == null || item.Length == 0 || item.Length > 1048576 || !Finite(sentAt)) { throw new InvalidDataException("Theft ask."); } return Write(delegate(BinaryWriter w) { w.Write((byte)4); w.Write(root); w.Write(counter); w.Write(sentAt); w.Write(fingerprint); w.Write(item.Length); w.Write(item); }); } public static TheftAsk DecodeAsk(byte[] bytes) { return Read(bytes, 4, 1048832, delegate(BinaryReader r) { string text = r.ReadString(); int num = r.ReadInt32(); double num2 = r.ReadDouble(); string text2 = r.ReadString(); int num3 = r.ReadInt32(); if (!IsRoot(text) || num <= 0 || !Finite(num2) || !IsFingerprint(text2) || num3 <= 0 || num3 > 1048576) { throw new InvalidDataException("Theft ask."); } byte[] array = r.ReadBytes(num3); if (array.Length != num3) { throw new InvalidDataException("Theft ask truncated."); } return new TheftAsk(text, num, text2, array, num2); }); } public static byte[] EncodeStatus(string root, string fingerprint) { return Identity(2, root, fingerprint, null); } public static byte[] EncodeAnswer(string root, string fingerprint, TheftStatusAnswer answer) { return Identity(3, root, fingerprint, answer); } public static TheftStatusQuery DecodeStatus(byte[] bytes) { return Read(bytes, 2, 256, (BinaryReader r) => ReadIdentity(r, answer: false)); } public static TheftStatusQuery DecodeAnswer(byte[] bytes) { return Read(bytes, 3, 256, (BinaryReader r) => ReadIdentity(r, answer: true)); } private static byte[] Identity(byte kind, string root, string fingerprint, TheftStatusAnswer? answer) { if (!IsRoot(root) || !IsFingerprint(fingerprint) || (answer.HasValue && !Enum.IsDefined(typeof(TheftStatusAnswer), answer.Value))) { throw new InvalidDataException("Theft status identity."); } return Write(delegate(BinaryWriter w) { w.Write(kind); w.Write(root); w.Write(fingerprint); if (answer.HasValue) { w.Write((byte)answer.Value); } }); } private static TheftStatusQuery ReadIdentity(BinaryReader r, bool answer) { string text = r.ReadString(); string text2 = r.ReadString(); TheftStatusAnswer theftStatusAnswer = (TheftStatusAnswer)(answer ? r.ReadByte() : 0); if (!IsRoot(text) || !IsFingerprint(text2) || !Enum.IsDefined(typeof(TheftStatusAnswer), theftStatusAnswer)) { throw new InvalidDataException("Theft status identity."); } return new TheftStatusQuery(text, text2, theftStatusAnswer); } private static byte[] Write(Action<BinaryWriter> body) { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(2); body(binaryWriter); } return memoryStream.ToArray(); } private static T Read<T>(byte[] bytes, byte kind, int bound, Func<BinaryReader, T> body) { if (bytes == null || bytes.Length == 0 || bytes.Length > bound) { throw new InvalidDataException("Theft message bound."); } using BinaryReader binaryReader = new BinaryReader(new MemoryStream(bytes, writable: false), Encoding.UTF8); try { if (binaryReader.ReadInt32() != 2 || binaryReader.ReadByte() != kind) { throw new InvalidDataException("Theft message kind."); } T result = body(binaryReader); if (binaryReader.BaseStream.Position != bytes.Length) { throw new InvalidDataException("Theft message trailing data."); } return result; } catch (EndOfStreamException) { throw new InvalidDataException("Theft message truncated."); } } } public sealed class TheftOutcomeArchive { public const int Capacity = 32; private readonly List<(string Root, string Fingerprint, bool Approved)> entries = new List<(string, string, bool)>(); public int Count => entries.Count; public bool CanRecord => entries.Count < 32; public IReadOnlyList<(string Root, string Fingerprint, bool Approved)> Entries => entries.AsReadOnly(); public TheftStatusAnswer Find(string root, string fingerprint) { foreach (var entry in entries) { if (entry.Root == root) { return (!(entry.Fingerprint != fingerprint)) ? (entry.Approved ? TheftStatusAnswer.Approved : TheftStatusAnswer.Denied) : TheftStatusAnswer.Unknown; } } return TheftStatusAnswer.Unknown; } public bool Contains(string root) { return entries.Any<(string, string, bool)>(((string Root, string Fingerprint, bool Approved) e) => e.Root == root); } public bool Record(string root, string fingerprint, bool approved) { if (!TheftWire.IsRoot(root) || !TheftWire.IsFingerprint(fingerprint)) { return false; } foreach (var entry in entries) { if (entry.Root == root) { return entry.Fingerprint == fingerprint && entry.Approved == approved; } } if (!CanRecord) { return false; } entries.Add((root, fingerprint, approved)); return true; } public bool Acknowledge(string root, string fingerprint) { int num = entries.FindIndex(((string Root, string Fingerprint, bool Approved) e) => e.Root == root && e.Fingerprint == fingerprint); if (num < 0) { return false; } entries.RemoveAt(num); return true; } public byte[] Encode() { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(1); binaryWriter.Write(entries.Count); foreach (var entry in entries) { binaryWriter.Write(entry.Root); binaryWriter.Write(entry.Fingerprint); binaryWriter.Write(entry.Approved); } } return memoryStream.ToArray(); } public static TheftOutcomeArchive Decode(byte[]? bytes) { TheftOutcomeArchive theftOutcomeArchive = new TheftOutcomeArchive(); if (bytes == null || bytes.Length == 0) { return theftOutcomeArchive; } if (bytes.Length > 3208) { throw new InvalidDataException("Theft outcome archive exceeds its bound."); } using BinaryReader binaryReader = new BinaryReader(new MemoryStream(bytes, writable: false), Encoding.UTF8); try { if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Theft outcome archive version."); } int num = binaryReader.ReadInt32(); if (num < 0 || num > 32) { throw new InvalidDataException("Theft outcome archive count."); } for (int i = 0; i < num; i++) { string text = binaryReader.ReadString(); string text2 = binaryReader.ReadString(); bool item = binaryReader.ReadBoolean(); if (!TheftWire.IsRoot(text) || !TheftWire.IsFingerprint(text2) || theftOutcomeArchive.Contains(text)) { throw new InvalidDataException("Theft outcome archive row."); } theftOutcomeArchive.entries.Add((text, text2, item)); } if (binaryReader.BaseStream.Position != bytes.Length) { throw new InvalidDataException("Theft outcome archive trailing data."); } return theftOutcomeArchive; } catch (EndOfStreamException) { throw new InvalidDataException("Theft outcome archive truncated."); } } } public static class TheftOwnerStatus { public static TheftStatusAnswer Answer(TheftOutcomeArchive archive, Func<string, string, bool> pouchHolds, string root, string fingerprint) { if (!TheftWire.IsRoot(root) || !TheftWire.IsFingerprint(fingerprint)) { return TheftStatusAnswer.Unknown; } if (archive.Contains(root)) { return archive.Find(root, fingerprint); } if (!pouchHolds(root, fingerprint)) { return TheftStatusAnswer.Unknown; } return TheftStatusAnswer.Approved; } public static TheftStatusAnswer Decide(TheftOutcomeArchive archive, Func<string, string, bool> pouchHolds, string root, string fingerprint, out bool recorded) { recorded = false; TheftStatusAnswer theftStatusAnswer = Answer(archive, pouchHolds, root, fingerprint); if (theftStatusAnswer != TheftStatusAnswer.Unknown || !TheftWire.IsRoot(root) || !TheftWire.IsFingerprint(fingerprint) || archive.Contains(root)) { return theftStatusAnswer; } if (!archive.Record(root, fingerprint, approved: false)) { return TheftStatusAnswer.Unknown; } recorded = true; return TheftStatusAnswer.Denied; } } public static class TheftWorldArchive { public const string Prefix = "wildblood_theft_"; public const int Capacity = 256; public static string Key(string root) { return "wildblood_theft_" + root; } public static string Entry(string root, string fingerprint, bool approved) { if (!TheftWire.IsRoot(root) || !TheftWire.IsFingerprint(fingerprint)) { throw new InvalidDataException("Theft world archive row."); } return Key(root) + " " + Value(fingerprint, approved); } public static string Value(string fingerprint, bool approved) { return fingerprint + (approved ? "1" : "0"); } public static TheftStatusAnswer Read(string? value, string fingerprint) { if (value == null || value.Length != 65 || !TheftWire.IsFingerprint(fingerprint) || !value.StartsWith(fingerprint, StringComparison.Ordinal)) { return TheftStatusAnswer.Unknown; } if (value[64] != '1') { if (value[64] != '0') { return TheftStatusAnswer.Unknown; } return TheftStatusAnswer.Denied; } return TheftStatusAnswer.Approved; } public static IReadOnlyList<string> Rows(TheftOutcomeArchive archive) { return archive.Entries.Select<(string, string, bool), string>(((string Root, string Fingerprint, bool Approved) e) => Entry(e.Root, e.Fingerprint, e.Approved)).ToArray(); } public static bool Admit(int existing, int adding, bool lastChance) { if (!(adding == 0 || lastChance)) { return existing + adding <= 256; } return true; } } public enum TheftRecoveryStep { Retain, Record, Settled } public static class TheftRecovery { public static bool Eligible(TheftRequest request, bool v2) { if (v2 && request != null && TheftWire.IsRoot(request.Root)) { return TheftWire.IsFingerprint(request.Fingerprint); } return false; } public static TheftRecoveryStep Step(TheftStatusAnswer answer, bool outcomeKnown) { if (!outcomeKnown) { if (answer != TheftStatusAnswer.Unknown) { return TheftRecoveryStep.Record; } return TheftRecoveryStep.Retain; } return TheftRecoveryStep.Settled; } public static TheftReceipt? Receipt(TheftRequest request, TheftStatusAnswer answer) { if (answer != TheftStatusAnswer.Unknown) { return TheftReceipt.For(request, answer == TheftStatusAnswer.Approved); } return null; } } public sealed class TheftIntent { public readonly InventoryDecisionRecord Record; private readonly byte[] request; public TheftRequest Request => TheftMetadata.Decode(request); public string Root => Record.Id; public int WireVersion { get; } internal TheftIntent(InventoryDecisionRecord record) { Record = record; using (MemoryStream memoryStream = new MemoryStream(record.Metadata, writable: false)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8); WireVersion = binaryReader.ReadInt32(); if (WireVersion != 1 && WireVersion != 2) { throw new InvalidDataException("Requester intent version."); } request = binaryReader.ReadBytes(checked((int)(memoryStream.Length - memoryStream.Position))); } if (Request.Root != record.Id || TheftMetadata.Hash(request) != record.Fingerprint) { throw new InvalidDataException("Requester intent identity."); } } } public sealed class TheftClientLedger : IDisposable { private readonly InventoryDecisionJournal intents; private readonly InventoryDecisionJournal outcomes; private readonly InventoryDecisionJournal debits; private readonly InventoryDecisionJournal settlements; public IReadOnlyList<TheftIntent> All => (from r in intents.Snapshot() select new TheftIntent(r)).ToArray(); public IReadOnlyList<TheftIntent> Incomplete => All.Where((TheftIntent i) => i.Record.State != InventoryDecision.Committed).ToArray(); public bool CanAdmit { get { if (intents.CanAdmit && outcomes.CanAdmit && debits.CanAdmit && settlements.CanAdmit) { return Incomplete.Count == 0; } return false; } } public TheftClientLedger(string directory) { try { intents = new InventoryDecisionJournal(Path.Combine(directory, "intent.bin")); outcomes = new InventoryDecisionJournal(Path.Combine(directory, "outcome.bin")); debits = new InventoryDecisionJournal(Path.Combine(directory, "debit.bin")); settlements = new InventoryDecisionJournal(Path.Combine(directory, "settlement.bin")); InventoryDecisionJournal[] array = new InventoryDecisionJournal[2] { debits, settlements }; foreach (InventoryDecisionJournal inventoryDecisionJournal in array) { foreach (InventoryDecisionRecord item in inventoryDecisionJournal.Snapshot()) { if (item.State == InventoryDecision.Prepared) { inventoryDecisionJournal.Transition(item, InventoryDecision.Decided); } } } foreach (TheftIntent item2 in All) { if (item2.Record.State == InventoryDecision.Aborted) { throw new InvalidDataException("Requester intent was aborted."); } if (item2.Record.State == InventoryDecision.Committed && Settlement(item2) == null) { throw new InvalidDataException("Requester settlement missing."); } } } catch { Dispose(); throw; } } public TheftIntent Admit(TheftRequest request, byte[] before, byte[] escrow, int wireVersion = 2) { if (wireVersion != 1 && wireVersion != 2) { throw new ArgumentOutOfRangeException("wireVersion"); } if (!CanAdmit) { throw new InvalidOperationException("Requester already holds inventory or ledger is full."); } byte[] array = TheftMetadata.Encode(request); TheftMetadata.Decode(array); byte[] array2; using (MemoryStream memoryStream = new MemoryStream()) { using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(wireVersion); binaryWriter.Write(array); } array2 = memoryStream.ToArray(); } if (array2.Length > 2048) { throw new InvalidDataException("Requester intent exceeds bound."); } return new TheftIntent(intents.Prepare(request.Root, request.Root, TheftMetadata.Hash(array), 0.0, before, escrow, array2)); } public bool HasOutcome(TheftIntent intent) { return outcomes.Find(intent.Root) != null; } public TheftReceipt Outcome(TheftIntent intent) { TheftReceipt theftReceipt = TheftMetadata.Receipt((outcomes.Find(intent.Root) ?? throw new InvalidOperationException("No authoritative outcome.")).Metadata); if (!theftReceipt.Matches(intent.Request) || theftReceipt.Disposition == TheftDisposition.Recovery) { throw new InvalidDataException("Unsettleable outcome."); } return theftReceipt; } public bool RecordOutcome(TheftIntent intent, TheftReceipt receipt) { if (receipt == null || !receipt.Matches(intent.Request) || receipt.Disposition == TheftDisposition.Recovery) { return false; } byte[] array = TheftMetadata.Encode(receipt); TheftMetadata.Receipt(array); InventoryDecisionRecord inventoryDecisionRecord = outcomes.Find(intent.Root); if (inventoryDecisionRecord != null) { return ((ReadOnlySpan<byte>)inventoryDecisionRecord.Metadata).SequenceEqual((ReadOnlySpan<byte>)array); } if (intent.Record.State == InventoryDecision.Committed) { return false; } InventoryDecisionRecord record = outcomes.Prepare(intent.Root, intent.Root, TheftMetadata.Hash(array), 0.0, Array.Empty<byte>(), Array.Empty<byte>(), array); outcomes.Transition(record, InventoryDecision.Decided); return true; } public InventoryDecisionRecord? Settlement(TheftIntent intent) { return settlements.Find(intent.Root); } public void PrepareSettlement(TheftIntent intent, byte[] before, byte[] after) { TheftReceipt receipt = Outcome(intent); if (Settlement(intent) != null) { throw new InvalidOperationException("Settlement image is already immutable."); } InventoryDecisionRecord record = settlements.Prepare(intent.Root, "wildblood.theft.settle." + intent.Root, TheftMetadata.Hash(TheftMetadata.Encode(receipt)), 0.0, before, after); settlements.Transition(record, InventoryDecision.Decided); } public InventoryDecision CommitDebit(TheftIntent intent, Func<InventoryDecisionJournal, IInventoryImageStore> storeFactory) { InventoryDecisionRecord inventoryDecisionRecord = debits.Find(intent.Root); if (inventoryDecisionRecord == null) { inventoryDecisionRecord = debits.Prepare(intent.Root, "wildblood.theft.debit." + intent.Root, intent.Record.Fingerprint, 0.0, intent.Record.Before, intent.Record.After); debits.Transition(inventoryDecisionRecord, InventoryDecision.Decided); } return Commit(debits, inventoryDecisionRecord, storeFactory); } public InventoryDecision CommitSettlement(TheftIntent intent, Func<InventoryDecisionJournal, IInventoryImageStore> storeFactory) { Outcome(intent); InventoryDecisionRecord inventoryDecisionRecord = Settlement(intent); if (inventoryDecisionRecord == null) { return InventoryDecision.RecoveryRequired; } InventoryDecision num = Commit(settlements, inventoryDecisionRecord, storeFactory); if (num == InventoryDecision.Committed) { InventoryDecisionRecord inventoryDecisionRecord2 = outcomes.Find(intent.Root) ?? throw new InvalidDataException("Requester outcome missing."); if (inventoryDecisionRecord2.State == InventoryDecision.Prepared) { outcomes.Transition(inventoryDecisionRecord2, InventoryDecision.Decided); } outcomes.Transition(inventoryDecisionRecord2, InventoryDecision.Committed); if (intent.Record.State == InventoryDecision.Prepared) { intents.Transition(intent.Record, InventoryDecision.Decided); } intents.Transition(intent.Record, InventoryDecision.Committed); } return num; } public InventoryDecision Reconcile(TheftIntent intent, Func<InventoryDecisionJournal, IInventoryImageStore> storeFactory) { InventoryDecisionRecord inventoryDecisionRecord = Settlement(intent); if (inventoryDecisionRecord != null && storeFactory(settlements) is IInventoryCheckpointStore inventoryCheckpointStore && inventoryCheckpointStore.ReceiptConfirmed(inventoryDecisionRecord.Source)) { return CommitSettlement(intent, storeFactory); } InventoryDecision inventoryDecision = CommitDebit(intent, storeFactory); if (inventoryDecision == InventoryDecision.Committed) { if (inventoryDecisionRecord != null) { return CommitSettlement(intent, storeFactory); } return InventoryDecision.Prepared; } return inventoryDecision; } public bool FinalReceiptConfirmed(TheftIntent intent, Func<InventoryDecisionJournal, IInventoryImageStore> storeFactory) { InventoryDecisionRecord inventoryDecisionRecord = Settlement(intent); if (inventoryDecisionRecord != null && inventoryDecisionRecord.State == InventoryDecision.Committed && storeFactory(settlements) is IInventoryCheckpointStore inventoryCheckpointStore) { return inventoryCheckpointStore.ReceiptConfirmed(inventoryDecisionRecord.Source); } return false; } private static InventoryDecision Commit(InventoryDecisionJournal journal, InventoryDecisionRecord record, Func<InventoryDecisionJournal, IInventoryImageStore> storeFactory) { if (record.State == InventoryDecision.Prepared) { journal.Transition(record, InventoryDecision.Decided); } IInventoryImageStore inventoryImageStore = storeFactory(journal); InventoryDecision inventoryDecision = new DurableInventoryParticipant(journal, inventoryImageStore, () => 0.0).Commit(record.Id); if (inventoryDecision != InventoryDecision.Committed || !(inventoryImageStore is IInventoryCheckpointStore inventoryCheckpointStore) || inventoryCheckpointStore.ReceiptConfirmed(record.Source)) { return inventoryDecision; } return InventoryDecision.RecoveryRequired; } public void Dispose() { settlements?.Dispose(); debits?.Dispose(); outcomes?.Dispose(); intents?.Dispose(); } } public enum HeldWrite { Run, Refuse, Defer, Neutral, Skipped } public static class TheftHoldRules { public static readonly string[] Guarded = new string[7] { "AddItem", "RemoveItem", "RemoveAll", "RemoveOneItem", "RemoveItemByName", "MoveItemToThis", "MoveAll" }; public const int MaxDeferred = 256; public static HeldWrite Classify(string method, bool returnsVoid) { return Classify(method, returnsVoid, insideLoad: false); } public static HeldWrite Classify(string method, bool returnsVoid, bool insideLoad) { if (insideLoad || Array.IndexOf(Guarded, method) < 0) { return HeldWrite.Run; } if (!returnsVoid) { return HeldWrite.Refuse; } if (!(method == "MoveAll") && !(method == "MoveItemToThis")) { return HeldWrite.Defer; } return HeldWrite.Neutral; } public static HeldWrite Decide(string method, bool returnsVoid, bool held, bool runOriginal) { return Decide(method, returnsVoid, held, runOriginal, insideLoad: false); } public static HeldWrite Decide(string method, bool returnsVoid, bool held, bool runOriginal, bool insideLoad) { if (!runOriginal) { return HeldWrite.Skipped; } if (!held) { return HeldWrite.Run; } return Classify(method, returnsVoid, insideLoad); } public static bool HoldOnOpen(IEnumerable<bool> settledAndConfirmed) { return settledAndConfirmed.Any((bool settled) => !settled); } } public sealed class DeferredWrites<TTarget, TCall> where TTarget : class { private sealed class ReferenceComparer : IEqualityComparer<TTarget> { internal static readonly ReferenceComparer Instance = new ReferenceComparer(); public bool Equals(TTarget? x, TTarget? y) { return x == y; } public int GetHashCode(TTarget obj) { return RuntimeHelpers.GetHashCode(obj); } } private readonly List<(TTarget Target, TCall Call)> entries = new List<(TTarget, TCall)>(); private readonly int limit; public int Count => entries.Count; public DeferredWrites(int limit = 256) { this.limit = limit; } public bool TryAdd(TTarget target, TCall call) { if (entries.Count >= limit) { return false; } entries.Add((target, call)); return true; } public int Drain(Func<TTarget, bool> held, Action<TTarget, TCall> apply) { if (entries.Count == 0) { return 0; } int num = 0; HashSet<TTarget> hashSet = new HashSet<TTarget>(ReferenceComparer.Instance); int num2 = 0; while (num2 < entries.Count) { var (val, arg) = entries[num2]; if (hashSet.Contains(val) || held(val)) { hashSet.Add(val); num2++; } else { entries.RemoveAt(num2); num++; apply(val, arg); } } return num; } } public sealed class TheftCleanupResult { public static readonly TheftCleanupResult None = new TheftCleanupResult(Array.Empty<TheftRequest>(), Array.Empty<string>(), 0, 0, null); public IReadOnlyList<TheftRequest> Pruned { get; } public IReadOnlyList<string> ReceiptRoots { get; } public int Rows { get; } public int Kept { get; } public string? Skipped { get; } public string? Error { get; } public TheftCleanupResult(IReadOnlyList<TheftRequest> pruned, IReadOnlyList<string> receiptRoots, int rows, int kept, string? skipped, string? error = null) { Pruned = pruned; ReceiptRoots = receiptRoots; Rows = rows; Kept = kept; Skipped = skipped; Error = error; } } public static class TheftJournalCleanup { public const int KeepRecent = 32; public const int Trigger = 64; public const string Manifest = "cleanup.pending"; private const string Copy = ".cleanup-new"; private const string Backup = ".cleanup-old"; private static readonly string[] Journals = new string[4] { "intent.bin", "outcome.bin", "debit.bin", "settlement.bin" }; public static string DebitSource(string root) { return "wildblood.theft.debit." + root; } public static string SettleSource(string root) { return "wildblood.theft.settle." + root; } public static IEnumerable<string> Sources(string root) { yield return DebitSource(root); yield return SettleSource(root); } public static void Recover(string directory) { if (!Directory.Exists(directory)) { return; } string[] journals = Journals; foreach (string path in journals) { string text = Path.Combine(directory, path); string text2 = text + ".cleanup-old"; string path2 = text + ".cleanup-new"; if (File.Exists(text2)) { if (File.Exists(text)) { File.Delete(text2); } else { File.Move(text2, text); } } if (File.Exists(path2)) { File.Delete(path2); } } string path3 = Path.Combine(directory, "cleanup.pending") + ".cleanup-new"; if (File.Exists(path3)) { File.Delete(path3); } } public static TheftCleanupResult Run(string directory, Func<string, bool> receiptSaved) { Recover(directory); try { return Prune(directory, receiptSaved, 32, 64, null); } catch (Exception ex) { Recover(directory); return new TheftCleanupResult(Array.Empty<TheftRequest>(), Array.Empty<string>(), 0, 0, null, ex.GetType().Name); } } public static TheftCleanupResult Prune(string directory, Func<string, bool> receiptSaved, int keep, int trigger, Action<string>? step) { if (keep < 1 || trigger < keep) { throw new ArgumentOutOfRangeException("trigger"); } if (!File.Exists(Path.Combine(directory, Journals[0]))) { return TheftCleanupResult.None; } Dictionary<string, IReadOnlyList<InventoryDecisionRecord>> dictionary = Journals.ToDictionary((string n) => n, (string n) => Read(Path.Combine(directory, n))); List<TheftIntent> list = dictionary[Journals[0]].Select((InventoryDecisionRecord r) => new TheftIntent(r)).ToList(); HashSet<string> present = new HashSet<string>(dictionary.Values.SelectMany((IReadOnlyList<InventoryDecisionRecord> l) => l.Select((InventoryDecisionRecord r) => r.Id)), StringComparer.Ordinal); List<string> pending = (from root in ReadManifest(directory) where present.Contains(root) || Sources(root).Any(receiptSaved) select root).ToList(); string text = null; if (dictionary.Values.SelectMany((IReadOnlyList<InventoryDecisionRecord> l) => l).Any((InventoryDecisionRecord r) => r.State != InventoryDecision.Committed)) { text = "unsettled"; } else { List<IReadOnlyList<InventoryDecisionRecord>> source = (from p in dictionary where p.Key != Journals[0] select p.Value).ToList(); foreach (TheftIntent intent in list) { if (source.Any((IReadOnlyList<InventoryDecisionRecord> l) => l.All((InventoryDecisionRecord r) => r.Id != intent.Root)) || !Sources(intent.Root).All(receiptSaved)) { text = "unconfirmed"; break; } } } List<TheftIntent> list2 = new List<TheftIntent>(); HashSet<string> remove = new HashSet<string>(StringComparer.Ordinal); if (text == null) { IEnumerable<TheftIntent> first; if (list.Count <= trigger) { IEnumerable<TheftIntent> enumerable = Enumerable.Empty<TheftIntent>(); first = enumerable; } else { first = list.OrderByDescending((TheftIntent i) => i.Request.Counter).Skip(keep); } list2 = first.Concat(list.Where((TheftIntent i) => pending.Contains(i.Root))).Distinct().ToList(); remove.UnionWith(list2.Select((TheftIntent i) => i.Root)); HashSet<string> known = new HashSet<string>(list.Select((TheftIntent i) => i.Root), StringComparer.Ordinal); remove.UnionWith(present.Where((string id) => !known.Contains(id))); } List<string> list3 = pending.Concat(remove).Distinct<string>(StringComparer.Ordinal).ToList(); List<string> receiptRoots = list3.Where((string r) => !present.Contains(r) || remove.Contains(r)).ToList(); WriteManifest(directory, list3); if (remove.Count == 0) { if (list3.Count != 0 || text != null) { return new TheftCleanupResult(Array.Empty<TheftRequest>(), receiptRoots, 0, list.Count, text); } return TheftCleanupResult.None; } step?.Invoke("manifest"); int num = 0; string[] journals = Journals; foreach (string text2 in journals) { num += Compact(directory, text2, dictionary[text2], remove, step); } return new TheftCleanupResult(list2.Select((TheftIntent i) => i.Request).ToList(), receiptRoots, num, list.Count - list2.Count, null); } private static IReadOnlyList<InventoryDecisionRecord> Read(string path) { if (!File.Exists(path)) { return Array.Empty<InventoryDecisionRecord>(); } using InventoryDecisionJournal inventoryDecisionJournal = new InventoryDecisionJournal(path, requireExisting: true); return inventoryDecisionJournal.Snapshot(); } private static int Compact(string directory, string name, IReadOnlyList<InventoryDecisionRecord> all, HashSet<string> remove, Action<string>? step) { List<InventoryDecisionRecord> list = all.Where((InventoryDecisionRecord r) => !remove.Contains(r.Id)).ToList(); if (list.Count == all.Count) { return 0; } string text = Path.Combine(directory, name); string text2 = text + ".cleanup-new"; string text3 = text + ".cleanup-old"; if (File.Exists(text2)) { File.Delete(text2); } using (InventoryDecisionJournal inventoryDecisionJournal = new InventoryDecisionJournal(text2)) { foreach (InventoryDecisionRecord item in list) { InventoryDecisionRecord record = inventoryDecisionJournal.Prepare(item.Id, item.Source, item.Fingerprint, item.Deadline, item.Before, item.After, item.Metadata); foreach (InventoryDecision item2 in Steps(item.State)) { inventoryDecisionJournal.Transition(record, item2); } } } InventoryDecisionJournal check = new InventoryDecisionJournal(text2, requireExisting: true); try { if (check.Snapshot().Count != list.Count || list.Any((InventoryDecisionRecord r) => !Same(r, check.Find(r.Id)))) { throw new InvalidDataException("Theft journal copy differs from the kept records."); } } finally { if (check != null) { ((IDisposable)check).Dispose(); } } step?.Invoke(name + ".copied"); File.Replace(text2, text, text3); step?.Invoke(name + ".swapped"); File.Delete(text3); return all.Count - list.Count; } private static IEnumerable<InventoryDecision> Steps(InventoryDecision state) { switch (state) { case InventoryDecision.Decided: yield return InventoryDecision.Decided; break; case InventoryDecision.Committed: yield return InventoryDecision.Decided; yield return InventoryDecision.Committed; break; case InventoryDecision.Aborted: yield return InventoryDecision.Aborted; break; } } private static bool Same(InventoryDecisionRecord a, InventoryDecisionRecord? b) { if (b != null && a.Id == b.Id && a.Source == b.Source && a.Fingerprint == b.Fingerprint && a.Deadline.Equals(b.Deadline) && a.State == b.State && ((ReadOnlySpan<byte>)a.Before).SequenceEqual((ReadOnlySpan<byte>)b.Before) && ((ReadOnlySpan<byte>)a.After).SequenceEqual((ReadOnlySpan<byte>)b.After)) { return ((ReadOnlySpan<byte>)a.Metadata).SequenceEqual((ReadOnlySpan<byte>)b.Metadata); } return false; } private static List<string> ReadManifest(string directory) { string path = Path.Combine(directory, "cleanup.pending"); if (!File.Exists(path)) { return new List<string>(); } try { List<string> source = (from l in File.ReadAllLines(path) where l.Length != 0 select l).ToList(); return source.All(TheftWire.IsRoot) ? source.Distinct<string>(StringComparer.Ordinal).ToList() : new List<string>(); } catch (IOException) { return new List<string>(); } } private static void WriteManifest(string directory, IReadOnlyCollection<string> roots) { string text = Path.Combine(directory, "cleanup.pending"); if (roots.Count == 0) { if (File.Exists(text)) { File.Delete(text); } return; } string text2 = text + ".cleanup-new"; using (FileStream fileStream = new FileStream(text2, FileMode.Create, FileAccess.Write, FileShare.None)) { using StreamWriter streamWriter = new StreamWriter(fileStream); foreach (string root in roots) { streamWriter.WriteLine(root); } streamWriter.Flush(); fileStream.Flush(flushToDisk: true); } if (File.Exists(text)) { File.Replace(text2, text, null); } else { File.Move(text2, text); } } } public sealed class TheftRequest { public string Root { get; } public long World { get; } public long Actor { get; } public long SourceUser { get; } public uint SourceId { get; } public long Owner { get; } public int Counter { get; } public int X { get; } public int Y { get; } public string Session { get; } public string Fingerprint { get; } public TheftRequest(string root, long world, long actor, long sourceUser, uint sourceId, long owner, int counter, int x, int y, string session, string fingerprint) { Root = root; World = world; Actor = actor; SourceUser = sourceUser; SourceId = sourceId; Owner = owner; Counter = counter; X = x; Y = y; Session = session; Fingerprint = fingerprint; } } public enum TheftDisposition { Approved, Denied, Recovery } public sealed class TheftReceipt { public string Root { get; } public string Fingerprint { get; } public TheftDisposition Disposition { get; } public TheftReceipt(string root, string fingerprint, TheftDisposition disposition) { Root = root; Fingerprint = fingerprint; Disposition = disposition; } public bool Matches(TheftRequest request) { if (request != null && Root == request.Root) { return Fingerprint == TheftMetadata.Hash(TheftMetadata.Encode(request)); } return false; } public static TheftReceipt For(TheftRequest request, bool banked) { return new TheftReceipt(request.Root, TheftMetadata.Hash(TheftMetadata.Encode(request)), (!banked) ? TheftDisposition.Denied : TheftDisposition.Approved); } } public static class TheftMetadata { public const int Maximum = 2048; public static string Hash(byte[] value) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(value)).Replace("-", "").ToLowerInvariant(); } private static bool Hex(string value, int length) { if (value != null && value.Length == length) { return value.All((char c) => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); } return false; } public static byte[] Encode(TheftRequest request) { if (request == null || !Hex(request.Root, 32) || request.World == 0L || request.Actor == 0L || request.Owner == 0L || request.Counter <= 0 || request.SourceUser == 0L || request.SourceId == 0 || request.X < 0 || request.Y < 0 || request.X >= 256 || request.Y >= 256 || string.IsNullOrEmpty(request.Session) || request.Session.Length > 128 || request.Session.Any(char.IsControl) || !Hex(request.Fingerprint, 64)) { throw new InvalidDataException("Theft correlation invalid."); } using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(1); binaryWriter.Write(request.Root); binaryWriter.Write(request.World); binaryWriter.Write(request.Actor); binaryWriter.Write(request.SourceUser); binaryWriter.Write(request.SourceId); binaryWriter.Write(request.Owner); binaryWriter.Write(request.Counter); binaryWriter.Write(request.X); binaryWriter.Write(request.Y); binaryWriter.Write(request.Session); binaryWriter.Write(request.Fingerprint); } return memoryStream.ToArray(); } public static TheftRequest Decode(byte[] value) { if (value == null || value.Length > 2048) { throw new InvalidDataException("Theft correlation bound."); } using MemoryStream memoryStream = new MemoryStream(value, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8); if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Theft correlation version."); } TheftRequest theftRequest = new TheftRequest(binaryReader.ReadString(), binaryReader.ReadInt64(), binaryReader.ReadInt64(), binaryReader.ReadInt64(), binaryReader.ReadUInt32(), binaryReader.ReadInt64(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadString(), binaryReader.ReadString()); if (memoryStream.Position != memoryStream.Length || !((ReadOnlySpan<byte>)Encode(theftRequest)).SequenceEqual((ReadOnlySpan<byte>)value)) { throw new InvalidDataException("Theft correlation trailing data."); } return theftRequest; } public static byte[] Encode(TheftReceipt receipt) { if (receipt == null || !Hex(receipt.Root, 32) || !Hex(receipt.Fingerprint, 64) || !Enum.IsDefined(typeof(TheftDisposition), receipt.Disposition)) { throw new InvalidDataException("Theft outcome invalid."); } using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(1); binaryWriter.Write(receipt.Root); binaryWriter.Write(receipt.Fingerprint); binaryWriter.Write((int)receipt.Disposition); } return memoryStream.ToArray(); } public static TheftReceipt Receipt(byte[] value) { if (value == null || value.Length > 2048) { throw new InvalidDataException("Theft outcome bound."); } using MemoryStream memoryStream = new MemoryStream(value, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8); if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("Theft outcome version."); } TheftReceipt theftReceipt = new TheftReceipt(binaryReader.ReadString(), binaryReader.ReadString(), (TheftDisposition)binaryReader.ReadInt32()); if (memoryStream.Position != memoryStream.Length || !((ReadOnlySpan<byte>)Encode(theftReceipt)).SequenceEqual((ReadOnlySpan<byte>)value)) { throw new InvalidDataException("Theft outcome trailing data."); } return theftReceipt; } } } namespace ExtraPotions.Wildblood.Traits { public static class LegacyMasks { private static readonly (Modifier? Modifier, Mutation? Mutation)[] CreatureManagerBits = new(Modifier?, Mutation?)[32] { (Modifier.Armored, null), (Modifier.Enraged, null), (Modifier.Deathward, null), (Modifier.Swift, null), (Modifier.Regenerating, null), (Modifier.Vampiric, null), (Modifier.Fire, null), (Modifier.Frost, null), (Modifier.Lightning, null), (Modifier.ToxicDeath, null), (Modifier.ArmorPiercing, null), (Modifier.Staggering, null), (Modifier.AttackSpeed, null), (Modifier.Exposed, null), (Modifier.Weakened, null), (Modifier.Withered, null), (Modifier.Reflection, null), (Modifier.Vortex, null), (Modifier.Crippling, null), (Modifier.Disruptive, null), (Modifier.Adaptive, null), (Modifier.Omen, null), (Modifier.Reaping, null), (Modifier.Spirit, null), (null, Mutation.Blinking), (Modifier.Crushing, null), (Modifier.Unflinching, null), (Modifier.AdrenalineDrain, null), (null, Mutation.Corrodent), (Modifier.Undodgeable, null), (Modifier.Chameleon, null), (Modifier.Blamer, null) }; public static long ToCreatureManagerModifiers(IEnumerable<Modifier> modifiers) { HashSet<Modifier> hashSet = new HashSet<Modifier>(modifiers); long num = 0L; for (int i = 0; i < CreatureManagerBits.Length; i++) { Modifier? item = CreatureManagerBits[i].Modifier; if (item.HasValue) { Modifier valueOrDefault = item.GetValueOrDefault(); if (hashSet.Contains(valueOrDefault)) { num |= 1L << i; } } } return num; } public static TraitSet FromCreatureManager(long mask, Biome biome = Biome.Other) { List<Modifier> list = new List<Modifier>(); int num = 0; for (int i = 0; i < CreatureManagerBits.Length; i++) { if ((mask & (1L << i)) != 0L) { var (modifier, mutation) = CreatureManagerBits[i]; if (modifier.HasValue) { list.Add(modifier.Value); } if (mutation.HasValue) { num |= Mutations.Bit(mutation.Value); } } } return new TraitSet(num, list, biome); } } public enum ModifierGroup { Offense, Defense, Affliction, Special } public enum Modifier { Enraged, Fire, Frost, Lightning, Spirit, ArmorPiercing, Staggering, Undodgeable, Armored, Deathward, Regenerating, Reflection, Vortex, Adaptive, Unflinching, Chameleon, Exposed, Weakened, Withered, Crippling, Disruptive, AdrenalineDrain, ToxicDeath, Swift, AttackSpeed, Vampiric, Reaping, Omen, Crushing, Blamer } public enum ModifierCap { Max4, Max3, Max2, Max1, Off } public static class Modifiers { public static IReadOnlyList<Modifier> InOrder { get; } = Array.AsReadOnly((Modifier[])Enum.GetValues(typeof(Modifier))); public static ModifierGroup GroupOf(Modifier modifier) { if (modifier <= Modifier.Undodgeable) { return ModifierGroup.Offense; } if (modifier <= Modifier.Chameleon) { return ModifierGroup.Defense; } if (modifier <= Modifier.ToxicDeath) { return ModifierGroup.Affliction; } return ModifierGroup.Special; } public static IReadOnlyList<Modifier> InGroup(ModifierGroup group) { return InOrder.Where((Modifier m) => GroupOf(m) == group).ToList(); } public static int Count(ModifierCap cap) { return cap switch { ModifierCap.Max4 => 4, ModifierCap.Max3 => 3, ModifierCap.Max2 => 2, ModifierCap.Max1 => 1, _ => 0, }; } public static string Id(Modifier modifier) { return modifier.ToString().ToLowerInvariant(); } public static bool TryParseId(string? id, out Modifier modifier) { foreach (Modifier item in InOrder) { if (string.Equals(Id(item), (id ?? "").Trim(), StringComparison.OrdinalIgnoreCase)) { modifier = item; return true; } } modifier = Modifier.Enraged; return false; } public static Modifier? FromCreatureManager(string? name, out Mutation? merged) { merged = null; string text = (name ?? "").Replace(" ", "").Trim(); if (text.Equals("Blink", StringComparison.OrdinalIgnoreCase)) { merged = Mutation.Blinking; return null; } if (text.Equals("Corrosive", StringComparison.OrdinalIgnoreCase)) { merged = Mutation.Corrodent; return null; } if (text.Equals("Juggernaut", StringComparison.OrdinalIgnoreCase)) { return Modifier.Crushing; } foreach (Modifier item in InOrder) { if (item.ToString().Equals(text, StringComparison.OrdinalIgnoreCase)) { return item; } } return null; } } public enum Mutation { Mad, Bloated, Cloaked, Splintering, Leeching, Warding, Plated, Miasmic, Devouring, Thieving, Gilded, Blinking, Relentless, Juggernaut, Screecher, Frostbound, Mudbound, Corrodent, Cloning, Flamebound, Piercing, Howling, Binding } public static class Mutations { public static IReadOnlyList<Mutation> InOrder { get; } = Array.AsReadOnly((Mutation[])Enum.GetValues(typeof(Mutation))); public static int AllBits => (1 << InOrder.Count) - 1; public static int Bit(Mutation mutation) { return 1 << (int)mutation; } public static int Mask(IEnumerable<Mutation> mutations) { return mutations.Aggregate(0, (int mask, Mutation m) => mask | Bit(m)); } public static IReadOnlyList<Mutation> Of(int mask) { return InOrder.Where((Mutation m) => (mask & Bit(m)) != 0).ToList(); } public static string Id(Mutation mutation) { return mutation.ToString().ToLowerInvariant(); } public static bool TryParseId(string? id, out Mutation mutation) { foreach (Mutation item in InOrder) { if (string.Equals(Id(item), (id ?? "").Trim(), StringComparison.OrdinalIgnoreCase)) { mutation = item; return true; } } mutation = Mutation.Mad; return false; } } public static class BodySize { public const float LargeRadius = 1f; public const float LargeLength = 3.5f; public const float MinHealth = 300f; private const float Slack = 0.01f; public static int LargeBars => Mutations.Bit(Mutation.Gilded) | Mutations.Bit(Mutation.Relentless); public static bool IsLarge(float radius, float length, float baseHealth) { if (baseHealth >= 300f) { if (!(radius >= 0.99f)) { return length >= 3.49f; } return true; } return false; } } public static class MutationBars { private static readonly Dictionary<string, int> KindBars = new Dictionary<string, int>(StringComparer.Ordinal) { ["Deathsquito"] = Mutations.Bit(Mutation.Cloaked), ["Hatchling"] = Mutations.Bit(Mutation.Cloaked), ["Bat"] = Mutations.Bit(Mutation.Mad) | Mutations.Bit(Mutation.Cloaked) }; public static int Of(string prefab, bool large) { return (large ? BodySize.LargeBars : 0) | OfKind(prefab); } public static int OfKind(string? prefab) { if (prefab == null || !KindBars.TryGetValue(prefab, out var value)) { return 0; } return value; } } public static class MutationPalette { private static readonly IReadOnlyDictionary<Mutation, string> Defaults = new Dictionary<Mutation, string> { [Mutation.Mad] = "#E23030", [Mutation.Bloated] = "#8B5A2B", [Mutation.Cloaked] = "#4AA6FF", [Mutation.Splintering] = "#FFFFFF", [Mutation.Leeching] = "#33CC33", [Mutation.Warding] = "#101C50", [Mutation.Plated] = "#F2C40C", [Mutation.Miasmic] = "#1E6B2E", [Mutation.Devouring] = "#7A0F0F", [Mutation.Thieving] = "#A64BE0", [Mutation.Gilded] = "#FFE066", [Mutation.Blinking] = "#29E0E0", [Mutation.Relentless] = "#FF7F24", [Mutation.Juggernaut] = "#6E6E6E", [Mutation.Screecher] = "#FF5CC8", [Mutation.Frostbound] = "#BFEFFF", [Mutation.Mudbound] = "#5E4A1E", [Mutation.Corrodent] = "#B7410E", [Mutation.Cloning] = "#B4A8FF", [Mutation.Flamebound] = "#FF4A1C", [Mutation.Piercing] = "#A8B8C8", [Mutation.Howling] = "#D4C08A", [Mutation.Binding] = "#6B8E23" }; public static string Default { get; } = Format(Defaults); public static string DefaultColour(Mutation mutation) { return Defaults[mutation]; } public static IReadOnlyDictionary<Mutation, string> Parse(string? text) { Dictionary<Mutation, string> dictionary = new Dictionary<Mutation, string>(Defaults); string[] array = (text ?? "").Split(','); foreach (string text2 in array) { int num = text2.IndexOf('='); if (num > 0) { string value = text2.Substring(0, num).Trim(); string text3 = text2.Substring(num + 1).Trim(); if (Enum.TryParse<Mutation>(value, ignoreCase: true, out var result) && Enum.IsDefined(typeof(Mutation), result) && TryColour(text3, out string canonical)) { dictionary[result] = canonical; } } } return dictionary; } public static string Normalize(string? text) { return Format(Parse(text)); } public static bool TryColour(string? text, out string canonical) { canonical = ""; string text2 = (text ?? "").Trim(); if (text2.Length != 7 || text2[0] != '#') { return false; } if (!int.TryParse(text2.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var _)) { return false; } canonical = text2.ToUpperInvariant(); return true; } private static string Format(IReadOnlyDictionary<Mutation, string> colours) { return string.Join(", ", Mutations.InOrder.Select((Mutation m) => m.ToString() + "=" + colours[m])); } } public static class SwiftMovement { public static float Bonus(bool swift, float power) { if (!swift || float.IsNaN(power)) { return 0f; } return Math.Clamp(power, 0f, 1f); } public static bool NeedsReapply(float applied, float current) { return Math.Abs(applied - current) > 0.0001f; } } public sealed class TraitSet : IEquatable<TraitSet> { public int Mutations { get; } public IReadOnlyList<Modifier> Modifiers { get; } public Biome Biome { get; } public bool IsPlain { get { if (Mutations == 0) { return Modifiers.Count == 0; } return false; } } public TraitSet(int mutations, IEnumerable<Modifier> modifiers, Biome biome) { Mutations = mutations & ExtraPotions.Wildblood.Traits.Mutations.AllBits; Modifiers = (from m in (modifiers ?? throw new ArgumentNullException("modifiers")).Distinct() orderby m select m).ToList(); Biome = biome; } public static TraitSet Plain(Biome biome) { return new TraitSet(0, Array.Empty<Modifier>(), biome); } public bool Equals(TraitSet? other) { if (other != null && Mutations == other.Mutations && Biome == other.Biome) { return Modifiers.SequenceEqual(other.Modifiers); } return false; } public override bool Equals(object? obj) { return Equals(obj as TraitSet); } public override int GetHashCode() { return (Mutations, Biome, Modifiers.Count).GetHashCode(); } public override string ToString() { return TraitBlob.Encode(this); } } public static class TraitBlob { public const string Version = "wb1"; public const int MaxLength = 256; public const string LegacyPrefix = "ecr:"; public static string Encode(TraitSet traits) { if (traits == null) { throw new ArgumentNullException("traits"); } if (traits.IsPlain) { return ""; } string text = "wb1;m=" + string.Join(",", Mutations.Of(traits.Mutations).Select(Mutations.Id)) + ";c=" + string.Join(",", traits.Modifiers.Select(Modifiers.Id)) + ";b=" + BiomeId(traits.Biome); if (text.Length > 256) { throw new ArgumentException("Trait blob longer than " + 256 + " characters.", "traits"); } return text; } public static bool TryDecode(string? blob, out TraitSet traits) { traits = TraitSet.Plain(Biome.Other); if (string.IsNullOrEmpty(blob)) { return true; } if (blob.Length > 256) { return false; } if (blob.StartsWith("ecr:", StringComparison.Ordinal)) { if (!int.TryParse(blob.Substring("ecr:".Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result < 0) { return false; } traits = new TraitSet(result, Array.Empty<Modifier>(), Biome.Other); return true; } string[] array = blob.Split(';'); if (array[0] != "wb1") { return false; } int num = 0; List<Modifier> list = new List<Modifier>(); Biome biome = Biome.Other; foreach (string item in array.Skip(1)) { int num2 = item.IndexOf('='); if (num2 < 0) { continue; } string text = item.Substring(0, num2); string[] array2 = item.Substring(num2 + 1).Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); switch (text) { case "m": { string[] array3 = array2; for (int i = 0; i < array3.Length; i++) { if (Mutations.TryParseId(array3[i], out var mutation)) { num |= Mutations.Bit(mutation); } } break; } case "c": { string[] array3 = array2; for (int i = 0; i < array3.Length; i++) { if (Modifiers.TryParseId(array3[i], out var modifier)) { list.Add(modifier); } } break; } case "b": biome = ParseBiome(array2.FirstOrDefault()); break; } } traits = new TraitSet(num, list, biome); return true; } public static string BiomeId(Biome biome) { return biome.ToString().ToLowerInvariant(); } public static Biome ParseBiome(string? id) { foreach (Biome value in Enum.GetValues(typeof(Biome))) { if (string.Equals(BiomeId(value), (id ?? "").Trim(), StringComparison.OrdinalIgnoreCase)) { return value; } } return Biome.Other; } public static string EggEncode(string blob) { StringBuilder stringBuilder = new StringBuilder(); byte[] bytes = Encoding.UTF8.GetBytes(blob ?? ""); for (int i = 0; i < bytes.Length; i++) { byte b = bytes[i]; char c = (char)b; if (b < 128 && (char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '-')) { stringBuilder.Append(c); } else { stringBuilder.Append('%').Append(b.ToString("X2", CultureInfo.InvariantCulture)); } } return stringBuilder.ToString(); } public static string EggDecode(string egg) { List<byte> list = new List<byte>(); string text = egg ?? ""; for (int i = 0; i < text.Length; i++) { if (text[i] != '%') { list.Add((byte)text[i]); continue; } if (i + 2 >= text.Length || !byte.TryParse(text.Substring(i + 1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { return ""; } list.Add(result); i += 2; } return Encoding.UTF8.GetString(list.ToArray()); } } public static class TraitInheritance { public static TraitSet Inherit(TraitSet mother, TraitSet? sire, float mutationChance, int enabled, int barred, Func<Modifier, bool> modifierAllowed, IDice dice, bool mutationsKept = true) { if (mother == null) { throw new ArgumentNullException("mother"); } if (modifierAllowed == null) { throw new ArgumentNullException("modifierAllowed"); } int mask = (mother.Mutations | (sire?.Mutations ?? 0)) & ~barred; List<Mutation> list = new List<Mutation>(); foreach (Mutation item in Mutations.Of(mask)) { if ((enabled & Mutations.Bit(item)) != 0) { list.Add(item); } } int mutations = 0; if (mutationsKept && list.Count > 0 && dice.Percent(mutationChance)) { mutations = Mutations.Bit(list[dice.Range(0, list.Count)]); } return new TraitSet(mutations, mother.Modifiers.Where(modifierAllowed), mother.Biome); } public static string Inherit(string mother, string sire, float mutationChance, int enabled, int barred, Func<Modifier, bool> modifierAllowed, IDice dice, bool mutationsKept = true) { TraitBlob.TryDecode(mother, out TraitSet traits); TraitBlob.TryDecode(sire, out TraitSet traits2); TraitSet traitSet = Inherit(traits, traits2, mutationChance, enabled, barred, modifierAllowed, dice, mutationsKept); if (traitSet.Mutations != 0 || traitSet.Modifiers.Count != 0) { return TraitBlob.Encode(traitSet); } return ""; } } public static class TraitOverlaps { public static IReadOnlyList<(Mutation Mutation, Modifier Modifier)> Pairs { get; } = new(Mutation, Modifier)[9] { (Mutation.Leeching, Modifier.Vampiric), (Mutation.Warding, Modifier.Reflection), (Mutation.Plated, Modifier.Armored), (Mutation.Mad, Modifier.Swift), (Mutation.Mad, Modifier.AttackSpeed), (Mutation.Juggernaut, Modifier.Unflinching), (Mutation.Flamebound, Modifier.Fire), (Mutation.Frostbound, Modifier.Frost), (Mutation.Juggernaut, Modifier.Crushing) }; public static bool Excludes(int mutations, Modifier modifier) { foreach (var pair in Pairs) { var (mutation, _) = pair; if (pair.Modifier == modifier && (mutations & Mutations.Bit(mutation)) != 0) { return true; } } return false; } public static IReadOnlyList<Modifier> Without(int mutations, IEnumerable<Modifier> modifiers) { return modifiers.Where((Modifier m) => !Excludes(mutations, m)).ToList(); } } public sealed class BiomeTraitRules { public float[] MutationChance { get; } public Dictionary<Mutation, float[]> MutationChances { get; } = new Dictionary<Mutation, float[]>(); public BiomeTraitRules(float[] mutationChance) { MutationChance = mutationChance ?? throw new ArgumentNullException("mutationChance"); } public static BiomeTraitRules Default() { BiomeTraitRules biomeTraitRules = new BiomeTraitRules(new float[6] { 2.5f, 3.5f, 5f, 6f, 7.5f, 10f }); biomeTraitRules.MutationChances[Mutation.Devouring] = new float[6] { 0.6f, 0.9f, 1.2f, 1.5f, 1.8f, 2.4f }; biomeTraitRules.MutationChances[Mutation.Gilded] = new float[6] { 0.2f, 0.25f, 0.35f, 0.45f, 0.55f, 0.7f }; return biomeTraitRules; } public float ChanceOf(Mutation mutation, int stars) { float[] value; float[] array = (MutationChances.TryGetValue(mutation, out value) ? value : MutationChance); if (array.Length == 0) { return 0f; } return array[Math.Min(array.Length - 1, Math.Max(0, stars))]; } } public static class TraitRoller { public static int RollMutationsCustom(BiomeTraitRules rules, int stars, int enabled, int barred, float boost, int maxMutations, IDice dice) { List<Mutation> list = new List<Mutation>(); foreach (Mutation item in Mutations.InOrder) { float chancePercent = Math.Min(100f, rules.ChanceOf(item, stars) * Math.Max(0f, boost)); if (MayRoll(item, enabled, barred) && dice.Percent(chancePercent)) { list.Add(item); } } if (maxMutations > 0 && list.Count > maxMutations) { for (int num = list.Count - 1; num > 0; num--) { int num2 = dice.Range(0, num + 1); List<Mutation> list2 = list; int index = num; int index2 = num2; Mutation value = list[num2]; Mutation value2 = list[num]; list2[index] = value; list[index2] = value2; } list.RemoveRange(maxMutations, list.Count - maxMutations); } return Mutations.Mask(list); } public static float PresetRate(float cellRate, int stars) { return Math.Min(100f, cellRate * (1f + 0.25f * (float)Math.Max(0, stars))); } public static int RollMutationPreset(float cellRate, int stars, BiomeTraitRules rules, int enabled, int barred, IDice dice) { if (!dice.Percent(PresetRate(cellRate, stars))) { return 0; } IReadOnlyList<Mutation> inOrder = Mutations.InOrder; float[] array = new float[inOrder.Count]; float num = 0f; for (int i = 0; i < inOrder.Count; i++) { array[i] = (MayRoll(inOrder[i], enabled, barred) ? Math.Max(0f, rules.ChanceOf(inOrder[i], stars)) : 0f); num += array[i]; } if (num <= 0f) { return 0; } return Mutations.Bit(inOrder[dice.PickWeighted(array)]); } public static bool MayRoll(Mutation mutation, int enabled, int barred) { if ((enabled & Mutations.Bit(mutation)) != 0) { return (barred & Mutations.Bit(mutation)) == 0; } return false; } public static float DefaultModifierChance(bool boss) { if (!boss) { return 5f; } return 10f; } public static IReadOnlyList<Modifier> RollModifiers(IReadOnlyDictionary<Modifier, float> chances, ModifierCap cap, IDice dice) { int num = Modifiers.Count(cap); if (num == 0) { return Array.Empty<Modifier>(); } List<Modifier> list = new List<Modifier>(); foreach (ModifierGroup value2 in Enum.GetValues(typeof(ModifierGroup))) { foreach (Modifier item in Modifiers.InGroup(value2)) { if (chances.TryGetValue(item, out var value) && dice.Percent(value)) { list.Add(item); break; } } } while (list.Count > num) { list.RemoveAt(dice.Range(0, list.Count)); } return list.OrderBy((Modifier m) => m).ToList(); } } } namespace ExtraPotions.Wildblood.Stars { public enum Difficulty { Easy, Medium, Hard, VeryHard, Extreme, Custom } public enum StarMode { Wildblood, KeepExisting, Off } public static class DifficultyNames { public const string Choices = "Easy, Medium, Hard, Very Hard, Extreme or Custom"; public static bool TryParse(string? text, out Difficulty difficulty) { switch ((text ?? "").Replace(" ", "").Replace("_", "").Replace("-", "") .ToLowerInvariant()) { case "custom": difficulty = Difficulty.Custom; return true; case "easy": difficulty = Difficulty.Easy; return true; case "medium": difficulty = Difficulty.Medium; return true; case "normal": difficulty = Difficulty.Medium; return true; case "hard": difficulty = Difficulty.Hard; return true; case "veryhard": difficulty = Difficulty.VeryHard; return true; case "extreme": difficulty = Difficulty.Extreme; return true; default: difficulty = Difficulty.Medium; return false; } } public static string Name(Difficulty difficulty) { if (difficulty != Difficulty.VeryHard) { return difficulty.ToString(); } return "Very Hard"; } } public static class DistanceScaling { public const int Damage = 0; public const int Health = 1; public static readonly float[] DefaultBand = new float[4] { 0.03f, 0.08f, 1000f, 5f }; public const string DefaultBandText = "[0.03, 0.08, 1000, 5]"; private const float DefaultStepMeters = 1000f; public static float Multiplier(float distance, IReadOnlyList<float>? scaling, int valueIndex) { if (scaling == null || valueIndex < 0 || scaling.Count <= valueIndex) { return 1f; } float num = Math.Max(0f, scaling[valueIndex]); if (num <= 0f || float.IsNaN(num) || float.IsInfinity(num)) { return 1f; } if (float.IsNaN(distance) || float.IsInfinity(distance) || distance <= 0f) { return 1f; } float num2 = ((scaling.Count >= 3 && scaling[2] > 0f) ? scaling[2] : 1000f); int num3 = (int)Math.Floor(distance / num2); if (scaling.Count >= 4 && scaling[3] > 0f) { num3 = Math.Min(num3, (int)Math.Floor(scaling[3])); } return Math.Max(0f, 1f + (float)num3 * num); } public static float Horizontal(float x, float z) { return (float)Math.Sqrt(x * x + z * z); } public static bool Active(bool enabled, SpawnSource source) { if (!enabled) { return false; } if ((uint)(source - 6) <= 4u) { return false; } return true; } } public static class TamedTraits { public static bool Stripped(bool keepTraits, bool tamed) { return !keepTraits && tamed; } } public readonly struct PlayerScalingValues { public float HealthPerPlayer { get; } public float DamagePerPlayer { get; } public int MaxPlayers { get; } public PlayerScalingValues(float healthPerPlayer, float damagePerPlayer, int maxPlayers) { HealthPerPlayer = healthPerPlayer; DamagePerPlayer = damagePerPlayer; MaxPlayers = maxPlayers; } } public static class PlayerScaling { public static PlayerScalingValues Resolve(float healthPercent, float damagePercent, int maxPlayers) { return new PlayerScalingValues(Fraction(healthPercent), Fraction(damagePercent), Math.Min(25, Math.Max(1, maxPlayers))); } private static float Fraction(float percent) { if (!float.IsNaN(percent) && !float.IsInfinity(percent)) { return Math.Min(200f, Math.Max(0f, percent)) / 100f; } return 0f; } public static bool Acts(bool pressureModuleOn, bool gameExists) { return pressureModuleOn && gameExists; } public static bool Same(PlayerScalingValues a, PlayerScalingValues b) { if (a.HealthPerPlayer.Equals(b.HealthPerPlayer) && a.DamagePerPlayer.Equals(b.DamagePerPlayer)) { return a.MaxPlayers == b.MaxPlayers; } return false; } } public static class SaddleScaling { public static bool Applies(bool scaleSaddleable, bool isSaddleable) { if (!scaleSaddleable) { return !isSaddleable; } return true; } } public enum PlayerScalingYieldReason { None, NamedMod, ExternalWrite } public static class PlayerScalingOwners { public static IReadOnlyList<ModuleSource> Builtin { get; } = Array.AsReadOnly(new ModuleSource[1] { ModuleStepAside.CreatureManager }); public static IReadOnlyList<ModuleSource> Installed(IEnumerable<string> installedGuids, string? configured) { if (installedGuids == null) { throw new ArgumentNullException("installedGuids"); } HashSet<string> hashSet = new HashSet<string>(installedGuids.Where((string g) => g != null), StringComparer.Ordinal); List<ModuleSource> list = new List<ModuleSource>(); foreach (ModuleSource item in Builtin) { if (hashSet.Contains(item.Guid)) { list.Add(item); } } foreach (string guid in NameList.Parse(configured)) { if (hashSet.Contains(guid) && list.All((ModuleSource f) => f.Guid != guid)) { list.Add(new ModuleSource(guid, guid)); } } return list; } } public sealed class PlayerScalingOwnersCache { private IReadOnlyList<ModuleSource>? owners; private string? configured; private int count = -1; public IReadOnlyList<ModuleSource> Installed(ICollection<string> installedGuids, string? configuredList) { if (installedGuids == null) { throw new ArgumentNullException("installedGuids"); } if (owners == null || count != installedGuids.Count || !string.Equals(configured, configuredList, StringComparison.Ordinal)) { owners = PlayerScalingOwners.Installed(installedGuids, configuredList); configured = configuredList; count = installedGuids.Count; } return owners; } public void Reset() { owners = null; configured = null; count = -1; } } public readonly struct PlayerScalingStep { public bool Write { get; } public PlayerScalingValues Values { get; } public PlayerScalingYieldReason StartedYield { get; } internal static PlayerScalingStep Nothing => new PlayerScalingStep(write: false, default(PlayerScalingValues), PlayerScalingYieldReason.None); public PlayerScalingStep(bool write, PlayerScalingValues values, PlayerScalingYieldReason startedYield) { Write = write; Values = values; StartedYield = startedYield; } } public sealed class PlayerScalingCoordinator { private bool applied; private PlayerScalingValues baseline; private PlayerScalingValues last; public PlayerScalingYieldReason Yielding { get; private set; } public bool Applied => applied; public PlayerScalingStep Step(bool moduleOn, bool namedOwnerInstalled, PlayerScalingValues current, PlayerScalingValues desired) { if (!moduleOn) { Yielding = PlayerScalingYieldReason.None; return TakeRestore(current); } if (namedOwnerInstalled) { PlayerScalingStep step = TakeRestore(current); return Yield(PlayerScalingYieldReason.NamedMod, step); } if (Yielding == PlayerScalingYieldReason.ExternalWrite) { return PlayerScalingStep.Nothing; } Yielding = PlayerScalingYieldReason.None; if (applied && !PlayerScaling.Same(current, last)) { applied = false; return Yield(PlayerScalingYieldReason.ExternalWrite, PlayerScalingStep.Nothing); } if (!applied) { baseline = current; applied = true; } last = desired; return new PlayerScalingStep(write: true, desired, PlayerScalingYieldReason.None); } public PlayerScalingStep Restore(PlayerScalingValues current) { return TakeRestore(current); } public void Reset() { applied = false; Yielding = PlayerScalingYieldReason.None; } private PlayerScalingStep TakeRest