using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using HowToFish.ExpansionKit.Packs; using HowToFish.ExpansionKit.Progression; 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("GamblersReach.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("GamblersReach.Core")] [assembly: AssemblyTitle("GamblersReach.Core")] [assembly: AssemblyVersion("1.0.0.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 GamblersReach.Core { internal static class Identifiers { internal static string Require(string value, string parameterName) { if (string.IsNullOrWhiteSpace(value) || value.Length > 128 || value != value.Trim()) { throw new ArgumentException("An identifier must contain 1–128 characters without surrounding whitespace.", parameterName); } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { throw new ArgumentException("Identifiers cannot contain control characters.", parameterName); } } return value; } } } namespace GamblersReach.Core.Wildlife { public readonly struct HawkFlightPose { public Vector3 Position { get; } public Vector3 Velocity { get; } public HawkFlightPose(Vector3 position, Vector3 velocity) { Position = position; Velocity = velocity; } } public sealed class HawkFlightCurve { private readonly Vector3 start; private readonly Vector3 startVelocity; private readonly Vector3 end; private readonly Vector3 endVelocity; public double Duration { get; } public HawkFlightCurve(Vector3 start, Vector3 startVelocity, Vector3 end, Vector3 endVelocity, double duration) { if (!Finite(start) || !Finite(startVelocity) || !Finite(end) || !Finite(endVelocity) || double.IsNaN(duration) || double.IsInfinity(duration) || duration < 1E-06 || duration > 3.4028234663852886E+38) { throw new ArgumentException("A flight curve requires finite positions, velocities and a positive duration."); } this.start = start; this.startVelocity = startVelocity; this.end = end; this.endVelocity = endVelocity; Duration = duration; } public HawkFlightPose Sample(double elapsed) { if (double.IsNaN(elapsed) || double.IsInfinity(elapsed) || elapsed < 0.0) { throw new ArgumentOutOfRangeException("elapsed"); } float num = (float)Duration; float num2 = (float)Math.Min(1.0, elapsed / Duration); float num3 = num2 * num2; float num4 = num3 * num2; Vector3 position = start * (2f * num4 - 3f * num3 + 1f) + startVelocity * (num * (num4 - 2f * num3 + num2)) + end * (-2f * num4 + 3f * num3) + endVelocity * (num * (num4 - num3)); Vector3 velocity = start * ((6f * num3 - 6f * num2) / num) + startVelocity * (3f * num3 - 4f * num2 + 1f) + end * ((-6f * num3 + 6f * num2) / num) + endVelocity * (3f * num3 - 2f * num2); return new HawkFlightPose(position, velocity); } internal static bool Finite(Vector3 value) { if (!float.IsNaN(value.X) && !float.IsInfinity(value.X) && !float.IsNaN(value.Y) && !float.IsInfinity(value.Y) && !float.IsNaN(value.Z)) { return !float.IsInfinity(value.Z); } return false; } } public sealed class HawkFlightHistory { public const double InterpolationDelay = 0.1; private readonly List<(Vector3 Position, double Time)> samples = new List<(Vector3, double)>(); public bool HasSamples => samples.Count != 0; public void Receive(Vector3 position, double receivedAt) { if (!HawkFlightCurve.Finite(position) || !Finite(receivedAt) || (samples.Count != 0 && receivedAt < samples[samples.Count - 1].Time)) { throw new ArgumentException("Hawk position samples require finite, monotonic arrival times."); } if (samples.Count != 0 && receivedAt == samples[samples.Count - 1].Time) { samples[samples.Count - 1] = (position, receivedAt); } else { samples.Add((position, receivedAt)); } if (samples.Count > 12) { samples.RemoveAt(0); } } public HawkFlightPose Sample(double now) { if (!HasSamples || !Finite(now)) { throw new InvalidOperationException("The hawk has no valid received flight history."); } double num = now - 0.1; if (samples.Count == 1 || num <= samples[0].Time) { return new HawkFlightPose(samples[0].Position, Vector3.Zero); } for (int i = 0; i < samples.Count - 1; i++) { (Vector3, double) tuple = samples[i]; (Vector3, double) tuple2 = samples[i + 1]; if (!(num > tuple2.Item2)) { return new HawkFlightCurve(tuple.Item1, Velocity(i), tuple2.Item1, Velocity(i + 1), tuple2.Item2 - tuple.Item2).Sample(num - tuple.Item2); } } (Vector3, double) tuple3 = samples[samples.Count - 1]; Vector3 vector = Velocity(samples.Count - 1); return new HawkFlightPose(tuple3.Item1 + vector * (float)Math.Min(0.1, num - tuple3.Item2), (num - tuple3.Item2 > 0.1) ? Vector3.Zero : vector); } private Vector3 Velocity(int index) { (Vector3, double) tuple = samples[Math.Max(0, index - 1)]; (Vector3, double) tuple2 = samples[Math.Min(samples.Count - 1, index + 1)]; Vector3 vector = (tuple2.Item1 - tuple.Item1) / (float)(tuple2.Item2 - tuple.Item2); if (!(vector.LengthSquared() > 2500f)) { return vector; } return Vector3.Normalize(vector) * 50f; } private static bool Finite(double value) { if (!double.IsNaN(value)) { return !double.IsInfinity(value); } return false; } } public static class HawkRules { public const int Health = 60; public const int BaseWorth = 20; public const int DiveDamage = 8; public const int MaximumAlive = 1; public const double NativeSeagullSpawnSeconds = 140.0; public const double SpawnSeconds = 420.0; public const double WarningSeconds = 1.2; public const double RecoverySeconds = 4.0; public const double CircleSeconds = 8.0; public const float StrikeRadius = 0.75f; public const float TargetRange = 35f; } public enum HawkStage { Circling, Telegraph, Diving, Recovering } public sealed class HawkSpawnClock { public double Elapsed { get; private set; } public bool Advance(double seconds, bool harborActive, int livingHawks) { if (!Finite(seconds) || seconds <= 0.0 || livingHawks < 0) { throw new ArgumentOutOfRangeException("seconds"); } if (!harborActive || livingHawks >= 1) { Elapsed = 0.0; return false; } Elapsed += seconds; if (Elapsed < 420.0) { return false; } Elapsed = 0.0; return true; } private static bool Finite(double value) { if (!double.IsNaN(value)) { return !double.IsInfinity(value); } return false; } } public sealed class HawkDiveCycle { private HawkFlightCurve? warning; private HawkFlightCurve? approach; private HawkFlightCurve? pullout; public HawkStage Stage { get; private set; } public double Elapsed { get; private set; } public Vector3 DiveOrigin { get; private set; } public Vector3 LockedAim { get; private set; } public Vector3 DiveEnd { get; private set; } public double DiveSeconds { get; private set; } public bool StrikeSpent { get; private set; } public bool CanWarn { get { if (Stage == HawkStage.Circling) { return Elapsed >= 8.0; } return false; } } public int DivesStarted { get; private set; } public void Warn(Vector3 origin, Vector3 target) { Warn(origin, target, Vector3.UnitX * 8f); } public void Warn(Vector3 origin, Vector3 target, Vector3 velocity) { if (!CanWarn) { throw new InvalidOperationException("The hawk must finish circling before another warning."); } if (!Finite(origin) || !Finite(target) || !Finite(velocity) || velocity.LengthSquared() < 1f || velocity.LengthSquared() > 400f || Vector3.DistanceSquared(origin, target) < 1f) { throw new ArgumentException("A dive needs finite, distinct origin and target positions."); } LockedAim = target; DiveOrigin = origin + velocity * 0.84f + Vector3.UnitY * 1.25f; Vector3 vector = Vector3.Normalize(target - DiveOrigin); Vector3 value = new Vector3(vector.X, 0f, vector.Z); value = ((value.LengthSquared() < 0.001f) ? Vector3.UnitZ : Vector3.Normalize(value)); DiveEnd = target + value * 2.5f + Vector3.UnitY * 1.5f; float num = Vector3.Distance(DiveOrigin, target); DiveSeconds = Math.Max(1.1, Math.Min(2.4, (num + 3f) / 15f)); double num2 = DiveSeconds * 0.78; Vector3 vector2 = vector * Math.Min(8f, num / (float)num2); Vector3 vector3 = value * Math.Max(8f, Math.Min(16f, num / (float)num2 * 0.75f)); warning = new HawkFlightCurve(origin, velocity, DiveOrigin, vector2, 1.2); approach = new HawkFlightCurve(DiveOrigin, vector2, target, vector3, num2); pullout = new HawkFlightCurve(target, vector3, DiveEnd, value * 6f + Vector3.UnitY * 7f, DiveSeconds - num2); StrikeSpent = false; Enter(HawkStage.Telegraph); } public void Advance(double seconds, bool targetValid = true) { if (double.IsNaN(seconds) || double.IsInfinity(seconds) || seconds <= 0.0) { throw new ArgumentOutOfRangeException("seconds"); } if (!targetValid && (Stage == HawkStage.Telegraph || Stage == HawkStage.Diving)) { Abort(); return; } Elapsed += seconds; if (Stage == HawkStage.Telegraph && Elapsed >= 1.2) { DivesStarted++; Enter(HawkStage.Diving); } else if (Stage == HawkStage.Diving && Elapsed >= DiveSeconds) { Enter(HawkStage.Recovering); } else if (Stage == HawkStage.Recovering && Elapsed >= 4.0) { Enter(HawkStage.Circling); } } public HawkFlightPose WarningPose(double elapsed) { if (Stage != HawkStage.Telegraph || warning == null) { throw new InvalidOperationException("Warning poses require an active wind-up."); } return warning.Sample(elapsed); } public HawkFlightPose DivePose(double elapsed) { if (Stage != HawkStage.Diving || approach == null || pullout == null) { throw new InvalidOperationException("Dive poses require an active dive."); } if (!(elapsed <= approach.Duration)) { return pullout.Sample(elapsed - approach.Duration); } return approach.Sample(elapsed); } public Vector3 DivePosition(double elapsed) { return DivePose(elapsed).Position; } public bool TryStrike(Vector3 previous, Vector3 next, Vector3 playerCenter) { if (!Finite(previous) || !Finite(next) || !Finite(playerCenter)) { throw new ArgumentException("Strike positions must be finite."); } if (Stage != HawkStage.Diving || StrikeSpent) { return false; } Vector3 vector = next - previous; float num = vector.LengthSquared(); float num2 = ((num < 1E-10f) ? 0f : Math.Max(0f, Math.Min(1f, Vector3.Dot(playerCenter - previous, vector) / num))); if (Vector3.DistanceSquared(playerCenter, previous + vector * num2) > 0.5625f) { return false; } StrikeSpent = true; return true; } public void Abort() { if (Stage != HawkStage.Circling && Stage != HawkStage.Recovering) { Enter(HawkStage.Recovering); } } private void Enter(HawkStage stage) { Stage = stage; Elapsed = 0.0; } private static bool Finite(Vector3 value) { if (Finite(value.X) && Finite(value.Y)) { return Finite(value.Z); } return false; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } } namespace GamblersReach.Core.Progression { public enum ChapterReward { None, AnglerLure, IslandFiveCoordinates } public sealed class ChapterProgress { public int Version { get; set; } = 1; public bool Initialized { get; set; } public bool ExistingIslandFiveAccess { get; set; } public bool BarracudaTrophyDelivered { get; set; } public bool AnglerLureClaimed { get; set; } public bool AnglerTrophyDelivered { get; set; } public bool CoordinatesClaimed { get; set; } public ChapterReward PendingReward { get; set; } public bool CanVisitIslandFive { get { if (!ExistingIslandFiveAccess) { return CoordinatesClaimed; } return true; } } public void Initialize(int nativeUnlockCount, int continuationIslandId = 4) { Validate(); if (nativeUnlockCount < 1 || nativeUnlockCount > 255) { throw new ArgumentOutOfRangeException("nativeUnlockCount"); } if (!Initialized) { ExistingIslandFiveAccess = InterludeRoutePolicy.IsUnlocked(continuationIslandId, nativeUnlockCount); Initialized = true; } } public void DeliverBarracudaTrophy() { RequireReady(); if (PendingReward != ChapterReward.None) { throw new InvalidOperationException("Take the waiting reward before offering another trophy."); } BarracudaTrophyDelivered = true; PendingReward = ChapterReward.AnglerLure; } public void DeliverAnglerTrophy() { RequireReady(); if (!AnglerLureClaimed || CoordinatesClaimed || PendingReward != ChapterReward.None) { throw new InvalidOperationException("The angler trophy is not needed at this stage."); } AnglerTrophyDelivered = true; PendingReward = ChapterReward.IslandFiveCoordinates; } public void Claim(ChapterReward reward) { RequireReady(); if (reward == ChapterReward.None || reward != PendingReward) { throw new InvalidOperationException("That reward is not waiting to be collected."); } if (reward == ChapterReward.AnglerLure) { AnglerLureClaimed = true; } else { CoordinatesClaimed = true; } PendingReward = ChapterReward.None; Validate(); } public void Validate() { if (Version != 1 || !Enum.IsDefined(typeof(ChapterReward), PendingReward) || (!Initialized && (ExistingIslandFiveAccess || BarracudaTrophyDelivered || AnglerLureClaimed || AnglerTrophyDelivered || CoordinatesClaimed || PendingReward != ChapterReward.None)) || (AnglerLureClaimed && !BarracudaTrophyDelivered) || (AnglerTrophyDelivered && !AnglerLureClaimed) || (CoordinatesClaimed && !AnglerTrophyDelivered) || (BarracudaTrophyDelivered && !AnglerLureClaimed && PendingReward != ChapterReward.AnglerLure) || (AnglerTrophyDelivered && !CoordinatesClaimed && PendingReward != ChapterReward.IslandFiveCoordinates) || (PendingReward == ChapterReward.AnglerLure && !BarracudaTrophyDelivered) || (PendingReward == ChapterReward.IslandFiveCoordinates && (!AnglerTrophyDelivered || CoordinatesClaimed))) { throw new ArgumentException("The harbor chapter's trophy and reward state is inconsistent."); } } private void RequireReady() { Validate(); if (!Initialized) { throw new InvalidOperationException("Select a crew save before starting the harbor chapter."); } } } public static class HarborSaveRevision { private const string Release080Plugin = "B82A1AA7035E23D749A682CA415B03FC585DDBC32A4A4640C5D47EDC9D031D44"; private const string Release080Core = "1AB9465A0B9A43B699DCAA2BF61EBD760B2F9C9C9BCE100ECE8BE268EE765260"; private const string Release081Plugin = "A697A545B6D2305402175F0291F1706D49762CF7F9DBBC8C4B1EB863858E36DF"; private const string Release081Core = "4DB633424F0CC228C9EAB0B136C58707873328676F246875CA97F769436B5ABA"; private const string Release082Plugin = "5A5102CF45FE8D331A2D080B24BC7A51900BFD644F75F80F752BAB2A0EAF61F0"; private const string Release082Core = "B02C833A811D2CA9327B473CD870FA26E425FB9D7E4BF55DBD135BD2ABD4F274"; private const string Verified083Plugin = "10E193C337211F02D717B03BC36C9F97BBB93DE4C95886095C246E384236C66C"; private const string Verified083Core = "3F47EA9DC4A2668B1734376BA66BDCBB2D1F227B670277D13DE7E148E83A806F"; private const string Final083Plugin = "BCE48FC27C98F680208514B75665A791FC16CBD2C08F821135B4985D19CE219B"; private const string Final083Core = "FA2A9199DE7B242B93BF9CDC1C34B8BC2801AE8A7B54AC23DA7F70FAD4517F2B"; private const string Published083Plugin = "2B4DDB34D01147F397367362370178E4B70961FFA6FB2121126164ED3C1DB946"; private const string Published083Core = "70DE6B29B13E099814C8C64FE808D73B9DB448CF771B3FCDA7C5A6642A2FF4CC"; public static bool Accept(SavedPack saved, IReadOnlyDictionary code) { if (saved == null || code == null || saved.Key != "gamblers_reach" || code.Count != 2 || !code.TryGetValue("GamblersReach", out string value) || !code.TryGetValue("GamblersReach.Core", out string value2)) { return false; } return saved.Version switch { "0.7.0" => saved.Fingerprint == "23737c25e8a8e86f7707f11d58c433a39f7c1217593500db09e41093c79c59c6" && value == "5226473DBAED2950D4C89AD542707E8EE60E82C963081C130A3A22FE636B78C4" && value2 == "6EE04CA3CF88F4ED9342B3A5E263404F96B722F4EBDC61A3668A96C383FEB7FD", "0.7.1" => saved.Fingerprint == "22918057faa48535942230ca43139bef87aecb8982e607c46630b4ccaa5e76ab" && value == "38FBA14506562EA94D8DC9E3D1B8B20557B9786BD40B276C37B07BE15FBA1242" && value2 == "1C684E478FD719ACEFA93AFBE6FA735E25135482780054DCAD5018028CDFD0DE", "0.7.2" => saved.Fingerprint == "ae798b2247b389e535e12004a9cce35eaa0102a9448485e577e5e0df401b9cfa" && value == "FB131E532CEEAAB58274836CADA223CD3A463D5C6DDA0CDAF4BCA26D7F78FF8B" && value2 == "7886CA9C0B52923355305DF503ECB8E71970BA3BF3A54C4CB4A7035862E71272", "0.7.3" => saved.Fingerprint == "68da1823c373e929acfdfe89029c6f0e53cd96db7065f030e8e5d5240acf875c" && value == "6B84E52DABA097A21AAF5A6721A91E9D93163423EDAEF30CACD861A8F74A2C7F" && value2 == "689ED1F4A5C7321B9F78E68F0EE059ED228186B8F47C97FDF58DD1EC6E942C68", "0.8.0" => saved.Fingerprint == "1f102a86294e76cd038a3e49cf09246ecca2648d90d2c4baeabd17e49e760ec3" && value == "B82A1AA7035E23D749A682CA415B03FC585DDBC32A4A4640C5D47EDC9D031D44" && value2 == "1AB9465A0B9A43B699DCAA2BF61EBD760B2F9C9C9BCE100ECE8BE268EE765260", "0.8.1" => saved.Fingerprint == "e9c47b6e0ef99e06d4082fb3204f3724b8d1e45f5ac58b1e4cc594a9413ef0d5" && value == "A697A545B6D2305402175F0291F1706D49762CF7F9DBBC8C4B1EB863858E36DF" && value2 == "4DB633424F0CC228C9EAB0B136C58707873328676F246875CA97F769436B5ABA", "0.8.2" => saved.Fingerprint == "c3c851a52ccd6fe58581e7dce2199935db86f70cc60bcbea2eba416fa171d41c" && value == "5A5102CF45FE8D331A2D080B24BC7A51900BFD644F75F80F752BAB2A0EAF61F0" && value2 == "B02C833A811D2CA9327B473CD870FA26E425FB9D7E4BF55DBD135BD2ABD4F274", "0.8.3" => saved.Fingerprint == "c9fcbdb01ece013997b6192aaeaa5285afdd63c2df8eea89ea801315ae484fa2" && IsVerified083Code(value, value2), _ => false, }; } public static bool AcceptWildlife(SavedPack saved, IReadOnlyDictionary code) { if (saved == null || saved.Key != "gamblers_reach_wildlife" || code == null || code.Count != 2 || !code.TryGetValue("GamblersReach", out string value) || !code.TryGetValue("GamblersReach.Core", out string value2)) { return false; } return saved.Version switch { "0.8.0" => saved.Fingerprint == "a7e42f62b2dc0bbaee2ae29d747f9d5d24a1e50939840209a32b452d7a1a5fb2" && value == "B82A1AA7035E23D749A682CA415B03FC585DDBC32A4A4640C5D47EDC9D031D44" && value2 == "1AB9465A0B9A43B699DCAA2BF61EBD760B2F9C9C9BCE100ECE8BE268EE765260", "0.8.1" => saved.Fingerprint == "7b978088f9d3925a2dc2641aa80abcf6cd5d75643c9204f1c22ecb1d40a83287" && value == "A697A545B6D2305402175F0291F1706D49762CF7F9DBBC8C4B1EB863858E36DF" && value2 == "4DB633424F0CC228C9EAB0B136C58707873328676F246875CA97F769436B5ABA", "0.8.2" => saved.Fingerprint == "8b9a83bbd5db323bdfae408d38db3d03cc47a4019b0a9752179db84c738ca2eb" && value == "5A5102CF45FE8D331A2D080B24BC7A51900BFD644F75F80F752BAB2A0EAF61F0" && value2 == "B02C833A811D2CA9327B473CD870FA26E425FB9D7E4BF55DBD135BD2ABD4F274", "0.8.3" => saved.Fingerprint == "9e5f9f2c0588b1a61258770f2692c299fbad35b2c56d1bae0059ef85cc7f339e" && IsVerified083Code(value, value2), _ => false, }; } private static bool IsVerified083Code(string? plugin, string? core) { if ((!(plugin == "10E193C337211F02D717B03BC36C9F97BBB93DE4C95886095C246E384236C66C") || !(core == "3F47EA9DC4A2668B1734376BA66BDCBB2D1F227B670277D13DE7E148E83A806F")) && (!(plugin == "BCE48FC27C98F680208514B75665A791FC16CBD2C08F821135B4985D19CE219B") || !(core == "FA2A9199DE7B242B93BF9CDC1C34B8BC2801AE8A7B54AC23DA7F70FAD4517F2B"))) { if (plugin == "2B4DDB34D01147F397367362370178E4B70961FFA6FB2121126164ED3C1DB946") { return core == "70DE6B29B13E099814C8C64FE808D73B9DB448CF771B3FCDA7C5A6642A2FF4CC"; } return false; } return true; } } public sealed class HuntProgress { public int Version { get; set; } = 1; public bool MiniBossDefeated { get; set; } public bool BossDefeated { get; set; } public bool TrophyReturned { get; set; } public bool RifleClaimed { get; set; } public void Validate() { if (Version != 1 || (BossDefeated && !MiniBossDefeated) || (TrophyReturned && !BossDefeated) || (RifleClaimed && !TrophyReturned)) { throw new ArgumentException("The hunt progress is inconsistent."); } } public bool RecordMiniBoss() { Validate(); if (MiniBossDefeated) { return false; } MiniBossDefeated = true; return true; } public bool RecordBoss() { Validate(); if (!MiniBossDefeated) { throw new InvalidOperationException("Defeat the Reef Marauder before the Breakwater King."); } if (BossDefeated) { return false; } BossDefeated = true; return true; } public void ReturnTrophy() { Validate(); if (!BossDefeated || TrophyReturned) { throw new InvalidOperationException("The crown is not needed at this stage."); } TrophyReturned = true; } public void ClaimRifle() { Validate(); if (!TrophyReturned || RifleClaimed) { throw new InvalidOperationException("The hunt reward is not available."); } RifleClaimed = true; } } public sealed class VanillaProgress { public const int FinalVanillaIslandId = 4; public int HighestCompletedIslandId { get; } public bool HasCompletedGame { get; } public VanillaProgress(int highestCompletedIslandId, bool hasCompletedGame = false) { if (highestCompletedIslandId < -1 || highestCompletedIslandId > 4) { throw new ArgumentOutOfRangeException("highestCompletedIslandId"); } HighestCompletedIslandId = highestCompletedIslandId; HasCompletedGame = hasCompletedGame; } } public enum QuestStage { Locked, Introduction, CollectSixSpecies, ClaimGearReward, PostgameRareCatch, Complete, AwaitFinale } public enum QuestNpcRole { Host, Angler, Outfitter } public sealed class QuestDefinition { public const int RequiredSpeciesCount = 6; private readonly ReadOnlyCollection speciesIds; public IReadOnlyList CollectionSpeciesIds => speciesIds; public string RareSpeciesId { get; } public QuestDefinition(IEnumerable collectionSpeciesIds, string rareSpeciesId) { if (collectionSpeciesIds == null) { throw new ArgumentNullException("collectionSpeciesIds"); } string[] array = (from id in collectionSpeciesIds.Take(7) select Identifiers.Require(id, "collectionSpeciesIds")).ToArray(); if (array.Length != 6 || array.Distinct(StringComparer.Ordinal).Count() != 6) { throw new ArgumentException("Exactly six distinct species IDs are required.", "collectionSpeciesIds"); } Array.Sort(array, (IComparer?)StringComparer.Ordinal); speciesIds = Array.AsReadOnly(array); RareSpeciesId = Identifiers.Require(rareSpeciesId, "rareSpeciesId"); if (!array.Contains(RareSpeciesId, StringComparer.Ordinal)) { throw new ArgumentException("The rare-catch target must be one of the six collection species.", "rareSpeciesId"); } } } public sealed class ModProgress { public int Version => 1; public bool IslandUnlocked { get; } public bool IntroductionCompleted { get; } public IReadOnlyList CollectedSpeciesIds { get; } public bool GearRewardClaimed { get; } public bool RareCatchQuestCompleted { get; } internal ModProgress(bool islandUnlocked, bool introductionCompleted, IEnumerable collectedSpeciesIds, bool gearRewardClaimed, bool rareCatchQuestCompleted) { IslandUnlocked = islandUnlocked; IntroductionCompleted = introductionCompleted; CollectedSpeciesIds = Array.AsReadOnly(collectedSpeciesIds.OrderBy((string id) => id, StringComparer.Ordinal).ToArray()); GearRewardClaimed = gearRewardClaimed; RareCatchQuestCompleted = rareCatchQuestCompleted; } public static ModProgress New() { return new ModProgress(islandUnlocked: false, introductionCompleted: false, Array.Empty(), gearRewardClaimed: false, rareCatchQuestCompleted: false); } public ModProgressSnapshot CreateSnapshot() { return new ModProgressSnapshot { Version = Version, IslandUnlocked = IslandUnlocked, IntroductionCompleted = IntroductionCompleted, CollectedSpeciesIds = CollectedSpeciesIds.ToArray(), GearRewardClaimed = GearRewardClaimed, RareCatchQuestCompleted = RareCatchQuestCompleted }; } } public sealed class ModProgressSnapshot { public const int CurrentVersion = 1; public int Version { get; set; } = 1; public bool IslandUnlocked { get; set; } public bool IntroductionCompleted { get; set; } public string[] CollectedSpeciesIds { get; set; } = Array.Empty(); public bool GearRewardClaimed { get; set; } public bool RareCatchQuestCompleted { get; set; } } public sealed class QuestProgression { public QuestDefinition Definition { get; } public QuestProgression(QuestDefinition definition) { Definition = definition ?? throw new ArgumentNullException("definition"); } public static bool IsVanillaEligible(VanillaProgress vanilla) { if (vanilla == null) { throw new ArgumentNullException("vanilla"); } if (!vanilla.HasCompletedGame) { return vanilla.HighestCompletedIslandId >= 3; } return true; } public bool CanVisitIsland(VanillaProgress vanilla, ModProgress progress) { Validate(progress); if (!IsVanillaEligible(vanilla)) { return progress.IslandUnlocked; } return true; } public ModProgress RefreshEligibility(VanillaProgress vanilla, ModProgress progress) { if (!CanVisitIsland(vanilla, progress) || progress.IslandUnlocked) { return progress; } return Copy(progress, true); } public QuestStage GetStage(VanillaProgress vanilla, ModProgress progress) { if (!CanVisitIsland(vanilla, progress)) { return QuestStage.Locked; } if (!progress.IntroductionCompleted) { return QuestStage.Introduction; } if (progress.CollectedSpeciesIds.Count < 6) { return QuestStage.CollectSixSpecies; } if (!progress.GearRewardClaimed) { return QuestStage.ClaimGearReward; } if (progress.RareCatchQuestCompleted) { return QuestStage.Complete; } if (!vanilla.HasCompletedGame) { return QuestStage.AwaitFinale; } return QuestStage.PostgameRareCatch; } public static QuestNpcRole? NpcFor(QuestStage stage) { switch (stage) { case QuestStage.Introduction: return QuestNpcRole.Host; case QuestStage.CollectSixSpecies: return QuestNpcRole.Angler; case QuestStage.ClaimGearReward: return QuestNpcRole.Outfitter; case QuestStage.PostgameRareCatch: return QuestNpcRole.Angler; case QuestStage.Locked: case QuestStage.Complete: case QuestStage.AwaitFinale: return null; default: throw new ArgumentOutOfRangeException("stage"); } } public ModProgress CompleteIntroduction(VanillaProgress vanilla, ModProgress progress) { RequireStage(vanilla, progress, QuestStage.Introduction); return Copy(progress, true, true); } public ModProgress RecordCatch(VanillaProgress vanilla, ModProgress progress, string speciesId) { Identifiers.Require(speciesId, "speciesId"); progress = RefreshEligibility(vanilla, progress); if (!progress.IntroductionCompleted || !Definition.CollectionSpeciesIds.Contains(speciesId, StringComparer.Ordinal)) { return progress; } if (!progress.CollectedSpeciesIds.Contains(speciesId, StringComparer.Ordinal)) { ModProgress progress2 = progress; IEnumerable collectedSpeciesIds = progress.CollectedSpeciesIds.Concat(new string[1] { speciesId }); return Copy(progress2, null, null, collectedSpeciesIds); } if (vanilla.HasCompletedGame && progress.GearRewardClaimed && !progress.RareCatchQuestCompleted && speciesId == Definition.RareSpeciesId) { ModProgress progress3 = progress; bool? rareCatchQuestCompleted = true; return Copy(progress3, null, null, null, null, rareCatchQuestCompleted); } return progress; } public ModProgress ClaimGearReward(VanillaProgress vanilla, ModProgress progress) { RequireStage(vanilla, progress, QuestStage.ClaimGearReward); bool? gearRewardClaimed = true; return Copy(progress, null, null, null, gearRewardClaimed); } public ModProgress Restore(ModProgressSnapshot snapshot) { if (snapshot == null) { throw new ArgumentNullException("snapshot"); } if (snapshot.Version != 1) { throw new ArgumentException("Unsupported mod progress version.", "snapshot"); } if (snapshot.CollectedSpeciesIds == null || snapshot.CollectedSpeciesIds.Length > 6) { throw new ArgumentException("Invalid collection state.", "snapshot"); } string[] collectedSpeciesIds = snapshot.CollectedSpeciesIds; for (int i = 0; i < collectedSpeciesIds.Length; i++) { Identifiers.Require(collectedSpeciesIds[i], "snapshot"); } if (snapshot.CollectedSpeciesIds.Distinct(StringComparer.Ordinal).Count() != snapshot.CollectedSpeciesIds.Length) { throw new ArgumentException("Duplicate collected species IDs.", "snapshot"); } ModProgress modProgress = new ModProgress(snapshot.IslandUnlocked, snapshot.IntroductionCompleted, snapshot.CollectedSpeciesIds, snapshot.GearRewardClaimed, snapshot.RareCatchQuestCompleted); Validate(modProgress); return modProgress; } private void Validate(ModProgress progress) { if (progress == null) { throw new ArgumentNullException("progress"); } if ((progress.IntroductionCompleted && !progress.IslandUnlocked) || (progress.CollectedSpeciesIds.Count > 0 && !progress.IntroductionCompleted) || (progress.GearRewardClaimed && progress.CollectedSpeciesIds.Count != 6) || (progress.RareCatchQuestCompleted && !progress.GearRewardClaimed) || progress.CollectedSpeciesIds.Any((string id) => !Definition.CollectionSpeciesIds.Contains(id, StringComparer.Ordinal))) { throw new ArgumentException("Progress is inconsistent with the quest definition or its prerequisites.", "progress"); } } private void RequireStage(VanillaProgress vanilla, ModProgress progress, QuestStage stage) { QuestStage stage2 = GetStage(vanilla, progress); if (stage2 != stage) { throw new InvalidOperationException($"This operation requires {stage}; the quest is {stage2}."); } } private static ModProgress Copy(ModProgress progress, bool? islandUnlocked = null, bool? introductionCompleted = null, IEnumerable? collectedSpeciesIds = null, bool? gearRewardClaimed = null, bool? rareCatchQuestCompleted = null) { return new ModProgress(islandUnlocked ?? progress.IslandUnlocked, introductionCompleted ?? progress.IntroductionCompleted, collectedSpeciesIds ?? progress.CollectedSpeciesIds, gearRewardClaimed ?? progress.GearRewardClaimed, rareCatchQuestCompleted ?? progress.RareCatchQuestCompleted); } } } namespace GamblersReach.Core.Content { public sealed class ManifestEntry { public string Id { get; } public string CanonicalData { get; } public ManifestEntry(string id, string canonicalData) { Id = Identifiers.Require(id, "id"); CanonicalData = canonicalData ?? throw new ArgumentNullException("canonicalData"); } } public static class ContentManifest { public const int FormatVersion = 1; public static string ComputeChecksum(IEnumerable entries) { if (entries == null) { throw new ArgumentNullException("entries"); } ManifestEntry[] array = entries.ToArray(); if (array.Any((ManifestEntry entry) => entry == null)) { throw new ArgumentException("A manifest cannot contain null entries.", "entries"); } Array.Sort(array, (ManifestEntry left, ManifestEntry right) => StringComparer.Ordinal.Compare(left.Id, right.Id)); for (int num = 1; num < array.Length; num++) { if (array[num - 1].Id == array[num].Id) { throw new ArgumentException("Duplicate content ID: " + array[num].Id, "entries"); } } using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), leaveOpen: true)) { WriteText(binaryWriter, "GamblersReach.ContentManifest"); binaryWriter.Write(1); binaryWriter.Write(array.Length); ManifestEntry[] array2 = array; foreach (ManifestEntry manifestEntry in array2) { WriteText(binaryWriter, manifestEntry.Id); WriteText(binaryWriter, manifestEntry.CanonicalData); } } using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(memoryStream.ToArray())).Replace("-", "").ToLowerInvariant(); } private static void WriteText(BinaryWriter writer, string value) { byte[] bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes(value); writer.Write(bytes.Length); writer.Write(bytes); } } } namespace GamblersReach.Core.Blackjack { public enum PlayerAction { Hit, Stand } public enum BlackjackRoundState { PlayerTurn, Resolved, SettlementPending, Settled } public sealed class BlackjackRound { private readonly CardDeck deck; private readonly List playerCards = new List(); private readonly List dealerCards = new List(); private readonly List actions = new List(); private readonly ReadOnlyCollection playerView; private readonly ReadOnlyCollection dealerView; private int nextCard; public Guid RoundId { get; } public ItemWager Wager { get; } public BlackjackRoundState State { get; private set; } public IReadOnlyList PlayerCards => playerView; public IReadOnlyList DealerCards => dealerView; public Card DealerUpCard => dealerCards[0]; public HandScore PlayerScore => HandEvaluator.Score(playerCards); public HandScore DealerScore => HandEvaluator.Score(dealerCards); public int CardsRemaining => deck.Cards.Count - nextCard; public BlackjackResult? Result { get; private set; } public SettlementInstruction? Settlement { get; private set; } public BlackjackRound(Guid roundId, ItemWager wager, CardDeck deck) { if (roundId == Guid.Empty) { throw new ArgumentException("A globally unique nonempty round ID is required.", "roundId"); } RoundId = roundId; Wager = wager ?? throw new ArgumentNullException("wager"); this.deck = deck ?? throw new ArgumentNullException("deck"); playerView = playerCards.AsReadOnly(); dealerView = dealerCards.AsReadOnly(); playerCards.Add(Draw()); dealerCards.Add(Draw()); playerCards.Add(Draw()); dealerCards.Add(Draw()); if (PlayerScore.IsNatural || DealerScore.IsNatural) { Resolve((PlayerScore.IsNatural && DealerScore.IsNatural) ? BlackjackOutcome.Push : (PlayerScore.IsNatural ? BlackjackOutcome.Natural : BlackjackOutcome.Loss)); } } public void Play(PlayerAction action) { if (action != PlayerAction.Hit && action != PlayerAction.Stand) { throw new ArgumentOutOfRangeException("action"); } RequireState(BlackjackRoundState.PlayerTurn); if (action == PlayerAction.Hit) { playerCards.Add(Draw()); if (PlayerScore.IsBust) { Resolve(BlackjackOutcome.Loss); } else if (PlayerScore.Total == 21) { FinishDealer(); } } else { FinishDealer(); } actions.Add(action); } public SettlementInstruction PrepareSettlement() { RequireState(BlackjackRoundState.Resolved); Settlement = new SettlementInstruction(Wager, Result); State = BlackjackRoundState.SettlementPending; return Settlement; } public void AcknowledgeSettlement(Guid settlementId) { RequireState(BlackjackRoundState.SettlementPending); if (settlementId != RoundId) { throw new ArgumentException("The acknowledgement belongs to a different settlement.", "settlementId"); } State = BlackjackRoundState.Settled; } public RoundSnapshot CreateSnapshot() { return new RoundSnapshot { Version = 1, RoundId = RoundId, EscrowId = Wager.EscrowId, WagerValue = Wager.Value, Deck = deck.Cards.Select((Card card) => new CardSnapshot { Suit = card.Suit, Rank = card.Rank }).ToArray(), Actions = actions.ToArray(), State = State }; } public static BlackjackRound Restore(RoundSnapshot snapshot) { if (snapshot == null) { throw new ArgumentNullException("snapshot"); } if (snapshot.Version != 1) { throw new ArgumentException("Unsupported round snapshot version.", "snapshot"); } if (snapshot.Deck == null || snapshot.Deck.Length != 52 || snapshot.Deck.Any((CardSnapshot card) => card == null)) { throw new ArgumentException("A complete recovery deck is required.", "snapshot"); } if (snapshot.Actions == null || snapshot.Actions.Length > 48) { throw new ArgumentException("Invalid action history.", "snapshot"); } CardDeck cardDeck = new CardDeck(snapshot.Deck.Select((CardSnapshot card) => new Card(card.Suit, card.Rank))); BlackjackRound blackjackRound = new BlackjackRound(snapshot.RoundId, new ItemWager(snapshot.EscrowId, snapshot.WagerValue), cardDeck); try { PlayerAction[] array = snapshot.Actions; foreach (PlayerAction action in array) { blackjackRound.Play(action); } switch (snapshot.State) { case BlackjackRoundState.PlayerTurn: case BlackjackRoundState.Resolved: if (blackjackRound.State != snapshot.State) { throw new ArgumentException("Saved state disagrees with the replayed round.", "snapshot"); } break; case BlackjackRoundState.SettlementPending: blackjackRound.PrepareSettlement(); break; case BlackjackRoundState.Settled: blackjackRound.PrepareSettlement(); blackjackRound.AcknowledgeSettlement(blackjackRound.RoundId); break; default: throw new ArgumentException("Unknown saved round state.", "snapshot"); } } catch (InvalidOperationException innerException) { throw new ArgumentException("The recovery history contains an out-of-turn transition.", "snapshot", innerException); } return blackjackRound; } private Card Draw() { if (nextCard == deck.Cards.Count) { throw new InvalidOperationException("The deck is exhausted."); } return deck.Cards[nextCard++]; } private void FinishDealer() { while (DealerScore.Total < 17) { dealerCards.Add(Draw()); } Resolve((DealerScore.IsBust || PlayerScore.Total > DealerScore.Total) ? BlackjackOutcome.Win : ((PlayerScore.Total == DealerScore.Total) ? BlackjackOutcome.Push : BlackjackOutcome.Loss)); } private void Resolve(BlackjackOutcome outcome) { RequireState(BlackjackRoundState.PlayerTurn); if (Result != null) { throw new InvalidOperationException("The round has already resolved."); } Result = new BlackjackResult(RoundId, outcome, PlayerScore, DealerScore); State = BlackjackRoundState.Resolved; } private void RequireState(BlackjackRoundState required) { if (State != required) { throw new InvalidOperationException($"This operation requires {required}; the round is {State}."); } } } public sealed class CardSnapshot { public Suit Suit { get; set; } public Rank Rank { get; set; } } public sealed class RoundSnapshot { public const int CurrentVersion = 1; public int Version { get; set; } = 1; public Guid RoundId { get; set; } public string EscrowId { get; set; } = string.Empty; public float WagerValue { get; set; } public CardSnapshot[] Deck { get; set; } = Array.Empty(); public PlayerAction[] Actions { get; set; } = Array.Empty(); public BlackjackRoundState State { get; set; } } public sealed class BlackjackTableGate { public long Revision { get; private set; } public bool IsLocked { get; private set; } public int ActorId { get; private set; } = -1; public Guid RoundId { get; private set; } public void RequireAvailable(long expectedRevision) { if (IsLocked) { throw new InvalidOperationException("The blackjack table is in use. Wait for the current hand to finish."); } if (expectedRevision != Revision) { throw new InvalidOperationException("The table changed before your deal arrived. Review the table and try again."); } } public void Claim(long expectedRevision, int actorId, Guid roundId) { RequireAvailable(expectedRevision); if (actorId < 0) { throw new ArgumentOutOfRangeException("actorId"); } if (roundId == Guid.Empty) { throw new ArgumentException("A hand must have a round identity.", "roundId"); } long revision = checked(Revision + 1); ActorId = actorId; RoundId = roundId; IsLocked = true; Revision = revision; } public void RequireActor(int actorId, Guid roundId) { if (!IsLocked || actorId != ActorId || roundId != RoundId) { throw new InvalidOperationException("Only the player who dealt this hand can control it."); } } public void Advance(int actorId, Guid roundId) { RequireActor(actorId, roundId); checked { Revision++; } } public void Release(int actorId, Guid roundId) { RequireActor(actorId, roundId); long revision = checked(Revision + 1); IsLocked = false; Revision = revision; } public void Reset() { long revision = checked(Revision + 1); IsLocked = false; ActorId = -1; RoundId = Guid.Empty; Revision = revision; } } public enum Suit { Clubs, Diamonds, Hearts, Spades } public enum Rank { Ace = 1, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King } public readonly struct Card : IEquatable { public Suit Suit { get; } public Rank Rank { get; } public bool IsValid { get { if (Suit >= Suit.Clubs && Suit <= Suit.Spades && Rank >= Rank.Ace) { return Rank <= Rank.King; } return false; } } public Card(Suit suit, Rank rank) { if (suit < Suit.Clubs || suit > Suit.Spades) { throw new ArgumentOutOfRangeException("suit"); } if (rank < Rank.Ace || rank > Rank.King) { throw new ArgumentOutOfRangeException("rank"); } Suit = suit; Rank = rank; } public bool Equals(Card other) { if (Suit == other.Suit) { return Rank == other.Rank; } return false; } public override bool Equals(object? obj) { if (obj is Card other) { return Equals(other); } return false; } public override int GetHashCode() { return (int)((int)Suit * 13 + Rank); } public override string ToString() { return $"{Rank} of {Suit}"; } public static bool operator ==(Card left, Card right) { return left.Equals(right); } public static bool operator !=(Card left, Card right) { return !left.Equals(right); } } public readonly struct HandScore { public int Total { get; } public bool IsSoft { get; } public int CardCount { get; } public bool IsBust => Total > 21; public bool IsNatural { get { if (CardCount == 2) { return Total == 21; } return false; } } internal HandScore(int total, bool isSoft, int cardCount) { Total = total; IsSoft = isSoft; CardCount = cardCount; } } public static class HandEvaluator { public static HandScore Score(IEnumerable cards) { if (cards == null) { throw new ArgumentNullException("cards"); } int num = 0; int num2 = 0; int num3 = 0; foreach (Card card in cards) { if (!card.IsValid) { throw new ArgumentException("A hand contains an invalid card.", "cards"); } checked { num += Math.Min(unchecked((int)card.Rank), 10); num3++; } if (card.Rank == Rank.Ace) { num2++; } } bool flag = num2 > 0 && num <= 11; return new HandScore(flag ? (num + 10) : num, flag, num3); } } public sealed class CardDeck { private readonly ReadOnlyCollection cards; public IReadOnlyList Cards => cards; public CardDeck(IEnumerable orderedCards) { if (orderedCards == null) { throw new ArgumentNullException("orderedCards"); } Card[] array = orderedCards.Take(53).ToArray(); if (array.Length != 52 || array.Any((Card card) => !card.IsValid) || array.Distinct().Count() != 52) { throw new ArgumentException("A deck must contain each of the 52 valid cards exactly once.", "orderedCards"); } cards = Array.AsReadOnly(array); } public static CardDeck Ordered() { List list = new List(52); for (int i = 0; i < 4; i++) { for (int j = 1; j <= 13; j++) { list.Add(new Card((Suit)i, (Rank)j)); } } return new CardDeck(list); } public static CardDeck Shuffled(Random random) { if (random == null) { throw new ArgumentNullException("random"); } Card[] array = Ordered().Cards.ToArray(); for (int num = array.Length - 1; num > 0; num--) { int num2 = random.Next(num + 1); if (num2 < 0 || num2 > num) { throw new ArgumentException("The random source returned an invalid index.", "random"); } ref Card reference = ref array[num]; ref Card reference2 = ref array[num2]; Card card = array[num2]; Card card2 = array[num]; reference = card; reference2 = card2; } return new CardDeck(array); } } public enum BlackjackOutcome { Loss, Push, Win, Natural } public enum SettlementKind { Forfeit, ReturnOriginalItems, AwardItems } public static class PayoutRules { public const float MaximumWagerValue = 1000000f; public const float MaximumReturnMultiplier = 2.5f; public static void ValidateWagerValue(float value) { if (float.IsNaN(value) || float.IsInfinity(value) || value <= 0f || value > 1000000f) { throw new ArgumentOutOfRangeException("value", "Wager value must be finite, positive, and at most 1,000,000."); } } public static float MultiplierFor(BlackjackOutcome outcome) { return outcome switch { BlackjackOutcome.Loss => 0f, BlackjackOutcome.Push => 1f, BlackjackOutcome.Win => 2f, BlackjackOutcome.Natural => 2.5f, _ => throw new ArgumentOutOfRangeException("outcome"), }; } public static void ValidateReturnMultiplier(float multiplier) { if (multiplier != 0f && multiplier != 1f && multiplier != 2f && multiplier != 2.5f) { throw new ArgumentOutOfRangeException("multiplier", "Only the four defined payout multipliers are supported."); } } public static float CalculateReturnValue(float wagerValue, float multiplier) { ValidateWagerValue(wagerValue); ValidateReturnMultiplier(multiplier); return wagerValue * multiplier; } } public sealed class ItemWager { public string EscrowId { get; } public float Value { get; } public ItemWager(string escrowId, float value) { EscrowId = Identifiers.Require(escrowId, "escrowId"); PayoutRules.ValidateWagerValue(value); Value = value; } } public sealed class BlackjackResult { public Guid RoundId { get; } public BlackjackOutcome Outcome { get; } public HandScore PlayerScore { get; } public HandScore DealerScore { get; } public float TotalReturnMultiplier => PayoutRules.MultiplierFor(Outcome); public bool ReturnOriginalItems => Outcome == BlackjackOutcome.Push; internal BlackjackResult(Guid roundId, BlackjackOutcome outcome, HandScore playerScore, HandScore dealerScore) { RoundId = roundId; Outcome = outcome; PlayerScore = playerScore; DealerScore = dealerScore; } } public sealed class SettlementInstruction { public Guid SettlementId { get; } public string EscrowId { get; } public float WagerValue { get; } public SettlementKind Kind { get; } public float TotalReturnMultiplier { get; } public float TotalReturnValue => PayoutRules.CalculateReturnValue(WagerValue, TotalReturnMultiplier); internal SettlementInstruction(ItemWager wager, BlackjackResult result) { SettlementId = result.RoundId; EscrowId = wager.EscrowId; WagerValue = wager.Value; TotalReturnMultiplier = result.TotalReturnMultiplier; Kind = ((result.Outcome != BlackjackOutcome.Loss) ? (result.ReturnOriginalItems ? SettlementKind.ReturnOriginalItems : SettlementKind.AwardItems) : SettlementKind.Forfeit); } } }