Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of HylandHelper v0.1.3
HylandHelper.BridgeMod.dll
Decompiled 5 hours 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.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using HylandHelper.BridgeContracts; using HylandHelper.BridgeMod; using MelonLoader; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: InternalsVisibleTo("HylandHelper.BridgeMod.Tests")] [assembly: MelonInfo(typeof(HylandHelperBridgeMod), "Hyland Helper Bridge", "0.1.3", "MadJag Studios", null)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("MadJag Studios")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © 2026 MadJag Studios")] [assembly: AssemblyDescription("Hyland Helper bridge for Schedule I")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3")] [assembly: AssemblyProduct("Hyland Helper")] [assembly: AssemblyTitle("HylandHelper.BridgeMod")] [assembly: AssemblyVersion("0.1.3.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 HylandHelper.BridgeContracts { public static class BridgeContract { public const string SchemaVersion = "1.0.0"; } public enum BridgeEnvelopeType { Heartbeat, Snapshot, Capabilities, SessionEvent, Ingest } public enum BridgeCapabilityStatus { Present, Partial, Unavailable, Degraded } public sealed class BridgeEnvelope { [JsonPropertyName("envelopeType")] public string EnvelopeType { get; set; } = "heartbeat"; [JsonPropertyName("schemaVersion")] public string SchemaVersion { get; set; } = "1.0.0"; [JsonPropertyName("modVersion")] public string ModVersion { get; set; } = ""; [JsonPropertyName("gameVersion")] public string GameVersion { get; set; } = ""; [JsonPropertyName("melonLoaderVersion")] public string MelonLoaderVersion { get; set; } = ""; [JsonPropertyName("saveFingerprint")] public BridgeSaveFingerprint SaveFingerprint { get; set; } = new BridgeSaveFingerprint(); [JsonPropertyName("sequence")] public long Sequence { get; set; } [JsonPropertyName("sentAt")] public string SentAt { get; set; } = ""; [JsonPropertyName("capabilities")] public BridgeCapabilities Capabilities { get; set; } = new BridgeCapabilities(); [JsonPropertyName("snapshot")] public BridgeSnapshot? Snapshot { get; set; } [JsonPropertyName("session")] public BridgeSessionEvent? Session { get; set; } } public sealed class BridgeSaveFingerprint { [JsonPropertyName("slotId")] public string SlotId { get; set; } = ""; [JsonPropertyName("steamIdHash")] public string? SteamIdHash { get; set; } [JsonPropertyName("fingerprint")] public string Fingerprint { get; set; } = ""; } public sealed class BridgeCapabilities { [JsonPropertyName("slices")] public Dictionary<string, BridgeCapabilitySlice> Slices { get; set; } = new Dictionary<string, BridgeCapabilitySlice>(); [JsonPropertyName("features")] public Dictionary<string, BridgeFeatureCapability>? Features { get; set; } } public sealed class BridgeFeatureCapability { [JsonPropertyName("supported")] public bool Supported { get; set; } [JsonPropertyName("protocolVersion")] public int ProtocolVersion { get; set; } } public sealed class BridgeCapabilitySlice { [JsonPropertyName("status")] public string Status { get; set; } = "unavailable"; [JsonPropertyName("source")] public string Source { get; set; } = "notSupported"; [JsonPropertyName("reason")] public string? Reason { get; set; } [JsonPropertyName("gameDataType")] public string? GameDataType { get; set; } [JsonPropertyName("gameDataVersion")] public int? GameDataVersion { get; set; } } public sealed class BridgeSnapshot { [JsonPropertyName("capturedAt")] public string CapturedAt { get; set; } = ""; [JsonPropertyName("status")] public string Status { get; set; } = "partial"; [JsonPropertyName("slices")] public List<string> Slices { get; set; } = new List<string>(); [JsonPropertyName("money")] public BridgeMoneySnapshot? Money { get; set; } [JsonPropertyName("rank")] public BridgeRankSnapshot? Rank { get; set; } [JsonPropertyName("time")] public BridgeTimeSnapshot? Time { get; set; } [JsonPropertyName("inventory")] public BridgeInventorySnapshot? Inventory { get; set; } [JsonPropertyName("properties")] public BridgePropertiesSnapshot? Properties { get; set; } [JsonPropertyName("products")] public BridgeProductsSnapshot? Products { get; set; } [JsonPropertyName("layout")] public BridgeLayoutSnapshot? Layout { get; set; } [JsonPropertyName("warnings")] public List<string> Warnings { get; set; } = new List<string>(); } public sealed class BridgeMoneySnapshot { [JsonPropertyName("onlineBalance")] public decimal OnlineBalance { get; set; } [JsonPropertyName("networth")] public decimal Networth { get; set; } [JsonPropertyName("lifetimeEarnings")] public decimal LifetimeEarnings { get; set; } [JsonPropertyName("cashOnHandTotal")] public decimal CashOnHandTotal { get; set; } [JsonPropertyName("appProfileCash")] public decimal AppProfileCash { get; set; } [JsonPropertyName("appProfileCashSource")] public string AppProfileCashSource { get; set; } = "cashOnHandTotal"; [JsonPropertyName("cashAggregation")] public BridgeCashAggregation? CashAggregation { get; set; } } public sealed class BridgeCashAggregation { [JsonPropertyName("includesPlayerInventory")] public bool IncludesPlayerInventory { get; set; } [JsonPropertyName("includesStorage")] public bool IncludesStorage { get; set; } [JsonPropertyName("missingContainers")] public List<string> MissingContainers { get; set; } = new List<string>(); } public sealed class BridgeRankSnapshot { [JsonPropertyName("rank")] public string? Rank { get; set; } [JsonPropertyName("tier")] public int? Tier { get; set; } [JsonPropertyName("xp")] public decimal? Xp { get; set; } [JsonPropertyName("totalXp")] public decimal? TotalXp { get; set; } [JsonPropertyName("unlockedRegions")] public List<string> UnlockedRegions { get; set; } = new List<string>(); } public sealed class BridgeTimeSnapshot { [JsonPropertyName("timeOfDay")] public decimal? TimeOfDay { get; set; } [JsonPropertyName("elapsedDays")] public int? ElapsedDays { get; set; } [JsonPropertyName("playtimeSeconds")] public decimal? PlaytimeSeconds { get; set; } } public sealed class BridgeInventorySnapshot { [JsonPropertyName("totalStacks")] public int TotalStacks { get; set; } [JsonPropertyName("totalQuantity")] public decimal TotalQuantity { get; set; } [JsonPropertyName("cashOnHandTotal")] public decimal CashOnHandTotal { get; set; } [JsonPropertyName("sources")] public List<BridgeInventorySourceSummary> Sources { get; set; } = new List<BridgeInventorySourceSummary>(); [JsonPropertyName("items")] public List<BridgeInventoryItemStack> Items { get; set; } = new List<BridgeInventoryItemStack>(); [JsonPropertyName("truncated")] public bool Truncated { get; set; } } public sealed class BridgeInventorySourceSummary { [JsonPropertyName("source")] public string Source { get; set; } = ""; [JsonPropertyName("container")] public string? Container { get; set; } [JsonPropertyName("itemStacks")] public int ItemStacks { get; set; } [JsonPropertyName("cashOnHand")] public decimal CashOnHand { get; set; } } public sealed class BridgeInventoryItemStack { [JsonPropertyName("id")] public string Id { get; set; } = ""; [JsonPropertyName("quantity")] public decimal Quantity { get; set; } [JsonPropertyName("source")] public string Source { get; set; } = ""; [JsonPropertyName("container")] public string? Container { get; set; } [JsonPropertyName("dataType")] public string? DataType { get; set; } [JsonPropertyName("quality")] public string? Quality { get; set; } [JsonPropertyName("packagingId")] public string? PackagingId { get; set; } } public sealed class BridgePropertiesSnapshot { [JsonPropertyName("ownedCount")] public int OwnedCount { get; set; } [JsonPropertyName("totalCount")] public int TotalCount { get; set; } [JsonPropertyName("properties")] public List<BridgePropertySummary> Properties { get; set; } = new List<BridgePropertySummary>(); } public sealed class BridgePropertySummary { [JsonPropertyName("code")] public string Code { get; set; } = ""; [JsonPropertyName("kind")] public string Kind { get; set; } = ""; [JsonPropertyName("isOwned")] public bool IsOwned { get; set; } [JsonPropertyName("objectCount")] public int ObjectCount { get; set; } [JsonPropertyName("employeeCount")] public int EmployeeCount { get; set; } [JsonPropertyName("launderingOperationCount")] public int LaunderingOperationCount { get; set; } } public sealed class BridgeLayoutSnapshot { [JsonPropertyName("properties")] public List<BridgeLayoutProperty> Properties { get; set; } = new List<BridgeLayoutProperty>(); [JsonPropertyName("truncated")] public bool Truncated { get; set; } } public sealed class BridgeLayoutProperty { [JsonPropertyName("propertyCode")] public string PropertyCode { get; set; } = ""; [JsonPropertyName("objects")] public List<BridgeLayoutObject> Objects { get; set; } = new List<BridgeLayoutObject>(); [JsonPropertyName("truncated")] public bool Truncated { get; set; } } public sealed class BridgeLayoutObject { [JsonPropertyName("itemId")] public string? ItemId { get; set; } [JsonPropertyName("gridGuid")] public string? GridGuid { get; set; } [JsonPropertyName("x")] public int? X { get; set; } [JsonPropertyName("y")] public int? Y { get; set; } [JsonPropertyName("rotation")] public int? Rotation { get; set; } [JsonPropertyName("dataType")] public string DataType { get; set; } = ""; } public sealed class BridgeProductsSnapshot { [JsonPropertyName("discoveredCount")] public int DiscoveredCount { get; set; } [JsonPropertyName("discoveredProducts")] public List<string> DiscoveredProducts { get; set; } = new List<string>(); [JsonPropertyName("listedProducts")] public List<string> ListedProducts { get; set; } = new List<string>(); [JsonPropertyName("activeMixOperation")] public BridgeActiveMixOperation? ActiveMixOperation { get; set; } [JsonPropertyName("isMixComplete")] public bool? IsMixComplete { get; set; } [JsonPropertyName("recipeCount")] public int RecipeCount { get; set; } [JsonPropertyName("recipes")] public List<BridgeMixRecipeSummary> Recipes { get; set; } = new List<BridgeMixRecipeSummary>(); [JsonPropertyName("truncated")] public bool Truncated { get; set; } } public sealed class BridgeActiveMixOperation { [JsonPropertyName("productId")] public string? ProductId { get; set; } [JsonPropertyName("ingredientId")] public string? IngredientId { get; set; } } public sealed class BridgeMixRecipeSummary { [JsonPropertyName("product")] public string Product { get; set; } = ""; [JsonPropertyName("mixer")] public string Mixer { get; set; } = ""; [JsonPropertyName("output")] public string Output { get; set; } = ""; } public sealed class BridgeSessionEvent { [JsonPropertyName("eventType")] public string EventType { get; set; } = "heartbeat"; [JsonPropertyName("reason")] public string? Reason { get; set; } } public sealed class BridgePendingMix { [JsonPropertyName("mixId")] public string? MixId { get; set; } [JsonPropertyName("productId")] public string ProductId { get; set; } = ""; [JsonPropertyName("ingredientIds")] public List<string> IngredientIds { get; set; } = new List<string>(); [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("capturedAt")] public string CapturedAt { get; set; } = ""; [JsonPropertyName("source")] public string Source { get; set; } = "inGameTile"; [JsonPropertyName("steps")] public List<BridgePendingMixStep>? Steps { get; set; } } public sealed class BridgePendingMixStep { [JsonPropertyName("ingredientId")] public string IngredientId { get; set; } = ""; [JsonPropertyName("outputProductId")] public string? OutputProductId { get; set; } [JsonPropertyName("outputEffects")] public List<string>? OutputEffects { get; set; } } public static class PendingMixLimits { public const int MaxChainSteps = 64; public const int MaxEffectsPerStep = 12; public const int MaxStringLength = 64; public static string OverLimitReason(int stepCount) { return $"Save/Track unavailable: this mix has {stepCount} steps; the shared limit is {64}."; } } public static class PhoneRecipeLimits { public const int MaxRecipes = 50; public const int MaxChainSteps = 64; public const int MaxEffects = 8; public const int MaxStringLength = 64; public const int MaxBodyBytes = 73728; public const int StaleGraceHours = 24; } public sealed class BridgePhoneRecipe { [JsonPropertyName("recipeId")] public string RecipeId { get; set; } = ""; [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("productId")] public string ProductId { get; set; } = ""; [JsonPropertyName("ingredientIds")] public List<string> IngredientIds { get; set; } = new List<string>(); [JsonPropertyName("effects")] public List<string>? Effects { get; set; } [JsonPropertyName("effectsAvailable")] public bool EffectsAvailable { get; set; } [JsonPropertyName("sellPrice")] public double SellPrice { get; set; } [JsonPropertyName("profit")] public double Profit { get; set; } [JsonPropertyName("profitMargin")] public double ProfitMargin { get; set; } [JsonPropertyName("profileMode")] public string ProfileMode { get; set; } = "standard"; [JsonPropertyName("source")] public string Source { get; set; } = "app-saved"; [JsonPropertyName("publishedAt")] public string PublishedAt { get; set; } = ""; [JsonPropertyName("expiresAt")] public string ExpiresAt { get; set; } = ""; } public sealed class BridgePhoneRecipeSnapshot { [JsonPropertyName("schemaVersion")] public int SchemaVersion { get; set; } = 1; [JsonPropertyName("snapshotId")] public string SnapshotId { get; set; } = ""; [JsonPropertyName("publishedAt")] public string PublishedAt { get; set; } = ""; [JsonPropertyName("expiresAt")] public string ExpiresAt { get; set; } = ""; [JsonPropertyName("omittedCount")] public int OmittedCount { get; set; } [JsonPropertyName("recipes")] public List<BridgePhoneRecipe> Recipes { get; set; } = new List<BridgePhoneRecipe>(); } public sealed class BridgeExchangeRequest { [JsonPropertyName("envelope")] public BridgeEnvelope Envelope { get; set; } = new BridgeEnvelope(); [JsonPropertyName("downlink")] public BridgeExchangeRequestDownlink Downlink { get; set; } = new BridgeExchangeRequestDownlink(); } public sealed class BridgeExchangeRequestDownlink { [JsonPropertyName("phoneRecipes")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public string? PhoneRecipes { get; set; } [JsonPropertyName("advisorDispatches")] [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public string? AdvisorDispatches { get; set; } } public sealed class BridgeExchangeResponse { [JsonPropertyName("ingest")] public BridgeIngestAcknowledgement Ingest { get; set; } = new BridgeIngestAcknowledgement(); [JsonPropertyName("downlink")] public BridgeExchangeResponseDownlink Downlink { get; set; } = new BridgeExchangeResponseDownlink(); [JsonPropertyName("pollHintSeconds")] public int? PollHintSeconds { get; set; } } public sealed class BridgeIngestAcknowledgement { [JsonPropertyName("stored")] public bool Stored { get; set; } [JsonPropertyName("stale")] public bool Stale { get; set; } [JsonPropertyName("sequence")] public long Sequence { get; set; } [JsonPropertyName("currentSequence")] public long? CurrentSequence { get; set; } [JsonPropertyName("receivedAt")] public string? ReceivedAt { get; set; } [JsonPropertyName("gameVersionChanged")] public bool? GameVersionChanged { get; set; } } public sealed class BridgeExchangeResponseDownlink { [JsonPropertyName("phoneRecipes")] public BridgePhoneRecipeExchangeSlice PhoneRecipes { get; set; } = new BridgePhoneRecipeExchangeSlice(); [JsonPropertyName("advisorDispatches")] public BridgeAdvisorDispatchExchangeSlice AdvisorDispatches { get; set; } = new BridgeAdvisorDispatchExchangeSlice(); } public sealed class BridgePhoneRecipeExchangeSlice { [JsonPropertyName("state")] public string State { get; set; } = "unsupported"; [JsonPropertyName("revision")] public string? Revision { get; set; } [JsonPropertyName("payload")] public BridgePhoneRecipeSnapshot? Payload { get; set; } [JsonPropertyName("retryAfterSeconds")] public int? RetryAfterSeconds { get; set; } } public sealed class BridgeAdvisorDispatchExchangeSlice { [JsonPropertyName("state")] public string State { get; set; } = "unsupported"; [JsonPropertyName("revision")] public string? Revision { get; set; } [JsonPropertyName("payload")] public BridgeAdvisorDispatchPayload? Payload { get; set; } [JsonPropertyName("retryAfterSeconds")] public int? RetryAfterSeconds { get; set; } } public sealed class BridgeAdvisorDispatchPayload { [JsonPropertyName("scopeGeneration")] public string ScopeGeneration { get; set; } = ""; [JsonPropertyName("capturedAt")] public string CapturedAt { get; set; } = ""; [JsonPropertyName("dispatches")] public List<BridgeAdvisorDispatchRecord> Dispatches { get; set; } = new List<BridgeAdvisorDispatchRecord>(); } public sealed class BridgeAdvisorDispatchRecord { [JsonPropertyName("id")] public string Id { get; set; } = ""; [JsonPropertyName("createdAt")] public string CreatedAt { get; set; } = ""; [JsonPropertyName("headline")] public string Headline { get; set; } = ""; [JsonPropertyName("body")] public string Body { get; set; } = ""; [JsonPropertyName("personaId")] public string PersonaId { get; set; } = ""; [JsonPropertyName("voiceVersion")] public string VoiceVersion { get; set; } = ""; [JsonPropertyName("triggerId")] public string TriggerId { get; set; } = ""; } public sealed class BridgeExchangeError { [JsonPropertyName("error")] public BridgeStructuredError Error { get; set; } = new BridgeStructuredError(); } public sealed class BridgeStructuredError { [JsonPropertyName("code")] public string Code { get; set; } = ""; [JsonPropertyName("message")] public string Message { get; set; } = ""; [JsonPropertyName("retriable")] public bool Retriable { get; set; } [JsonPropertyName("requiresRepair")] public string? RequiresRepair { get; set; } [JsonPropertyName("retryAfterSeconds")] public int? RetryAfterSeconds { get; set; } } public sealed class BridgeTileChallenge { [JsonPropertyName("version")] public int Version { get; set; } [JsonPropertyName("challenge")] public string Challenge { get; set; } = ""; [JsonPropertyName("tileBuild")] public string TileBuild { get; set; } = ""; [JsonPropertyName("protocolVersion")] public int ProtocolVersion { get; set; } } public static class GameDatasetIds { private static readonly Dictionary<string, string> CanonicalAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["ogkush"] = "og_kush", ["sourdiesel"] = "sour_diesel", ["greencrack"] = "green_crack", ["granddaddypurple"] = "granddaddy_purple", ["shroom"] = "shrooms", ["energydrink"] = "energy_drink", ["motoroil"] = "motor_oil", ["megabean"] = "mega_bean", ["flumedicine"] = "flu_medicine", ["horsesemen"] = "horse_semen", ["viagor"] = "viagra" }; private static readonly Dictionary<string, string> FriendlyLabels = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["og_kush"] = "OG Kush", ["sour_diesel"] = "Sour Diesel", ["green_crack"] = "Green Crack", ["granddaddy_purple"] = "Granddaddy Purple", ["shrooms"] = "Shrooms", ["meth"] = "Meth", ["cocaine"] = "Cocaine", ["flu_medicine"] = "Flu Medicine", ["energy_drink"] = "Energy Drink", ["motor_oil"] = "Motor Oil", ["mega_bean"] = "Mega Bean", ["horse_semen"] = "Horse Semen", ["viagra"] = "Viagor" }; private static readonly HashSet<string> BaseProducts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "og_kush", "sour_diesel", "green_crack", "granddaddy_purple", "meth", "cocaine", "shrooms" }; public static string Canonicalize(string value) { if (string.IsNullOrWhiteSpace(value)) { return value; } string text = value.Trim(); if (!CanonicalAliases.TryGetValue(text, out string value2)) { return text; } return value2; } public static bool IsBaseProduct(string value) { return BaseProducts.Contains(Canonicalize(value)); } public static string FriendlyName(string value) { string text = Canonicalize(value); if (FriendlyLabels.TryGetValue(text, out string value2)) { return value2; } string[] array = text.Replace('_', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { if (array[i].Length != 0) { array[i] = char.ToUpperInvariant(array[i][0]) + array[i].Substring(1); } } return string.Join(" ", array); } } } namespace HylandHelper.BridgeMod { internal enum AdvisorDispatchCacheState { Empty, Fresh, Expired } internal sealed class AdvisorDispatchCacheRead { public AdvisorDispatchCacheState State { get; init; } public string? ScopeGeneration { get; init; } public string? Revision { get; init; } public DateTimeOffset? CapturedAt { get; init; } public List<BridgeAdvisorDispatchRecord> Dispatches { get; init; } = new List<BridgeAdvisorDispatchRecord>(); } internal sealed record AdvisorDispatchCacheCheckpoint(bool Exists, byte[]? Bytes); internal sealed class AdvisorDispatchCache { private sealed class AdvisorDispatchCacheDocument { [JsonPropertyName("version")] public int Version { get; set; } [JsonPropertyName("saveFingerprint")] public string SaveFingerprint { get; set; } = ""; [JsonPropertyName("scopeGeneration")] public string ScopeGeneration { get; set; } = ""; [JsonPropertyName("revision")] public string Revision { get; set; } = ""; [JsonPropertyName("capturedAt")] public string CapturedAt { get; set; } = ""; [JsonPropertyName("dispatches")] public List<BridgeAdvisorDispatchRecord> Dispatches { get; set; } = new List<BridgeAdvisorDispatchRecord>(); } public static readonly TimeSpan DispatchTtl = TimeSpan.FromDays(7.0); private const int MaxCacheBytes = 65536; private const int MaxDispatches = 20; private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = true }; private readonly string _path; private readonly Action<string, string> _replaceFile; private readonly Action<string> _deleteFile; public static string DefaultPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper", "advisor-dispatches-cache.json"); public AdvisorDispatchCache(string path, Action<string, string>? replaceFile = null, Action<string>? deleteFile = null) { _path = path; _replaceFile = replaceFile ?? ((Action<string, string>)delegate(string source, string destination) { File.Move(source, destination, overwrite: true); }); _deleteFile = deleteFile ?? new Action<string>(File.Delete); } public bool Write(BridgeAdvisorDispatchPayload payload, string revision, DateTimeOffset now, string saveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) || !CanWrite(payload, revision, now, out var capturedAt)) { return false; } AdvisorDispatchCacheDocument value = new AdvisorDispatchCacheDocument { Version = 2, SaveFingerprint = saveFingerprint, ScopeGeneration = payload.ScopeGeneration, Revision = revision, CapturedAt = capturedAt.ToString("O"), Dispatches = payload.Dispatches }; return WriteRawAtomic(JsonSerializer.SerializeToUtf8Bytes(value, JsonOptions)); } internal static bool CanWrite(BridgeAdvisorDispatchPayload payload, string revision, DateTimeOffset now) { DateTimeOffset capturedAt; return CanWrite(payload, revision, now, out capturedAt); } private static bool CanWrite(BridgeAdvisorDispatchPayload payload, string revision, DateTimeOffset now, out DateTimeOffset capturedAt) { if (!IsSafePayload(payload, revision, out capturedAt)) { return false; } DateTimeOffset result; return payload.Dispatches.All((BridgeAdvisorDispatchRecord record) => DateTimeOffset.TryParse(record.CreatedAt, out result) && now < result + DispatchTtl); } public AdvisorDispatchCacheRead Read(DateTimeOffset now, string saveFingerprint) { try { if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) || !TryReadDocument(out AdvisorDispatchCacheDocument document) || !string.Equals(document.SaveFingerprint, saveFingerprint, StringComparison.Ordinal)) { return Empty(); } BridgeAdvisorDispatchPayload bridgeAdvisorDispatchPayload = new BridgeAdvisorDispatchPayload { ScopeGeneration = document.ScopeGeneration, CapturedAt = document.CapturedAt, Dispatches = (document.Dispatches ?? new List<BridgeAdvisorDispatchRecord>()) }; if (!IsSafePayload(bridgeAdvisorDispatchPayload, document.Revision, out var capturedAt)) { return Empty(); } DateTimeOffset result; List<BridgeAdvisorDispatchRecord> list = bridgeAdvisorDispatchPayload.Dispatches.Where((BridgeAdvisorDispatchRecord record) => DateTimeOffset.TryParse(record.CreatedAt, out result) && now < result + DispatchTtl).ToList(); return new AdvisorDispatchCacheRead { State = ((bridgeAdvisorDispatchPayload.Dispatches.Count <= 0 || list.Count != 0) ? AdvisorDispatchCacheState.Fresh : AdvisorDispatchCacheState.Expired), ScopeGeneration = bridgeAdvisorDispatchPayload.ScopeGeneration, Revision = document.Revision, CapturedAt = capturedAt, Dispatches = list }; } catch { return Empty(); } } public bool Clear() { try { if (File.Exists(_path)) { _deleteFile(_path); } return !File.Exists(_path); } catch { return false; } } public bool ClearForScope(string saveFingerprint) { try { if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint)) { return false; } if (!File.Exists(_path)) { return true; } if (!TryReadDocument(out AdvisorDispatchCacheDocument document) || !string.Equals(document.SaveFingerprint, saveFingerprint, StringComparison.Ordinal)) { return true; } return Clear(); } catch { return false; } } internal bool TryCapture(out AdvisorDispatchCacheCheckpoint checkpoint) { checkpoint = new AdvisorDispatchCacheCheckpoint(Exists: false, null); try { if (!File.Exists(_path)) { return true; } if (!IsBoundedFile()) { return false; } checkpoint = new AdvisorDispatchCacheCheckpoint(Exists: true, File.ReadAllBytes(_path)); return true; } catch { return false; } } internal bool Restore(AdvisorDispatchCacheCheckpoint checkpoint) { if (!checkpoint.Exists || checkpoint.Bytes == null) { return Clear(); } return WriteRawAtomic(checkpoint.Bytes); } private static AdvisorDispatchCacheRead Empty() { return new AdvisorDispatchCacheRead { State = AdvisorDispatchCacheState.Empty }; } private static bool IsSafePayload(BridgeAdvisorDispatchPayload? payload, string? revision, out DateTimeOffset capturedAt) { capturedAt = default(DateTimeOffset); if (payload == null || !Bounded(payload.ScopeGeneration, 128) || !Bounded(revision, 128) || !DateTimeOffset.TryParse(payload.CapturedAt, out capturedAt) || payload.Dispatches == null || payload.Dispatches.Count > 20) { return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (BridgeAdvisorDispatchRecord dispatch in payload.Dispatches) { if (dispatch == null || !Bounded(dispatch.Id, 64) || !hashSet.Add(dispatch.Id) || !DateTimeOffset.TryParse(dispatch.CreatedAt, out var result) || result > capturedAt || capturedAt - result > DispatchTtl || !Bounded(dispatch.Headline, 60) || !Bounded(dispatch.Body, 512) || !Bounded(dispatch.PersonaId, 64) || !Bounded(dispatch.VoiceVersion, 64) || !Bounded(dispatch.TriggerId, 64)) { return false; } } return true; } private bool WriteRawAtomic(byte[] bytes) { if (bytes.Length == 0 || bytes.Length > 65536) { return false; } if (ExistingBytesEqual(bytes)) { return true; } string text = null; try { string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrWhiteSpace(directoryName)) { Directory.CreateDirectory(directoryName); } text = $"{_path}.{Guid.NewGuid():N}.tmp"; using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { fileStream.Write(bytes, 0, bytes.Length); fileStream.Flush(flushToDisk: true); } _replaceFile(text, _path); text = null; return true; } catch { return false; } finally { if (text != null) { try { File.Delete(text); } catch { } } } } private bool ExistingBytesEqual(byte[] desired) { try { FileInfo fileInfo = new FileInfo(_path); return fileInfo.Exists && fileInfo.Length == desired.Length && File.ReadAllBytes(_path).SequenceEqual(desired); } catch { return false; } } private bool IsBoundedFile() { if (!File.Exists(_path)) { return false; } long length = new FileInfo(_path).Length; if (length > 0) { return length <= 65536; } return false; } private bool TryReadDocument(out AdvisorDispatchCacheDocument document) { document = null; if (!IsBoundedFile()) { return false; } byte[] array = File.ReadAllBytes(_path); using JsonDocument jsonDocument = JsonDocument.Parse(array); if (!HasExactProperties(jsonDocument.RootElement, "version", "saveFingerprint", "scopeGeneration", "revision", "capturedAt", "dispatches")) { return false; } AdvisorDispatchCacheDocument advisorDispatchCacheDocument = JsonSerializer.Deserialize<AdvisorDispatchCacheDocument>(array, JsonOptions); if (advisorDispatchCacheDocument == null || advisorDispatchCacheDocument.Version != 2 || !SaveGenerationIdentity.IsV2Fingerprint(advisorDispatchCacheDocument.SaveFingerprint)) { return false; } document = advisorDispatchCacheDocument; return true; } private static bool HasExactProperties(JsonElement element, params string[] expected) { if (element.ValueKind != JsonValueKind.Object) { return false; } HashSet<string> hashSet = new HashSet<string>(expected, StringComparer.Ordinal); foreach (JsonProperty item in element.EnumerateObject()) { if (!hashSet.Remove(item.Name)) { return false; } } return hashSet.Count == 0; } private static bool Bounded(string? value, int maxLength) { if (!string.IsNullOrWhiteSpace(value)) { return value.Length <= maxLength; } return false; } } internal interface IBridgeCadenceAuthority { bool TryAdmitAdaptiveFull(DateTimeOffset now); bool TryAdmitNonAdaptiveStart(DateTimeOffset now); bool TryAdmitHeartbeat(DateTimeOffset now, out bool adaptive); void NotifyStarted(BridgeEnvelopePlan plan, bool adaptive, DateTimeOffset now); void NotifyCompleted(BridgeEnvelopePlan plan, DateTimeOffset now); } internal sealed class BridgeCadenceController : IBridgeCadenceAuthority { internal static readonly TimeSpan FloorInterval = TimeSpan.FromSeconds(3.0); internal static readonly TimeSpan BurstInterval = TimeSpan.FromSeconds(3.0); internal static readonly TimeSpan BurstMaxDuration = TimeSpan.FromSeconds(30.0); internal static readonly TimeSpan ActiveVisibleInterval = TimeSpan.FromSeconds(7.0); internal static readonly TimeSpan HintLease = TimeSpan.FromSeconds(60.0); internal static readonly TimeSpan RuntimeObservationFloor = TimeSpan.FromSeconds(10.0); internal const int MaxAdaptiveStartsPerWindow = 20; private readonly TimeSpan _idle; private readonly bool _enabled; private DateTimeOffset _lastStart = DateTimeOffset.MinValue; private DateTimeOffset _lastCompleted = DateTimeOffset.MinValue; private DateTimeOffset _burst = DateTimeOffset.MinValue; private DateTimeOffset _lease = DateTimeOffset.MinValue; private DateTimeOffset _lastOverlayFull = DateTimeOffset.MinValue; private bool _visible; private bool _immediate; private readonly Queue<DateTimeOffset> _adaptive = new Queue<DateTimeOffset>(); private string? _facts; private DateTimeOffset? _armed; internal bool HasPendingImmediate => _immediate; internal BridgeCadenceController(TimeSpan idleInterval, bool accelerationEnabled) { _idle = idleInterval; _enabled = accelerationEnabled; } internal void ObserveScreen(bool visible, bool openedTransition, DateTimeOffset now) { if (_enabled) { _visible = visible; if (openedTransition) { _immediate = true; _burst = now; } } } internal void NotifyHint(int? hint, bool acceptedFreshDelivery, DateTimeOffset now) { if (_enabled && acceptedFreshDelivery && hint.HasValue) { int valueOrDefault = hint.GetValueOrDefault(); if (valueOrDefault >= 1 && valueOrDefault <= 60) { _lease = now + HintLease; } } } public bool TryAdmitAdaptiveFull(DateTimeOffset now) { return Admit(now, adaptive: true); } public bool TryAdmitNonAdaptiveStart(DateTimeOffset now) { return Admit(now, adaptive: false); } public bool TryAdmitHeartbeat(DateTimeOffset now, out bool adaptive) { adaptive = false; if (!_enabled) { return now >= _lastCompleted + _idle; } if (_immediate && Admit(now, adaptive: true)) { adaptive = true; return true; } TimeSpan timeSpan = Interval(now); adaptive = timeSpan < _idle; DateTimeOffset dateTimeOffset = (adaptive ? (_lastStart + timeSpan) : (_lastCompleted + _idle)); if (now >= dateTimeOffset) { return Admit(now, adaptive); } return false; } private TimeSpan Interval(DateTimeOffset now) { if (!_visible || !(now - _burst < BurstMaxDuration)) { if (!_visible && !(now < _lease)) { return _idle; } return ActiveVisibleInterval; } return BurstInterval; } private bool Admit(DateTimeOffset now, bool adaptive) { if (!_enabled) { return true; } if (now - _lastStart < FloorInterval) { return false; } while (_adaptive.Count > 0 && now - _adaptive.Peek() >= TimeSpan.FromMinutes(1.0)) { _adaptive.Dequeue(); } if (adaptive) { return _adaptive.Count < 20; } return true; } public void NotifyStarted(BridgeEnvelopePlan plan, bool adaptive, DateTimeOffset now) { if (plan.Kind == BridgeEnvelopeKind.FullSnapshot) { _armed = null; _lastOverlayFull = now; } if (_enabled) { _lastStart = now; if (adaptive) { _adaptive.Enqueue(now); } _immediate = false; } } public void NotifyCompleted(BridgeEnvelopePlan plan, DateTimeOffset now) { _lastCompleted = now; } internal void OnOverlayAccepted(LiveOverlayReadResult result, DateTimeOffset now) { if (result.Status != LiveOverlayReadStatus.Accepted || result.Document == null) { return; } string text = JsonSerializer.Serialize(result.Document.Facts, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); bool flag = text != _facts; _facts = text; if (result.Rotation || flag) { DateTimeOffset valueOrDefault = _armed.GetValueOrDefault(); if (!_armed.HasValue) { valueOrDefault = now; _armed = valueOrDefault; } } } internal bool ShouldRequestRuntimeObservationFull(DateTimeOffset now) { if (!_armed.HasValue || now - _lastOverlayFull < RuntimeObservationFloor) { return false; } _armed = null; return true; } internal void Reset() { _lastStart = (_lastCompleted = (_burst = (_lease = DateTimeOffset.MinValue))); _visible = (_immediate = false); _adaptive.Clear(); _armed = null; _facts = null; } } internal static class AcceptedFreshDelivery { internal static bool Evaluate(BridgeIngestAcknowledgement acknowledgement, BridgeDownlinkApplyOutcome outcome, string? requestKnownRevision, string? responseRevision) { bool flag = acknowledgement.Stored && !acknowledgement.Stale; if (flag) { bool flag2 = (uint)outcome <= 1u; flag = flag2; } if (flag && !string.IsNullOrWhiteSpace(requestKnownRevision) && !string.IsNullOrWhiteSpace(responseRevision)) { return responseRevision != requestKnownRevision; } return false; } } internal sealed class BridgeClient : IDisposable { public const int DefaultMaxExchangeRequestBytes = 1048576; public const int DefaultMaxExchangeResponseBytes = 262144; private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNameCaseInsensitive = true }; private readonly HttpClient _httpClient; private readonly Uri _baseUri; private readonly MelonBridgeLogger _logger; private readonly int _maxExchangeRequestBytes; private readonly int _maxExchangeResponseBytes; public BridgeClient(string baseUrl, MelonBridgeLogger logger) : this(baseUrl, logger, null) { } internal BridgeClient(string baseUrl, MelonBridgeLogger logger, HttpMessageHandler? handler, int maxExchangeRequestBytes = 1048576, int maxExchangeResponseBytes = 262144) { if (maxExchangeRequestBytes <= 0 || maxExchangeResponseBytes <= 0) { throw new ArgumentOutOfRangeException("maxExchangeRequestBytes"); } _baseUri = new Uri(baseUrl.TrimEnd('/') + "/", UriKind.Absolute); _logger = logger; _maxExchangeRequestBytes = maxExchangeRequestBytes; _maxExchangeResponseBytes = maxExchangeResponseBytes; _httpClient = ((handler == null) ? new HttpClient() : new HttpClient(handler)); _httpClient.Timeout = TimeSpan.FromSeconds(15.0); } public async Task<BridgeExchangeResponse> ExchangeAsync(string bridgeToken, BridgeExchangeRequest exchange, CancellationToken cancellationToken) { string text = JsonSerializer.Serialize(exchange, JsonOptions); if (Encoding.UTF8.GetByteCount(text) > _maxExchangeRequestBytes) { throw new BridgePayloadException("Bridge exchange request exceeded the local byte limit."); } using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, new Uri(_baseUri, "api/bridge/exchange")) { Content = new StringContent(text, Encoding.UTF8, "application/json") }; request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bridgeToken); using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); TimeSpan? retryAfter = ReadRetryAfter(response); byte[] array = await ReadBoundedBodyAsync(response, _maxExchangeResponseBytes, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (!response.IsSuccessStatusCode) { throw CreateExchangeException(response.StatusCode, array, retryAfter); } try { return JsonSerializer.Deserialize<BridgeExchangeResponse>(array, JsonOptions) ?? throw new BridgePayloadException("Bridge exchange response was empty."); } catch (JsonException innerException) { throw new BridgePayloadException("Bridge exchange response was malformed.", innerException); } } public async Task<PendingMixPostResponse?> SendPendingMixAsync(string bridgeToken, BridgePendingMix mix, CancellationToken cancellationToken) { try { return await PostJsonAsync<PendingMixPostResponse>("api/bridge/mixes", mix, bridgeToken, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (BridgeClientException ex) when (ex.IsUnauthorized) { throw; } catch (OperationCanceledException exception) when (!cancellationToken.IsCancellationRequested) { _logger.WarningThrottled("Pending mix POST quiet-failed.", exception); return null; } catch (Exception ex2) when (!(ex2 is OperationCanceledException)) { _logger.WarningThrottled("Pending mix POST quiet-failed.", ex2); return null; } } public async Task<PairingSession> StartPairingAsync(BridgeSaveFingerprint saveFingerprint, CancellationToken cancellationToken) { PairingStartResponse pairingStartResponse = await PostJsonAsync<PairingStartResponse>("api/bridge/pairing/start", new { saveFingerprint }, null, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (string.IsNullOrWhiteSpace(pairingStartResponse.PairingId) || string.IsNullOrWhiteSpace(pairingStartResponse.Code) || string.IsNullOrWhiteSpace(pairingStartResponse.ModSecret)) { throw new InvalidOperationException("Pairing start response was incomplete."); } return new PairingSession(pairingStartResponse.PairingId, pairingStartResponse.Code, pairingStartResponse.ModSecret); } public async Task<PairingStatusResponse> PollPairingStatusAsync(PairingSession session, CancellationToken cancellationToken) { string path = "api/bridge/pairing/" + Uri.EscapeDataString(session.PairingId) + "/status"; PairingStatusResponse pairingStatusResponse = await PostJsonAsync<PairingStatusResponse>(path, new { modSecret = session.ModSecret }, null, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (!string.IsNullOrWhiteSpace(pairingStatusResponse.BridgeToken)) { pairingStatusResponse.VerifiedScope = pairingStatusResponse.Scope?.ToPairingScope(session.PairingId) ?? throw new InvalidOperationException("Claimed pairing response omitted its verified scope."); } return pairingStatusResponse; } public Task SendHeartbeatAsync(string bridgeToken, BridgeEnvelope heartbeat, CancellationToken cancellationToken) { return PostJsonAsync<object>("api/bridge/ingest", heartbeat, bridgeToken, cancellationToken); } public async Task<LatestStateResponse?> GetLatestAsync(string bridgeToken, PairingScope scope, CancellationToken cancellationToken) { string relativeUri = $"api/bridge/latest?pairingId={Uri.EscapeDataString(scope.PairingId)}&deviceId={Uri.EscapeDataString(scope.DeviceId)}&profileId={Uri.EscapeDataString(scope.ProfileId)}"; try { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, relativeUri)); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bridgeToken); using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (response.StatusCode == HttpStatusCode.Unauthorized) { throw new BridgeClientException(HttpStatusCode.Unauthorized, "Latest poll was unauthorized."); } if (!response.IsSuccessStatusCode) { _logger.WarningThrottled($"Latest poll returned {response.StatusCode}."); return null; } return JsonSerializer.Deserialize<LatestStateResponse>(await ReadBoundedBodyAsync(response, _maxExchangeResponseBytes, cancellationToken).ConfigureAwait(continueOnCapturedContext: false), JsonOptions); } catch (BridgeClientException ex) when (ex.IsUnauthorized) { throw; } catch (Exception ex2) when (!(ex2 is OperationCanceledException)) { _logger.WarningThrottled("Latest poll quiet-failed.", ex2); return null; } } public async Task<PhoneRecipePollResult?> GetPhoneRecipeAvailabilityAsync(string bridgeToken, CancellationToken cancellationToken) { _ = 1; try { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, "api/bridge/recipes/availability")); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bridgeToken); using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (response.StatusCode == HttpStatusCode.Unauthorized) { throw new BridgeClientException(HttpStatusCode.Unauthorized, "Phone recipe poll was unauthorized."); } if (response.StatusCode == HttpStatusCode.NotFound) { return new PhoneRecipePollResult(null, NotPublished: true); } if (!response.IsSuccessStatusCode) { _logger.WarningThrottled($"Phone recipe poll returned {response.StatusCode}."); return null; } BridgePhoneRecipeSnapshot bridgePhoneRecipeSnapshot = JsonSerializer.Deserialize<BridgePhoneRecipeSnapshot>(await ReadBoundedBodyAsync(response, _maxExchangeResponseBytes, cancellationToken).ConfigureAwait(continueOnCapturedContext: false), JsonOptions); return (bridgePhoneRecipeSnapshot == null) ? null : new PhoneRecipePollResult(bridgePhoneRecipeSnapshot, NotPublished: false); } catch (BridgeClientException ex) when (ex.IsUnauthorized) { throw; } catch (Exception ex2) when (!(ex2 is OperationCanceledException)) { _logger.WarningThrottled("Phone recipe poll quiet-failed.", ex2); return null; } } public void Dispose() { _httpClient.Dispose(); } private async Task<T> PostJsonAsync<T>(string path, object body, string? bearerToken, CancellationToken cancellationToken) { string content = JsonSerializer.Serialize(body, JsonOptions); using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, new Uri(_baseUri, path)) { Content = new StringContent(content, Encoding.UTF8, "application/json") }; if (!string.IsNullOrWhiteSpace(bearerToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); } using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (!response.IsSuccessStatusCode) { throw new BridgeClientException(response.StatusCode, $"{path} returned HTTP {response.StatusCode}."); } if (typeof(T) == typeof(object)) { return (T)new object(); } return JsonSerializer.Deserialize<T>(await ReadBoundedBodyAsync(response, _maxExchangeResponseBytes, cancellationToken).ConfigureAwait(continueOnCapturedContext: false), JsonOptions) ?? throw new InvalidOperationException(path + " returned an empty response."); } private static BridgeClientException CreateExchangeException(HttpStatusCode statusCode, byte[] body, TimeSpan? retryAfter) { BridgeStructuredError bridgeStructuredError = null; try { BridgeExchangeError bridgeExchangeError = JsonSerializer.Deserialize<BridgeExchangeError>(body, JsonOptions); if (bridgeExchangeError?.Error != null && IsSafeErrorCode(bridgeExchangeError.Error.Code)) { bridgeStructuredError = bridgeExchangeError.Error; } } catch (JsonException) { } string text = bridgeStructuredError?.Code; string requiresRepair = (IsBounded(bridgeStructuredError?.RequiresRepair, 64) ? bridgeStructuredError.RequiresRepair : null); int? num = bridgeStructuredError?.RetryAfterSeconds; TimeSpan? obj; if (num.HasValue) { int valueOrDefault = num.GetValueOrDefault(); if (valueOrDefault >= 0 && valueOrDefault <= 86400) { obj = TimeSpan.FromSeconds(bridgeStructuredError.RetryAfterSeconds.Value); goto IL_00cd; } } obj = null; goto IL_00cd; IL_00cd: TimeSpan? timeSpan = obj; string value = ((text == null) ? "HTTP_ERROR" : text); return new BridgeClientException(statusCode, $"Bridge exchange failed with HTTP {statusCode} ({value}).", text, bridgeStructuredError?.Retriable ?? false, requiresRepair, retryAfter ?? timeSpan); } private static bool IsSafeErrorCode(string? code) { switch (code) { case "SAVE_SCOPE_MISMATCH": case "BRIDGE_TOKEN_INVALID": case "PAIRING_SCOPE_MISMATCH": case "ENTITLEMENT_PROOF_REQUIRED": case "RATE_LIMITED": return true; default: return false; } } private static bool IsBounded(string? value, int maxLength) { if (!string.IsNullOrWhiteSpace(value)) { return value.Length <= maxLength; } return false; } private static TimeSpan? ReadRetryAfter(HttpResponseMessage response) { RetryConditionHeaderValue retryAfter = response.Headers.RetryAfter; TimeSpan? timeSpan = retryAfter?.Delta; if (timeSpan.HasValue) { return timeSpan.GetValueOrDefault(); } DateTimeOffset? dateTimeOffset = retryAfter?.Date; if (dateTimeOffset.HasValue) { DateTimeOffset valueOrDefault = dateTimeOffset.GetValueOrDefault(); TimeSpan timeSpan2 = valueOrDefault - DateTimeOffset.UtcNow; return (timeSpan2 > TimeSpan.Zero) ? timeSpan2 : TimeSpan.Zero; } return null; } private static async Task<byte[]> ReadBoundedBodyAsync(HttpResponseMessage response, int maxBytes, CancellationToken cancellationToken) { long? contentLength = response.Content.Headers.ContentLength; if (contentLength.HasValue && contentLength.GetValueOrDefault() > 0 && response.Content.Headers.ContentLength > maxBytes) { throw new BridgePayloadException("Bridge response exceeded the local byte limit."); } byte[] result; await using (Stream source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) { using MemoryStream destination = new MemoryStream(); byte[] buffer = new byte[Math.Min(8192, maxBytes + 1)]; while (true) { int num = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (num == 0) { break; } if (destination.Length + num > maxBytes) { throw new BridgePayloadException("Bridge response exceeded the local byte limit."); } destination.Write(buffer, 0, num); } result = destination.ToArray(); } return result; } } internal sealed class BridgeClientException : Exception { public HttpStatusCode StatusCode { get; } public string? Code { get; } public bool Retriable { get; } public string? RequiresRepair { get; } public TimeSpan? RetryAfter { get; } public bool IsUnauthorized => StatusCode == HttpStatusCode.Unauthorized; public bool IsUnsupported => StatusCode == HttpStatusCode.NotFound; public bool IsTransient { get { if (!Retriable && StatusCode != HttpStatusCode.TooManyRequests) { return StatusCode >= HttpStatusCode.InternalServerError; } return true; } } public BridgeClientException(HttpStatusCode statusCode, string message, string? code = null, bool retriable = false, string? requiresRepair = null, TimeSpan? retryAfter = null) : base(message) { StatusCode = statusCode; Code = code; Retriable = retriable; RequiresRepair = requiresRepair; RetryAfter = retryAfter; } } internal sealed class BridgePayloadException : Exception { public BridgePayloadException(string message) : base(message) { } public BridgePayloadException(string message, Exception innerException) : base(message, innerException) { } } internal sealed record PhoneRecipePollResult(BridgePhoneRecipeSnapshot? Snapshot, bool NotPublished); internal sealed record PairingSession(string PairingId, string Code, string ModSecret); internal sealed class PairingStartResponse { [JsonPropertyName("pairingId")] public string? PairingId { get; set; } [JsonPropertyName("code")] public string? Code { get; set; } [JsonPropertyName("modSecret")] public string? ModSecret { get; set; } } internal sealed class PairingStatusResponse { [JsonPropertyName("status")] public string Status { get; set; } = ""; [JsonPropertyName("bridgeToken")] public string? BridgeToken { get; set; } [JsonPropertyName("scope")] public PairingScopeResponse? Scope { get; set; } [JsonIgnore] public PairingScope? VerifiedScope { get; set; } } internal sealed class PairingScopeResponse { [JsonPropertyName("deviceId")] public string DeviceId { get; set; } = ""; [JsonPropertyName("profileId")] public string ProfileId { get; set; } = ""; [JsonPropertyName("saveFingerprint")] public BridgeSaveFingerprint? SaveFingerprint { get; set; } public PairingScope ToPairingScope(string pairingId) { PairingScope obj = new PairingScope { PairingId = pairingId, DeviceId = DeviceId, ProfileId = ProfileId, SaveFingerprint = (SaveFingerprint ?? new BridgeSaveFingerprint()) }; if (!obj.IsValid()) { throw new InvalidOperationException("Claimed pairing response contained an invalid verified scope."); } return obj; } } internal sealed class LatestStateResponse { [JsonPropertyName("sequence")] public long? Sequence { get; set; } } internal sealed class PendingMixPostResponse { [JsonPropertyName("accepted")] public bool Accepted { get; set; } [JsonPropertyName("deduped")] public bool Deduped { get; set; } [JsonPropertyName("mixId")] public string? MixId { get; set; } } internal sealed class BridgeCommandReader { private const long MaxCommandBytes = 8192L; private readonly MelonBridgeLogger _logger; private readonly string _path; private static readonly Regex IsoTimestampPattern = new Regex("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$", RegexOptions.CultureInvariant); private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public BridgeCommandReader(MelonBridgeLogger logger) { _logger = logger; _path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper", "bridge-command.json"); } public BridgeCommand? TakeCommand() { try { if (!File.Exists(_path)) { return null; } BridgeCommand command = null; try { if (new FileInfo(_path).Length <= 8192) { if (!TryParseCommand(File.ReadAllText(_path), out command)) { command = null; } } else { _logger.WarningThrottled("Bridge command file exceeded the safety limit; discarding."); } } catch (Exception exception) { _logger.WarningThrottled("Bridge command file was unreadable; discarding.", exception); } try { File.Delete(_path); } catch { } return command; } catch (Exception exception2) { _logger.WarningThrottled("Bridge command check failed.", exception2); return null; } } public bool TakeUnlink() { try { if (!File.Exists(_path)) { return false; } string a = null; BridgeCommand command = null; try { if (new FileInfo(_path).Length <= 8192 && TryParseCommand(File.ReadAllText(_path), out command)) { a = command?.Command; } } catch (Exception exception) { _logger.WarningThrottled("Bridge command file was unreadable; discarding.", exception); } if (IsSupported(command) && string.Equals(a, "saveMix", StringComparison.OrdinalIgnoreCase)) { return false; } try { File.Delete(_path); } catch { } return string.Equals(a, "unlink", StringComparison.OrdinalIgnoreCase); } catch (Exception exception2) { _logger.WarningThrottled("Bridge unlink command check failed.", exception2); return false; } } private static bool IsSupported(BridgeCommand? command) { if (command == null || string.IsNullOrWhiteSpace(command.Command)) { return false; } if (string.Equals(command.Command, "unlink", StringComparison.OrdinalIgnoreCase)) { return true; } if (!SaveGenerationIdentity.IsV2Fingerprint(command.SaveFingerprint)) { return false; } if (string.Equals(command.Command, "syncNow", StringComparison.OrdinalIgnoreCase)) { if (command.Version == 1 && IsSafeSyncRequestId(command.RequestId) && IsUtcIsoTimestamp(command.RequestedAt)) { return command.Mix == null; } return false; } if (!string.Equals(command.Command, "saveMix", StringComparison.OrdinalIgnoreCase) || !IsSafeRequestId(command.RequestId) || command.Mix == null) { return false; } BridgePendingMix mix = command.Mix; if (!IsBoundedString(mix.ProductId) || mix.IngredientIds == null || mix.IngredientIds.Count == 0 || mix.IngredientIds.Count > 64 || mix.IngredientIds.Exists((string id) => !IsBoundedString(id)) || (mix.Name != null && mix.Name.Length > 64) || !IsIsoTimestamp(mix.CapturedAt) || !string.Equals(mix.Source, "inGameTile", StringComparison.Ordinal) || mix.Steps == null || mix.Steps.Count != mix.IngredientIds.Count) { return false; } for (int num = 0; num < mix.Steps.Count; num++) { BridgePendingMixStep bridgePendingMixStep = mix.Steps[num]; if (bridgePendingMixStep == null || !IsBoundedString(bridgePendingMixStep.IngredientId) || !string.Equals(bridgePendingMixStep.IngredientId, mix.IngredientIds[num], StringComparison.OrdinalIgnoreCase) || !IsBoundedString(bridgePendingMixStep.OutputProductId) || bridgePendingMixStep.OutputEffects == null || bridgePendingMixStep.OutputEffects.Count > 12 || bridgePendingMixStep.OutputEffects.Exists((string effect) => !IsBoundedString(effect))) { return false; } } return true; } private static bool TryParseCommand(string json, out BridgeCommand? command) { command = null; using JsonDocument jsonDocument = JsonDocument.Parse(json); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object || !rootElement.TryGetProperty("command", out var value) || value.ValueKind != JsonValueKind.String) { return false; } string[] array = value.GetString()?.ToLowerInvariant() switch { "unlink" => new string[1] { "command" }, "syncnow" => new string[5] { "command", "version", "saveFingerprint", "requestId", "requestedAt" }, "savemix" => new string[4] { "command", "saveFingerprint", "requestId", "mix" }, _ => Array.Empty<string>(), }; if (array.Length == 0 || !HasExactFields(rootElement, (IReadOnlyCollection<string>)(object)array)) { return false; } command = JsonSerializer.Deserialize<BridgeCommand>(json, JsonOptions); return IsSupported(command); } private static bool HasExactFields(JsonElement root, IReadOnlyCollection<string> expected) { HashSet<string> hashSet = new HashSet<string>(expected, StringComparer.Ordinal); foreach (JsonProperty item in root.EnumerateObject()) { if (!hashSet.Remove(item.Name)) { return false; } } return hashSet.Count == 0; } private static bool IsSafeRequestId(string? requestId) { if (string.IsNullOrWhiteSpace(requestId) || requestId.Length > 64) { return false; } foreach (char c in requestId) { if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') { return false; } } return true; } private static bool IsSafeSyncRequestId(string? requestId) { if (requestId == null || requestId.Length != 32) { return false; } for (int i = 0; i < requestId.Length; i++) { if (!char.IsLetterOrDigit(requestId[i])) { return false; } } return true; } private static bool IsBoundedString(string? value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Length <= 64; } return false; } private static bool IsIsoTimestamp(string? value) { DateTimeOffset result; if (!string.IsNullOrWhiteSpace(value) && value.Length >= 20 && value.Length <= 40 && IsoTimestampPattern.IsMatch(value)) { return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out result); } return false; } private static bool IsUtcIsoTimestamp(string? value) { if (IsIsoTimestamp(value)) { return value.EndsWith("Z", StringComparison.Ordinal); } return false; } } internal sealed class BridgeCommand { [JsonPropertyName("command")] public string? Command { get; set; } [JsonPropertyName("version")] public int? Version { get; set; } [JsonPropertyName("saveFingerprint")] public string? SaveFingerprint { get; set; } [JsonPropertyName("requestId")] public string? RequestId { get; set; } [JsonPropertyName("requestedAt")] public string? RequestedAt { get; set; } [JsonPropertyName("mix")] public BridgePendingMix? Mix { get; set; } } internal sealed class BridgeCommandResultWriter { private readonly MelonBridgeLogger _logger; private readonly string _directory; public BridgeCommandResultWriter(MelonBridgeLogger logger) { _logger = logger; _directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper"); try { Directory.CreateDirectory(_directory); } catch { } } public void Write(string requestId, string status, string saveFingerprint) { if (!IsSafeRequestId(requestId) || string.IsNullOrWhiteSpace(status) || !SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint)) { return; } string text = Path.Combine(_directory, "bridge-command-result-" + requestId + ".json"); string text2 = $"{text}.{Guid.NewGuid():N}.tmp"; try { Directory.CreateDirectory(_directory); File.WriteAllText(text2, JsonSerializer.Serialize(new { version = 1, requestId = requestId, status = status, saveFingerprint = saveFingerprint })); File.Move(text2, text, overwrite: true); } catch (Exception exception) { try { if (File.Exists(text2)) { File.Delete(text2); } } catch { } _logger.WarningThrottled("Bridge command result write failed.", exception); } } private static bool IsSafeRequestId(string requestId) { if (string.IsNullOrWhiteSpace(requestId) || requestId.Length > 64) { return false; } foreach (char c in requestId) { if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') { return false; } } return true; } } internal static class BridgeConstants { public const string BackendBaseUrl = "https://hyland-helper-backend.vercel.app"; public const string ModVersion = "0.1.3"; public const string TargetGameVersion = "0.4.5f2"; public const string TargetMelonLoaderVersion = "0.7.3"; public static readonly TimeSpan PairingPollInterval = TimeSpan.FromSeconds(5.0); public static readonly TimeSpan SnapshotInterval = TimeSpan.FromSeconds(30.0); public static readonly TimeSpan LatestPollInterval = TimeSpan.FromSeconds(30.0); public static readonly TimeSpan PhoneRecipePollInterval = TimeSpan.FromSeconds(30.0); public static readonly TimeSpan RetryBackoff = TimeSpan.FromSeconds(10.0); } internal enum BridgeDownlinkApplyOutcome { Applied, Cleared, Unchanged, NotPublished, TemporarilyUnavailable, Unsupported, NotConsented, EntitlementUnavailable, Malformed, PersistenceFailed } internal sealed class BridgeDownlinkCoordinator { private readonly PhoneRecipeCache _phoneRecipes; private readonly BridgeSettingsStore _settingsStore; private readonly BridgeLocalSettings _settings; private readonly Func<DateTimeOffset> _utcNow; private readonly AdvisorDispatchCache? _advisorDispatches; public bool PhoneRecipesRequireLegacyPoll { get; private set; } public bool AdvisorDispatchesSupported { get; private set; } = true; public BridgeDownlinkCoordinator(PhoneRecipeCache phoneRecipes, BridgeSettingsStore settingsStore, BridgeLocalSettings settings, Func<DateTimeOffset> utcNow, AdvisorDispatchCache? advisorDispatches = null) { _phoneRecipes = phoneRecipes; _settingsStore = settingsStore; _settings = settings; _utcNow = utcNow; _advisorDispatches = advisorDispatches; } public void PopulateKnownRevisions(BridgeExchangeRequestDownlink downlink) { downlink.PhoneRecipes = _settings.PhoneRecipesRevision; downlink.AdvisorDispatches = _settings.AdvisorDispatchesRevision; } public BridgeDownlinkApplyOutcome ApplyPhoneRecipes(BridgePhoneRecipeExchangeSlice? slice, string saveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) || slice == null || string.IsNullOrWhiteSpace(slice.State)) { return BridgeDownlinkApplyOutcome.Malformed; } switch (slice.State) { case "changed": if (!ValidRevision(slice.Revision) || slice.Payload == null) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = false; return ReplacePhoneRecipes(slice.Payload, slice.Revision, saveFingerprint); case "unchanged": if (!ValidRevision(slice.Revision) || !string.Equals(slice.Revision, _settings.PhoneRecipesRevision, StringComparison.Ordinal)) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = false; return BridgeDownlinkApplyOutcome.Unchanged; case "cleared": if (!ValidRevision(slice.Revision)) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = false; return ClearPhoneRecipes(slice.Revision, saveFingerprint); case "notPublished": if (slice.Revision != null || slice.Payload != null) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = false; return BridgeDownlinkApplyOutcome.NotPublished; case "temporarilyUnavailable": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision)) || !ValidRetryAfter(slice.RetryAfterSeconds)) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = false; _phoneRecipes.MarkOffline(_utcNow(), saveFingerprint); return BridgeDownlinkApplyOutcome.TemporarilyUnavailable; case "unsupported": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision)) || slice.RetryAfterSeconds.HasValue) { return BridgeDownlinkApplyOutcome.Malformed; } PhoneRecipesRequireLegacyPoll = true; return BridgeDownlinkApplyOutcome.Unsupported; default: return BridgeDownlinkApplyOutcome.Malformed; } } public BridgeDownlinkApplyOutcome ApplyAdvisorDispatches(BridgeAdvisorDispatchExchangeSlice? slice, string saveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) || _advisorDispatches == null || slice == null || string.IsNullOrWhiteSpace(slice.State)) { return BridgeDownlinkApplyOutcome.Malformed; } switch (slice.State) { case "changed": if (!ValidRevision(slice.Revision) || slice.Payload == null) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return ReplaceAdvisorDispatches(slice.Payload, slice.Revision, saveFingerprint); case "unchanged": if (!ValidRevision(slice.Revision) || !string.Equals(slice.Revision, _settings.AdvisorDispatchesRevision, StringComparison.Ordinal)) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return BridgeDownlinkApplyOutcome.Unchanged; case "cleared": if (!ValidRevision(slice.Revision)) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return ClearAdvisorDispatches(slice.Revision, saveFingerprint); case "notConsented": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision))) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return BridgeDownlinkApplyOutcome.NotConsented; case "entitlementUnavailable": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision))) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return BridgeDownlinkApplyOutcome.EntitlementUnavailable; case "temporarilyUnavailable": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision)) || !ValidRetryAfter(slice.RetryAfterSeconds)) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = true; return BridgeDownlinkApplyOutcome.TemporarilyUnavailable; case "unsupported": if (slice.Payload != null || (slice.Revision != null && !ValidRevision(slice.Revision)) || slice.RetryAfterSeconds.HasValue) { return BridgeDownlinkApplyOutcome.Malformed; } AdvisorDispatchesSupported = false; return BridgeDownlinkApplyOutcome.Unsupported; default: return BridgeDownlinkApplyOutcome.Malformed; } } public void ClearTokenScopedState() { _phoneRecipes.Clear(); _advisorDispatches?.Clear(); PhoneRecipesRequireLegacyPoll = false; AdvisorDispatchesSupported = true; } private BridgeDownlinkApplyOutcome ReplacePhoneRecipes(BridgePhoneRecipeSnapshot snapshot, string revision, string saveFingerprint) { if (!_phoneRecipes.TryCapture(out PhoneRecipeCacheCheckpoint checkpoint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } if (!_phoneRecipes.Write(snapshot, _utcNow(), saveFingerprint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } return PersistPhoneRevision(revision, checkpoint, BridgeDownlinkApplyOutcome.Applied); } private BridgeDownlinkApplyOutcome ClearPhoneRecipes(string revision, string saveFingerprint) { if (!_phoneRecipes.TryCapture(out PhoneRecipeCacheCheckpoint checkpoint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } if (!_phoneRecipes.ClearForScope(saveFingerprint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } return PersistPhoneRevision(revision, checkpoint, BridgeDownlinkApplyOutcome.Cleared); } private BridgeDownlinkApplyOutcome PersistPhoneRevision(string revision, PhoneRecipeCacheCheckpoint checkpoint, BridgeDownlinkApplyOutcome success) { string phoneRecipesRevision = _settings.PhoneRecipesRevision; _settings.PhoneRecipesRevision = revision; if (_settingsStore.Save(_settings)) { return success; } _settings.PhoneRecipesRevision = phoneRecipesRevision; _phoneRecipes.Restore(checkpoint); return BridgeDownlinkApplyOutcome.PersistenceFailed; } private BridgeDownlinkApplyOutcome ReplaceAdvisorDispatches(BridgeAdvisorDispatchPayload payload, string revision, string saveFingerprint) { if (!AdvisorDispatchCache.CanWrite(payload, revision, _utcNow())) { return BridgeDownlinkApplyOutcome.Malformed; } if (_advisorDispatches == null || !_advisorDispatches.TryCapture(out AdvisorDispatchCacheCheckpoint checkpoint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } if (!_advisorDispatches.Write(payload, revision, _utcNow(), saveFingerprint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } return PersistAdvisorRevision(revision, checkpoint, BridgeDownlinkApplyOutcome.Applied); } private BridgeDownlinkApplyOutcome ClearAdvisorDispatches(string revision, string saveFingerprint) { if (_advisorDispatches == null || !_advisorDispatches.TryCapture(out AdvisorDispatchCacheCheckpoint checkpoint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } if (!_advisorDispatches.ClearForScope(saveFingerprint)) { return BridgeDownlinkApplyOutcome.PersistenceFailed; } return PersistAdvisorRevision(revision, checkpoint, BridgeDownlinkApplyOutcome.Cleared); } private BridgeDownlinkApplyOutcome PersistAdvisorRevision(string revision, AdvisorDispatchCacheCheckpoint checkpoint, BridgeDownlinkApplyOutcome success) { string advisorDispatchesRevision = _settings.AdvisorDispatchesRevision; _settings.AdvisorDispatchesRevision = revision; if (_settingsStore.Save(_settings)) { return success; } _settings.AdvisorDispatchesRevision = advisorDispatchesRevision; _advisorDispatches.Restore(checkpoint); return BridgeDownlinkApplyOutcome.PersistenceFailed; } private static bool ValidRevision(string? revision) { return PairingScope.IsBounded(revision, 128); } private static bool ValidRetryAfter(int? retryAfterSeconds) { if (retryAfterSeconds.HasValue) { if (retryAfterSeconds.HasValue) { int valueOrDefault = retryAfterSeconds.GetValueOrDefault(); if (valueOrDefault >= 0) { return valueOrDefault <= 86400; } } return false; } return true; } } internal enum BridgeExchangeMode { Negotiating, Exchange, Legacy, Repair } internal sealed record BridgeAcknowledgementResult(long SequenceFloor, bool RequiresFullResynchronization); internal sealed class BridgeExchangeState { private DateTimeOffset _retryAt = DateTimeOffset.MinValue; private int _attempt; public BridgeExchangeMode Mode { get; private set; } public bool CanSendHeartbeat => Mode == BridgeExchangeMode.Exchange; public bool CanAttemptExchange(DateTimeOffset now) { BridgeExchangeMode mode = Mode; if ((uint)mode <= 1u) { return now >= _retryAt; } return false; } public BridgeAcknowledgementResult ApplySuccess(BridgeIngestAcknowledgement acknowledgement, long localSequence, DateTimeOffset now) { Mode = BridgeExchangeMode.Exchange; _attempt = 0; _retryAt = DateTimeOffset.MinValue; return new BridgeAcknowledgementResult(Math.Max(localSequence, Math.Max(acknowledgement.Sequence, acknowledgement.CurrentSequence.GetValueOrDefault())), acknowledgement.Stale || !acknowledgement.Stored); } public BridgeRetryDecision ApplyFailure(Exception exception, BridgeRetryPolicy policy, DateTimeOffset now) { BridgeRetryDecision bridgeRetryDecision = policy.Decide(exception, _attempt, now); switch (bridgeRetryDecision.Action) { case BridgeRetryAction.StickyLegacy: Mode = BridgeExchangeMode.Legacy; _retryAt = DateTimeOffset.MaxValue; break; case BridgeRetryAction.Repair: Mode = BridgeExchangeMode.Repair; _retryAt = DateTimeOffset.MaxValue; break; case BridgeRetryAction.Retry: case BridgeRetryAction.Resynchronize: _retryAt = now + (bridgeRetryDecision.Delay ?? BridgeRetryPolicy.MinDelay); _attempt++; break; } return bridgeRetryDecision; } public void EnterRepair() { Mode = BridgeExchangeMode.Repair; _retryAt = DateTimeOffset.MaxValue; } public void Reset() { Mode = BridgeExchangeMode.Negotiating; _retryAt = DateTimeOffset.MinValue; _attempt = 0; } } public static class BridgeFeatureAdvertisement { public const string PhoneRecipeAvailability = "phoneRecipeAvailability"; public const int PhoneRecipeProtocolVersion = 1; public const string BridgeExchange = "bridgeExchange"; public const string AdvisorDispatches = "advisorDispatches"; public const int ExchangeProtocolVersion = 1; public const int AdvisorDispatchProtocolVersion = 1; public static Dictionary<string, BridgeFeatureCapability> Current(bool exchangeOperational = false, bool advisorOperational = false) { Dictionary<string, BridgeFeatureCapability> dictionary = new Dictionary<string, BridgeFeatureCapability> { ["phoneRecipeAvailability"] = new BridgeFeatureCapability { Supported = true, ProtocolVersion = 1 } }; if (exchangeOperational) { dictionary["bridgeExchange"] = new BridgeFeatureCapability { Supported = true, ProtocolVersion = 1 }; } if (exchangeOperational && advisorOperational) { dictionary["advisorDispatches"] = new BridgeFeatureCapability { Supported = true, ProtocolVersion = 1 }; } return dictionary; } } internal sealed class BridgeOperationalCapabilityState { private bool _exchangeOperational; private bool _advisorOperational; public bool Update(BridgeExchangeMode mode, bool tileReady, bool advisorAvailable = true) { bool flag = mode == BridgeExchangeMode.Exchange; bool flag2 = flag && tileReady && advisorAvailable; if (flag == _exchangeOperational && flag2 == _advisorOperational) { return false; } _exchangeOperational = flag; _advisorOperational = flag2; return true; } public Dictionary<string, BridgeFeatureCapability> Current() { return BridgeFeatureAdvertisement.Current(_exchangeOperational, _advisorOperational); } } public sealed class HylandHelperBridgeMod : MelonMod { private readonly OverlayState _overlay = new OverlayState(); private CancellationTokenSource? _cts; private BridgeRunner? _runner; public override void OnInitializeMelon() { try { _cts = new CancellationTokenSource(); MelonBridgeLogger logger = new MelonBridgeLogger(((MelonBase)this).LoggerInstance); BridgeOperationalCapabilityState bridgeOperationalCapabilityState = new BridgeOperationalCapabilityState(); GameAdapter gameAdapter = new GameAdapter(logger, bridgeOperationalCapabilityState.Current); BridgeSettingsStore settingsStore = BridgeSettingsStore.Create(logger); BridgeClient bridgeClient = new BridgeClient("https://hyland-helper-backend.vercel.app", logger); _runner = new BridgeRunner(gameAdapter, bridgeClient, settingsStore, _overlay, logger, bridgeOperationalCapabilityState); _runner.RunAsync(_cts.Token); _overlay.SetStatus("Starting bridge..."); } catch (Exception ex) { ((MelonBase)this).LoggerInstance.Warning("Hyland Helper Bridge disabled during init: " + ex.Message); _overlay.SetStatus("Bridge disabled during init."); } } public override void OnDeinitializeMelon() { try { _cts?.Cancel(); _runner?.Dispose(); _cts?.Dispose(); } catch { } } public override void OnGUI() { try { OverlaySnapshot overlaySnapshot = _overlay.Snapshot(); if (overlaySnapshot.Status == null || (!overlaySnapshot.Status.Contains("synced") && !overlaySnapshot.Status.Contains("Paired"))) { OverlayRenderer.Draw(_overlay); } } catch { } } } internal sealed class BridgePreferencesStore { public const int SchemaVersionCurrent = 1; public const int MaxPreferencesBytes = 4096; private readonly string _path; internal static string DefaultPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper", "bridge-preferences.json"); internal BridgePreferencesStore(string? path = null) { _path = path ?? DefaultPath; } internal bool ReadLiveOverlayEnabled() { return ReadEnabledFlag("liveOverlayEnabled"); } internal bool ReadAdaptiveCadenceEnabled() { return ReadEnabledFlag("adaptiveCadenceEnabled"); } private bool ReadEnabledFlag(string property) { try { FileInfo fileInfo = new FileInfo(_path); if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 4096) { return true; } byte[] array = File.ReadAllBytes(_path); if (array.Length == 0 || array.Length > 4096) { return true; } using JsonDocument jsonDocument = JsonDocument.Parse(array); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object) { return true; } if (!rootElement.TryGetProperty(property, out var value)) { return true; } return value.ValueKind != JsonValueKind.False; } catch { return true; } } internal bool Save(bool liveOverlayEnabled) { try { string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } string s = "{\"schemaVersion\":" + 1 + ",\"liveOverlayEnabled\":" + (liveOverlayEnabled ? "true" : "false") + "}"; string text = $"{_path}.{Guid.NewGuid():N}.tmp"; try { File.WriteAllBytes(text, Encoding.UTF8.GetBytes(s)); File.Move(text, _path, overwrite: true); } finally { try { if (File.Exists(text)) { File.Delete(text); } } catch { } } return true; } catch { return false; } } } internal enum BridgeRetryAction { Continue, Retry, Repair, StickyLegacy, Resynchronize, Stop } internal sealed record BridgeRetryDecision(BridgeRetryAction Action, TimeSpan? Delay = null); internal sealed class BridgeRetryPolicy { public static readonly TimeSpan MinDelay = TimeSpan.FromSeconds(2.0); public static readonly TimeSpan MaxDelay = TimeSpan.FromMinutes(2.0); private readonly Func<double> _nextJitter; public BridgeRetryPolicy(Func<double>? nextJitter = null) { _nextJitter = nextJitter ?? new Func<double>(Random.Shared.NextDouble); } public BridgeRetryDecision Decide(Exception exception, int attempt, DateTimeOffset now) { if ((exception is HttpRequestException || exception is TaskCanceledException) ? true : false) { return Retry(attempt, null); } if (exception is BridgePayloadException) { return Retry(attempt, null); } if (!(exception is BridgeClientException { StatusCode: var statusCode } ex)) { return new BridgeRetryDecision(BridgeRetryAction.Stop); } switch (statusCode) { case HttpStatusCode.Unauthorized: return new BridgeRetryDecision(BridgeRetryAction.Repair); case HttpStatusCode.Forbidden: { string code = ex.Code; if ((code == "SAVE_SCOPE_MISMATCH" || code == "PAIRING_SCOPE_MISMATCH") ? true : false) { return new BridgeRetryDecision(BridgeRetryAction.Repair); } return new BridgeRetryDecision(BridgeRetryAction.Continue); } case HttpStatusCode.NotFound: return new BridgeRetryDecision(BridgeRetryAction.StickyLegacy); case HttpStatusCode.Conflict: return Retry(attempt, ex.RetryAfter, BridgeRetryAction.Resynchronize); case HttpStatusCode.TooManyRequests: return Retry(attempt, ex.RetryAfter); default: if (ex.IsTransient) { return Retry(attempt, ex.RetryAfter); } return new BridgeRetryDecision(BridgeRetryAction.Stop); } } private BridgeRetryDecision Retry(int attempt, TimeSpan? serverDelay, BridgeRetryAction action = BridgeRetryAction.Retry) { if (serverDelay.HasValue) { return new BridgeRetryDecision(action, Clamp(serverDelay.Value)); } int num = Math.Clamp(attempt, 0, 16); double num2 = MinDelay.TotalSeconds * Math.Pow(2.0, num); double num3 = 0.5 + Math.Clamp(_nextJitter(), 0.0, 1.0) / 2.0; return new BridgeRetryDecision(action, Clamp(TimeSpan.FromSeconds(num2 * num3))); } private static TimeSpan Clamp(TimeSpan delay) { if (!(delay < MinDelay)) { if (!(delay > MaxDelay)) { return delay; } return MaxDelay; } return MinDelay; } } internal sealed class BridgeRunner : IDisposable { private readonly IBridgeGameAdapter _gameAdapter; private readonly BridgeClient _bridgeClient; private readonly BridgeSettingsStore _settingsStore; private readonly OverlayState _overlay; private readonly MelonBridgeLogger _logger; private readonly BridgeStatusWriter _statusWriter; private readonly BridgeCommandReader _commandReader; private readonly BridgeCommandResultWriter _commandResultWriter; private readonly MyMixesWriter _myMixesWriter; private readonly PhoneRecipePoller _phoneRecipePoller; private readonly TileChallengeState _tileChallenge; private readonly BridgeOperationalCapabilityState _capabilities; private readonly BridgeDownlinkCoordinator _downlinks; private readonly LocalRefreshInvalidationHub _refreshInvalidations = new LocalRefreshInvalidationHub(); private readonly BridgeRetryPolicy _retryPolicy = new BridgeRetryPolicy(); private readonly Func<DateTimeOffset> _utcNow; private readonly TimeSpan _exchangeCadence; private readonly TimeSpan _safetyCadence; private BridgeExchangeState _exchangeState = new BridgeExchangeState(); private BridgeSnapshotCoordinator _snapshotCoordinator; private DateTimeOffset _nextMyMixes = DateTimeOffset.MinValue; private BridgeLocalSettings _settings; private PairingSession? _pairing; private PairingScope? _tokenScope; private DateTimeOffset _nextPairingPoll = DateTimeOffset.MinValue; private DateTimeOffset _nextLatestPoll = DateTimeOffset.MinValue; private string? _lastImmediateRequestId; private bool _disposed; private readonly LiveOverlayReader? _liveOverlayReader; private readonly LiveFactBaseline? _liveFactBaseline; private readonly LiveFactMerger? _liveFactMerger; private readonly ScreenActivityReader? _screenActivityReader; private readonly bool _adaptiveCadenceEnabled; private BridgeCadenceController? _cadence; private string? _activeGenerationV2; private string? _lastLiveOverlayGeneration; internal ILocalRefreshInvalidationSource RefreshInvalidations => _refreshInvalidations; internal BridgeExchangeMode CurrentMode => _exchangeState.Mode; internal BridgeRunner(IBridgeGameAdapter gameAdapter, BridgeClient bridgeClient, BridgeSettingsStore settingsStore, OverlayState overlay, MelonBridgeLogger logger, BridgeOperationalCapabilityState? capabilities = null) : this(gameAdapter, bridgeClient, settingsStore, overlay, logger, capabilities ?? new BridgeOperationalCapabilityState(), () => DateTimeOffset.UtcNow, new PhoneRecipeCache(PhoneRecipeCache.DefaultPath), new AdvisorDispatchCache(AdvisorDispatchCache.DefaultPath), new TileChallengeState(TileChallengeState.DefaultPath), new BridgeStatusWriter(logger), null, null, null, new BridgePreferencesStore().ReadLiveOverlayEnabled(), null, new BridgePreferencesStore().ReadAdaptiveCadenceEnabled()) { } internal BridgeRunner(IBridgeGameAdapter gameAdapter, BridgeClient bridgeClient, BridgeSettingsStore settingsStore, OverlayState overlay, MelonBridgeLogger logger, BridgeOperationalCapabilityState capabilities, Func<DateTimeOffset> utcNow, PhoneRecipeCache phoneRecipeCache, AdvisorDispatchCache advisorDispatchCache, TileChallengeState tileChallenge, BridgeStatusWriter statusWriter, TimeSpan? exchangeCadence = null, TimeSpan? safetyCadence = null, MyMixesWriter? myMixesWriter = null, bool liveOverlayEnabled = true, string? liveOverlayPath = null, bool adaptiveCadenceEnabled = false, string? screenActivityPath = null) { _gameAdapter = gameAdapter; _bridgeClient = bridgeClient; _settingsStore = settingsStore; _overlay = overlay; _logger = logger; _capabilities = capabilities; _utcNow = utcNow; _exchangeCadence = exchangeCadence ?? BridgeConstants.SnapshotInterval; _safetyCadence = safetyCadence ?? TimeSpan.FromMinutes(5.0); _adaptiveCadenceEnabled = adaptiveCadenceEnabled; _statusWriter = statusWriter; _commandReader = new BridgeCommandReader(logger); _commandResultWriter = new BridgeCommandResultWriter(logger); _myMixesWriter = myMixesWriter ?? new MyMixesWriter(logger); _tileChallenge = tileChallenge; _snapshotCoordinator = NewSnapshotCoordinator(); if (adaptiveCadenceEnabled) { _screenActivityReader = new ScreenActivityReader(() => _activeGenerationV2, () => _statusWriter.RuntimeSession, screenActivityPath); } _settings = _settingsStore.Load(); _tokenScope = _settings.Scope; UpdateUnlinkAvailability(); _downlinks = new BridgeDownlinkCoordinator(phoneRecipeCache, _settingsStore, _settings, _utcNow, advisorDispatchCache); _phoneRecipePoller = new PhoneRecipePoller(_bridgeClient, phoneRecipeCache, _utcNow, BridgeConstants.PhoneRecipePollInterval); _overlay.TileChallenge = _tileChallenge.Status; if (!liveOverlayEnabled) { return; } _liveOverlayReader = new LiveOverlayReader(() => _activeGenerationV2, () => _statusWriter.RuntimeSession, liveOverlayPath); _liveFactBaseline = new LiveFactBaseline(); _liveFactMerger = new LiveFactMerger(_liveFactBaseline); if (gameAdapter is GameAdapter gameAdapter2) { gameAdapter2.SnapshotPostProcessor = (ParsedSnapshot parsed) => _liveFactMerger.Merge(parsed, _liveOverlayReader.TryGetCurrent(_utcNow(), out LiveOverlayDocument document) ? document : null); } } public async Task RunAsync(CancellationToken cancellationToken) { _logger.Info("Hyland Helper Bridge started."); while (!cancellationToken.IsCancellationRequested) { try { DetectedSave detectedSave = _gameAdapter.DetectCurrentSave(); _overlay.SaveLine = detectedSave.StatusLine; if (_utcNow() >= _nextMyMixes && CanWriteMyMixes(detectedSave)) { _nextMyMixes = _utcNow() + TimeSpan.FromSeconds(10.0); _myMixesWriter.Write(detectedSave); } if (string.IsNullOrWhiteSpace(_settings.BridgeToken)) { if (!_commandReader.TakeUnlink()) { await EnsurePairingAsync(detectedSave, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } else { ResetStoredToken("Unlinked from in-game menu; restarting pairing."); } } else { BridgeCommand bridgeCommand = _commandReader.TakeCommand(); bool unlink = bridgeCommand != null && string.Equals(bridgeCommand.Command, "unlink", StringComparison.OrdinalIgnoreCase); await RunCycleAsync(detectedSave, bridgeCommand, unlink, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { break; } catch (BridgeClientException ex2) when (ex2.IsUnauthorized) { ResetStoredToken("Stored bridge token was rejected; restarting pairing."); _logger.WarningThrottled("Stored bridge token was rejected; restarting pairing.", ex2); } catch (Exception exception) { _overlay.SetStatus("Bridge quiet-failed; retrying."); _logger.WarningThrottled("Bridge loop quiet-failed; retrying.", exception); await DelayQuietlyAsync(BridgeConstants.RetryBackoff, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } _statusWriter.Write(_overlay.Snapshot()); await DelayQuietlyAsync(TimeSpan.FromSeconds(1.0), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } } internal async Task RunCycleAsync(DetectedSave save, BridgeCommand? command, bool unlink, CancellationToken cancellationToken) { if (unlink) { ResetStoredToken("Unlinked from in-game menu; restarting pairing."); return; } ObserveLiveOverlayGeneration(save); if (string.IsNullOrWhiteSpace(_settings.BridgeToken)) { return; } if (_settings.RequiresGenerationRePair || _tokenScope == null) { _overlay.SetStatus("This install needs one re-pair to protect replacement save slots."); return; } if (!save.IsPairingEligible) { _overlay.SetStatus("Save generation details are not ready. Finish loading, then retry pairing."); return; } if (!_tokenScope.Matches(save.Fingerprint)) { _exchangeState.EnterRepair(); _overlay.SetStatus("Linked save changed; unlink and pair this playthrough again."); return; } if (command != null && !string.Equals(command.Command, "unlink", StringComparison.OrdinalIgnoreCase) && (!SaveGenerationIdentity.IsV2Fingerprint(command.SaveFingerprint) || !string.Equals(command.SaveFingerprint, save.Fingerprint.Fingerprint, StringComparison.Ordinal))) { _overlay.SetStatus("Ignored a command queued for a different save playthrough."); return; } try { PumpScreenActivity(); PumpLiveOverlay(); if (command != null && string.Equals(command.Command, "syncNow", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(command.RequestId) && !string.Equals(command.RequestId, _lastImmediateRequestId, StringComparison.Ordinal)) { _lastImmediateRequestId = command.RequestId; _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.SyncNow); _myMixesWriter.Invalidate(save.Fingerprint.Fingerprint); _refreshInvalidations.Invalidate(LocalRefreshSurface.ActiveSave | LocalRefreshSurface.MyMixes); _overlay.SetStatus("Save-complete sync requested."); _logger.Info("Save-complete sync requested."); } if (command != null && string.Equals(command.Command, "saveMix", StringComparison.OrdinalIgnoreCase) && command.Mix != null && !string.IsNullOrWhiteSpace(command.RequestId)) { await SendPendingMixAsync(command, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } await RunTransportCycleAsync(save, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (BridgeClientException ex) when (ex.IsUnauthorized) { ResetStoredToken("Stored bridge token was rejected; restarting pairing."); _logger.WarningThrottled("Stored bridge token was rejected; restarting pairing.", ex); } finally { _statusWriter.Write(_overlay.Snapshot()); } } private async Task RunTransportCycleAsync(DetectedSave save, CancellationToken cancellationToken) { _tileChallenge.Refresh(); if (save.IsFallback) { if (UpdateOperationalCapabilities(BridgeExchangeMode.Negotiating)) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.CapabilityChanged); } _overlay.SetStatus("Waiting for a validated save."); return; } _snapshotCoordinator.ObserveSave(save, _tokenScope.SaveFingerprint); if (UpdateOperationalCapabilities(_exchangeState.Mode)) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.CapabilityChanged); } if (_exchangeState.Mode == BridgeExchangeMode.Repair || _snapshotCoordinator.RepairRequired) { _exchangeState.EnterRepair(); _overlay.SetStatus("Linked save changed; unlink and pair this playthrough again."); return; } if (_exchangeState.Mode == BridgeExchangeMode.Legacy) { await RunLegacyCycleAsync(save, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); return; } if (!_exchangeState.CanAttemptExchange(_utcNow())) { if (_downlinks.PhoneRecipesRequireLegacyPoll) { await _phoneRecipePoller.PollIfDueAsync(_settings.BridgeToken, save.Fingerprint.Fingerprint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } return; } BridgeEnvelopePlan plan = _snapshotCoordinator.TakeDue(_utcNow(), _exchangeState.Mode); if (plan.Kind == BridgeEnvelopeKind.None) { if (_downlinks.PhoneRecipesRequireLegacyPoll) { await _phoneRecipePoller.PollIfDueAsync(_settings.BridgeToken, save.Fingerprint.Fingerprint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } return; } long sequence = _settingsStore.NextSequence(_settings, _utcNow()); BridgeEnvelope envelope = ((plan.Kind == BridgeEnvelopeKind.Heartbeat) ? _gameAdapter.BuildHeartbeat(save, sequence) : _gameAdapter.BuildSnapshot(save, sequence)); BridgeExchangeRequest request = new BridgeExchangeRequest { Envelope = envelope }; _downlinks.PopulateKnownRevisions(request.Downlink); BridgeExchangeResponse bridgeExchangeResponse; try { bridgeExchangeResponse = await _bridgeClient.ExchangeAsync(_settings.BridgeToken, request, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (BridgeClientException ex) when (ex.IsUnauthorized) { throw; } catch (Exception ex2) when (!(ex2 is OperationCanceledException) || !cancellationToken.IsCancellationRequested) { BridgeRetryDecision bridgeRetryDecision = _exchangeState.ApplyFailure(ex2, _retryPolicy, _utcNow()); if (bridgeRetryDecision.Action == BridgeRetryAction.Resynchronize) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.StaleAcknowledgement); } if (UpdateOperationalCapabilities(_exchangeState.Mode)) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.CapabilityChanged); } OverlayState overlay = _overlay; overlay.SetStatus(bridgeRetryDecision.Action switch { BridgeRetryAction.StickyLegacy => "Exchange unavailable; using legacy bridge mode.", BridgeRetryAction.Repair => "Linked save changed; re-pair in the phone app to continue.", _ => "Exchange temporarily unavailable; retrying quietly.", }); return; } DetectedSave detectedSave = _gameAdapter.DetectCurrentSave(); if (!detectedSave.IsPairingEligible) { _overlay.SetStatus("Save generation details are not ready. Retrying before applying synced data."); _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.StaleAcknowledgement); return; } if (_tokenScope == null || !_tokenScope.Matches(detectedSave.Fingerprint)) { _exchangeState.EnterRepair(); _overlay.SetStatus("Linked save changed; unlink and pair this playthrough again."); return; } BridgeAcknowledgementResult bridgeAcknowledgementResult = _exchangeState.ApplySuccess(bridgeExchangeResponse.Ingest, _settings.LastSequence, _utcNow()); if (bridgeAcknowledgementResult.SequenceFloor > _settings.LastSequence) { long lastSequence = _settings.LastSequence; _settings.LastSequence = bridgeAcknowledgementResult.SequenceFloor; if (!_settingsStore.Save(_settings)) { _settings.LastSequence = lastSequence; _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.StaleAcknowledgement); _overlay.SetStatus("Sequence floor persistence failed; retrying full sync."); return; } } BridgeDownlinkApplyOutcome outcome = _downlinks.ApplyPhoneRecipes(bridgeExchangeResponse.Downlink.PhoneRecipes, save.Fingerprint.Fingerprint); BridgeDownlinkApplyOutcome outcome2 = _downlinks.ApplyAdvisorDispatches(bridgeExchangeResponse.Downlink.AdvisorDispatches, save.Fingerprint.Fingerprint); _cadence?.NotifyHint(bridgeExchangeResponse.PollHintSeconds, AcceptedFreshDelivery.Evaluate(bridgeExchangeResponse.Ingest, outcome, request.Downlink.PhoneRecipes, bridgeExchangeResponse.Downlink.PhoneRecipes?.Revision) || AcceptedFreshDelivery.Evaluate(bridgeExchangeResponse.Ingest, outcome2, request.Downlink.AdvisorDispatches, bridgeExchangeResponse.Downlink.AdvisorDispatches?.Revision), _utcNow()); _snapshotCoordinator.Complete(plan, _utcNow()); if (plan.Kind == BridgeEnvelopeKind.FullSnapshot && bridgeExchangeResponse.Ingest.Stored && !bridgeExchangeResponse.Ingest.Stale) { CommitDeliveredRuntimeBaselines(); } if (plan.EnterRepairAfterSend || _snapshotCoordinator.RepairRequired) { _exchangeState.EnterRepair(); } if (bridgeAcknowledgementResult.RequiresFullResynchronization) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.StaleAcknowledgement); } if (UpdateOperationalCapabilities(_exchangeState.Mode)) { _snapshotCoordinator.RequestFull(BridgeFullSnapshotReason.CapabilityChanged); } if (_downlinks.PhoneRecipesRequireLegacyPoll) { await _phoneRecipePoller.PollIfDueAsync(_settings.BridgeToken, save.Fingerprint.Fingerprint, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } _overlay.Sequence = s
HylandHelper.PhoneTile.dll
Decompiled 5 hours 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.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using HylandHelper.BridgeContracts; using HylandHelper.BridgeMod; using HylandHelper.PhoneTile; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.UI; using MelonLoader; using Microsoft.CodeAnalysis; using S1API.GameTime; using S1API.Leveling; using S1API.Lifecycle; using S1API.Money; using S1API.PhoneApp; using S1API.Property; using S1API.UI; using S1API.Utils; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(PhoneTileMod), "Hyland Helper Phone Tile", "0.1.3", "MadJag Studios", null)] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("MadJag Studios")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © 2026 MadJag Studios")] [assembly: AssemblyDescription("Hyland Helper in-game phone tile for Schedule I")] [assembly: AssemblyFileVersion("0.1.3.0")] [assembly: AssemblyInformationalVersion("0.1.3")] [assembly: AssemblyProduct("Hyland Helper")] [assembly: AssemblyTitle("HylandHelper.PhoneTile")] [assembly: AssemblyVersion("0.1.3.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 HylandHelper.BridgeMod { [Flags] internal enum LocalRefreshSurface { None = 0, ActiveSave = 1, MyMixes = 2, Status = 4, AdvisorDispatches = 8, PendingCommandResult = 0x10, LiveContext = 0x20 } internal readonly record struct LocalFileSignature(bool Exists, long Length, long LastWriteUtcTicks) { internal static LocalFileSignature Missing => new LocalFileSignature(Exists: false, 0L, 0L); } internal enum LocalRefreshReason { Initial, Dirty, SignatureChanged, ForcedRecovery, Unchanged } internal interface ILocalRefreshInvalidationSource { LocalRefreshSurface Pending { get; } event Action<LocalRefreshSurface>? Invalidated; } internal sealed class LocalRefreshInvalidationHub : ILocalRefreshInvalidationSource { public LocalRefreshSurface Pending { get; private set; } public event Action<LocalRefreshSurface>? Invalidated; public void Invalidate(LocalRefreshSurface surfaces) { if (surfaces != LocalRefreshSurface.None) { LocalRefreshSurface localRefreshSurface = surfaces & ~Pending; Pending |= surfaces; if (localRefreshSurface != LocalRefreshSurface.None) { this.Invalidated?.Invoke(localRefreshSurface); } } } public bool Take(LocalRefreshSurface surface) { if ((Pending & surface) == 0) { return false; } Pending &= ~surface; return true; } public void Reset(LocalRefreshSurface surfaces) { Pending &= ~surfaces; } } internal sealed class LocalFileRefreshGate { private readonly TimeSpan _forcedRecoveryInterval; private LocalFileSignature _lastAccepted; private DateTimeOffset _lastAcceptedAt; private bool _hasAccepted; private bool _retryTransient; public LocalFileRefreshGate(TimeSpan forcedRecoveryInterval) { if (forcedRecoveryInterval <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException("forcedRecoveryInterval"); } _forcedRecoveryInterval = forcedRecoveryInterval; } public bool ShouldRefresh(LocalFileSignature signature, DateTimeOffset now, bool dirty, out LocalRefreshReason reason) { if (!_hasAccepted) { reason = LocalRefreshReason.Initial; return true; } if (dirty || _retryTransient) { reason = LocalRefreshReason.Dirty; return true; } if (signature != _lastAccepted) { reason = LocalRefreshReason.SignatureChanged; return true; } if (now - _lastAcceptedAt >= _forcedRecoveryInterval) { reason = LocalRefreshReason.ForcedRecovery; return true; } reason = LocalRefreshReason.Unchanged; return false; } public void Accept(LocalFileSignature signature, DateTimeOffset now) { _lastAccepted = signature; _lastAcceptedAt = now; _hasAccepted = true; _retryTransient = false; } public void RejectTransient() { _retryTransient = true; } public void Reset() { _lastAccepted = default(LocalFileSignature); _lastAcceptedAt = default(DateTimeOffset); _hasAccepted = false; _retryTransient = false; } } internal sealed record ProvenSaveIdentity(string SteamIdHash, string SlotId, string Fingerprint); internal static class SaveGenerationIdentity { internal const string FingerprintV2Prefix = "v2-"; private const long MaxMetadataBytes = 1048576L; internal static bool TryCreate(string activeSaveDirectory, out ProvenSaveIdentity? identity, out string reason) { identity = null; reason = "generation-metadata-unavailable"; try { if (!TryGetDirectoryIdentity(activeSaveDirectory, out string steamDirectory, out string slotId)) { reason = "generation-save-path-invalid"; return false; } if (!TryReadMetadata(activeSaveDirectory, out var creation) || !TryReadSeed(activeSaveDirectory, out var seed)) { return false; } string text = Sha256(Path.GetFileName(steamDirectory)); string value = string.Join("\n", "hyland-helper/save-fingerprint/v2", "steam=" + text, "slot=" + slotId.ToLowerInvariant(), "created=" + creation.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture), "seed=" + seed.ToString(CultureInfo.InvariantCulture)); identity = new ProvenSaveIdentity(text, slotId, "v2-" + Sha256(value)); reason = string.Empty; return true; } catch (IOException) { return false; } catch (JsonException) { return false; } catch (UnauthorizedAccessException) { return false; } } internal static bool IsV2Fingerprint(string? value) { if (value == null || value.Length != "v2-".Length + 64 || !value.StartsWith("v2-", StringComparison.Ordinal)) { return false; } for (int i = "v2-".Length; i < value.Length; i++) { char c = value[i]; if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } private static bool TryGetDirectoryIdentity(string activeSaveDirectory, out string steamDirectory, out string slotId) { steamDirectory = string.Empty; slotId = string.Empty; if (string.IsNullOrWhiteSpace(activeSaveDirectory)) { return false; } string path = Path.GetFullPath(activeSaveDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (!Directory.Exists(path)) { return false; } slotId = Path.GetFileName(path); if (!IsSaveSlot(slotId)) { return false; } steamDirectory = Path.GetDirectoryName(path) ?? string.Empty; return !string.IsNullOrWhiteSpace(Path.GetFileName(steamDirectory)); } private static bool TryReadMetadata(string saveDirectory, out DateTime creation) { creation = default(DateTime); if (!TryReadJson(Path.Combine(saveDirectory, "Metadata.json"), out JsonDocument document)) { return false; } using (document) { JsonElement rootElement = document.RootElement; if (!HasString(rootElement, "DataType", "MetaData") || !HasInt32(rootElement, "DataVersion") || !HasNonEmptyString(rootElement, "GameVersion") || !HasNonEmptyString(rootElement, "CreationVersion") || !TryGetUniqueProperty(rootElement, "CreationDate", out var value) || value.ValueKind != JsonValueKind.Object || !HasNonEmptyString(value, "DataType") || !HasInt32(value, "DataVersion") || !HasNonEmptyString(value, "GameVersion") || !TryGetInt32(value, "Year", out var value2) || !TryGetInt32(value, "Month", out var value3) || !TryGetInt32(value, "Day", out var value4) || !TryGetInt32(value, "Hour", out var value5) || !TryGetInt32(value, "Minute", out var value6) || !TryGetInt32(value, "Second", out var value7)) { return false; } try { creation = new DateTime(value2, value3, value4, value5, value6, value7, DateTimeKind.Unspecified); return true; } catch (ArgumentOutOfRangeException) { return false; } } } private static bool TryReadSeed(string saveDirectory, out long seed) { seed = 0L; if (!TryReadJson(Path.Combine(saveDirectory, "Game.json"), out JsonDocument document)) { return false; } using (document) { JsonElement rootElement = document.RootElement; return HasString(rootElement, "DataType", "GameData") && HasInt32(rootElement, "DataVersion") && HasNonEmptyString(rootElement, "GameVersion") && TryGetInt64(rootElement, "Seed", out seed); } } private static bool TryReadJson(string path, out JsonDocument document) { document = null; FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists || fileInfo.Length > 1048576) { return false; } using FileStream utf8Json = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); document = JsonDocument.Parse(utf8Json); return true; } private static bool HasString(JsonElement element, string propertyName, string expected) { if (TryGetUniqueProperty(element, propertyName, out var value) && value.ValueKind == JsonValueKind.String) { return string.Equals(value.GetString(), expected, StringComparison.Ordinal); } return false; } private static bool HasNonEmptyString(JsonElement element, string propertyName) { if (TryGetUniqueProperty(element, propertyName, out var value) && value.ValueKind == JsonValueKind.String) { return !string.IsNullOrWhiteSpace(value.GetString()); } return false; } private static bool HasInt32(JsonElement element, string propertyName) { int value; return TryGetInt32(element, propertyName, out value); } private static bool TryGetInt32(JsonElement element, string propertyName, out int value) { value = 0; if (TryGetUniqueProperty(element, propertyName, out var value2) && value2.ValueKind == JsonValueKind.Number) { return value2.TryGetInt32(out value); } return false; } private static bool TryGetInt64(JsonElement element, string propertyName, out long value) { value = 0L; if (TryGetUniqueProperty(element, propertyName, out var value2) && value2.ValueKind == JsonValueKind.Number) { return value2.TryGetInt64(out value); } return false; } private static bool TryGetUniqueProperty(JsonElement element, string propertyName, out JsonElement value) { value = default(JsonElement); if (element.ValueKind != JsonValueKind.Object) { return false; } bool flag = false; foreach (JsonProperty item in element.EnumerateObject()) { if (string.Equals(item.Name, propertyName, StringComparison.Ordinal)) { if (flag) { value = default(JsonElement); return false; } flag = true; value = item.Value; } } return flag; } private static bool IsSaveSlot(string value) { if (!value.StartsWith("SaveGame_", StringComparison.OrdinalIgnoreCase) || value.Length == "SaveGame_".Length) { return false; } for (int i = "SaveGame_".Length; i < value.Length; i++) { if (value[i] < '0' || value[i] > '9') { return false; } } return true; } private static string Sha256(string value) { using SHA256 sHA = SHA256.Create(); return Convert.ToHexString(sHA.ComputeHash(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); } } internal sealed class LiveOverlayDocument { public const int SchemaVersionCurrent = 1; public const int MaxSerializedBytes = 8192; [JsonPropertyName("schemaVersion")] public int SchemaVersion { get; set; } = 1; [JsonPropertyName("playthroughGeneration")] public string PlaythroughGeneration { get; set; } = ""; [JsonPropertyName("runtimeSession")] public string RuntimeSession { get; set; } = ""; [JsonPropertyName("tileInstanceId")] public string TileInstanceId { get; set; } = ""; [JsonPropertyName("loadEpoch")] public int LoadEpoch { get; set; } [JsonPropertyName("sequence")] public long Sequence { get; set; } [JsonPropertyName("observedAt")] public string ObservedAt { get; set; } = ""; [JsonPropertyName("tileVersion")] public string TileVersion { get; set; } = ""; [JsonPropertyName("s1ApiVersion")] public string S1ApiVersion { get; set; } = ""; [JsonPropertyName("facts")] public LiveOverlayFacts Facts { get; set; } = new LiveOverlayFacts(); } internal sealed class LiveOverlayFacts { [JsonPropertyName("rank")] public LiveRankFact? Rank { get; set; } [JsonPropertyName("elapsedDays")] public LiveElapsedDaysFact? ElapsedDays { get; set; } [JsonPropertyName("properties")] public LivePropertiesFact? Properties { get; set; } [JsonPropertyName("money")] public LiveMoneyFact? Money { get; set; } } internal sealed class LiveRankFact { [JsonPropertyName("scope")] public string Scope { get; set; } = "shared"; [JsonPropertyName("rankName")] public string RankName { get; set; } = ""; [JsonPropertyName("tier")] public int Tier { get; set; } [JsonPropertyName("xp")] public int Xp { get; set; } [JsonPropertyName("totalXp")] public int TotalXp { get; set; } } internal sealed class LiveElapsedDaysFact { [JsonPropertyName("scope")] public string Scope { get; set; } = "shared"; [JsonPropertyName("value")] public int Value { get; set; } } internal sealed class LivePropertiesFact { [JsonPropertyName("scope")] public string Scope { get; set; } = "shared"; [JsonPropertyName("codes")] public List<string> Codes { get; set; } = new List<string>(); [JsonPropertyName("businessCodes")] public List<string> BusinessCodes { get; set; } = new List<string>(); } internal sealed class LiveMoneyFact { [JsonPropertyName("cashOnHand")] public LiveMoneyValue CashOnHand { get; set; } = new LiveMoneyValue { Scope = "player" }; [JsonPropertyName("onlineBalance")] public LiveMoneyValue OnlineBalance { get; set; } = new LiveMoneyValue(); [JsonPropertyName("netWorth")] public LiveMoneyValue NetWorth { get; set; } = new LiveMoneyValue(); } internal sealed class LiveMoneyValue { [JsonPropertyName("scope")] public string Scope { get; set; } = "shared"; [JsonPropertyName("value")] public double Value { get; set; } } internal static class LiveOverlaySerializer { private static readonly JsonSerializerOptions Options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private static readonly string[] TopLevelKeys = new string[10] { "schemaVersion", "playthroughGeneration", "runtimeSession", "tileInstanceId", "loadEpoch", "sequence", "observedAt", "tileVersion", "s1ApiVersion", "facts" }; private static readonly string[] FactKeys = new string[4] { "rank", "elapsedDays", "properties", "money" }; private static readonly string[] RankKeys = new string[5] { "scope", "rankName", "tier", "xp", "totalXp" }; private static readonly string[] ValueFactKeys = new string[2] { "scope", "value" }; private static readonly string[] PropertiesKeys = new string[3] { "scope", "codes", "businessCodes" }; private static readonly string[] MoneyKeys = new string[3] { "cashOnHand", "onlineBalance", "netWorth" }; internal static bool TrySerialize(LiveOverlayDocument document, out byte[] utf8, out string? reason) { utf8 = JsonSerializer.SerializeToUtf8Bytes(document, Options); if (utf8.Length > 8192) { reason = "overBudget"; utf8 = Array.Empty<byte>(); return false; } reason = null; return true; } internal static bool TryParse(byte[] utf8, out LiveOverlayDocument? document, out string reason) { document = null; if (utf8 == null || utf8.Length == 0) { reason = "empty"; return false; } if (utf8.Length > 8192) { reason = "overBudget"; return false; } try { using JsonDocument jsonDocument = JsonDocument.Parse(utf8); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object) { reason = "rootNotObject"; return false; } if (!CheckExactKeys(rootElement, TopLevelKeys, requireAll: true, out reason)) { return false; } LiveOverlayDocument liveOverlayDocument = new LiveOverlayDocument(); if (!TryInt(rootElement, "schemaVersion", 1, 1, out var value, out reason)) { return false; } liveOverlayDocument.SchemaVersion = value; JsonElement property = rootElement.GetProperty("playthroughGeneration"); if (property.ValueKind != JsonValueKind.String || !SaveGenerationIdentity.IsV2Fingerprint(property.GetString())) { reason = "playthroughGeneration"; return false; } liveOverlayDocument.PlaythroughGeneration = property.GetString(); if (!TryBoundedString(rootElement, "runtimeSession", 1, 64, out string value2, out reason)) { return false; } liveOverlayDocument.RuntimeSession = value2; if (!TryBoundedString(rootElement, "tileInstanceId", 32, 32, out string value3, out reason)) { return false; } if (!IsLowerHex(value3)) { reason = "tileInstanceId"; return false; } liveOverlayDocument.TileInstanceId = value3; if (!TryInt(rootElement, "loadEpoch", 1, 1000000, out var value4, out reason)) { return false; } liveOverlayDocument.LoadEpoch = value4; if (!TryLong(rootElement, "sequence", 1L, 999999999999L, out var value5, out reason)) { return false; } liveOverlayDocument.Sequence = value5; if (!TryBoundedString(rootElement, "observedAt", 1, 40, out string value6, out reason)) { return false; } if (!DateTimeOffset.TryParse(value6, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var _)) { reason = "observedAt"; return false; } liveOverlayDocument.ObservedAt = value6; if (!TryBoundedString(rootElement, "tileVersion", 1, 64, out string value7, out reason)) { return false; } liveOverlayDocument.TileVersion = value7; if (!TryBoundedString(rootElement, "s1ApiVersion", 1, 64, out string value8, out reason)) { return false; } liveOverlayDocument.S1ApiVersion = value8; JsonElement property2 = rootElement.GetProperty("facts"); if (property2.ValueKind != JsonValueKind.Object) { reason = "facts"; return false; } if (!CheckExactKeys(property2, FactKeys, requireAll: false, out reason)) { return false; } if (property2.TryGetProperty("rank", out var value9)) { if (!TryParseRank(value9, out LiveRankFact fact, out reason)) { return false; } liveOverlayDocument.Facts.Rank = fact; } if (property2.TryGetProperty("elapsedDays", out var value10)) { if (!TryParseElapsedDays(value10, out LiveElapsedDaysFact fact2, out reason)) { return false; } liveOverlayDocument.Facts.ElapsedDays = fact2; } if (property2.TryGetProperty("properties", out var value11)) { if (!TryParseProperties(value11, out LivePropertiesFact fact3, out reason)) { return false; } liveOverlayDocument.Facts.Properties = fact3; } if (property2.TryGetProperty("money", out var value12)) { if (!TryParseMoney(value12, out LiveMoneyFact fact4, out reason)) { return false; } liveOverlayDocument.Facts.Money = fact4; } document = liveOverlayDocument; reason = "ok"; return true; } catch (JsonException) { reason = "malformedJson"; return false; } } private static bool TryParseRank(JsonElement element, out LiveRankFact? fact, out string reason) { fact = null; if (element.ValueKind != JsonValueKind.Object || !CheckExactKeys(element, RankKeys, requireAll: true, out reason)) { reason = "rank"; return false; } if (!IsScope(element, "shared")) { reason = "rank.scope"; return false; } if (!TryBoundedString(element, "rankName", 1, 32, out string value, out string reason2)) { reason = "rank.rankName"; return false; } if (!TryInt(element, "tier", 1, 99999, out int value2, out reason2)) { reason = "rank.tier"; return false; } if (!TryInt(element, "xp", 0, 999999999, out int value3, out reason2)) { reason = "rank.xp"; return false; } if (!TryInt(element, "totalXp", 0, 999999999, out int value4, out reason2)) { reason = "rank.totalXp"; return false; } fact = new LiveRankFact { RankName = value, Tier = value2, Xp = value3, TotalXp = value4 }; reason = "ok"; return true; } private static bool TryParseElapsedDays(JsonElement element, out LiveElapsedDaysFact? fact, out string reason) { fact = null; if (element.ValueKind != JsonValueKind.Object || !CheckExactKeys(element, ValueFactKeys, requireAll: true, out reason)) { reason = "elapsedDays"; return false; } if (!IsScope(element, "shared")) { reason = "elapsedDays.scope"; return false; } if (!TryInt(element, "value", 0, 99999, out int value, out string _)) { reason = "elapsedDays.value"; return false; } fact = new LiveElapsedDaysFact { Value = value }; reason = "ok"; return true; } private static bool TryParseProperties(JsonElement element, out LivePropertiesFact? fact, out string reason) { fact = null; if (element.ValueKind != JsonValueKind.Object || !CheckExactKeys(element, PropertiesKeys, requireAll: true, out reason)) { reason = "properties"; return false; } if (!IsScope(element, "shared")) { reason = "properties.scope"; return false; } if (!TryCodes(element, "codes", 64, out HashSet<string> codes)) { reason = "properties.codes"; return false; } if (!TryCodes(element, "businessCodes", 32, out HashSet<string> codes2)) { reason = "properties.businessCodes"; return false; } foreach (string item in codes2) { if (!codes.Contains(item)) { reason = "properties.businessSubset"; return false; } } fact = new LivePropertiesFact { Codes = new List<string>(codes), BusinessCodes = new List<string>(codes2) }; reason = "ok"; return true; } private static bool TryParseMoney(JsonElement element, out LiveMoneyFact? fact, out string reason) { fact = null; if (element.ValueKind != JsonValueKind.Object || !CheckExactKeys(element, MoneyKeys, requireAll: true, out reason)) { reason = "money"; return false; } if (!TryMoneyValue(element, "cashOnHand", "player", 0.0, out LiveMoneyValue value)) { reason = "money.cashOnHand"; return false; } if (!TryMoneyValue(element, "onlineBalance", "shared", 0.0, out LiveMoneyValue value2)) { reason = "money.onlineBalance"; return false; } if (!TryMoneyValue(element, "netWorth", "shared", -1000000000.0, out LiveMoneyValue value3)) { reason = "money.netWorth"; return false; } fact = new LiveMoneyFact { CashOnHand = value, OnlineBalance = value2, NetWorth = value3 }; reason = "ok"; return true; } private static bool TryMoneyValue(JsonElement parent, string name, string scope, double minimum, out LiveMoneyValue? value) { value = null; if (!parent.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.Object) { return false; } if (!CheckExactKeys(value2, ValueFactKeys, requireAll: true, out string _)) { return false; } if (!IsScope(value2, scope)) { return false; } if (!value2.TryGetProperty("value", out var value3) || value3.ValueKind != JsonValueKind.Number) { return false; } if (!decimal.TryParse(value3.GetRawText(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return false; } if (result != Math.Round(result, 2)) { return false; } if (result < (decimal)minimum || result >= 1000000000m) { return false; } value = new LiveMoneyValue { Scope = scope, Value = (double)result }; return true; } private static bool TryCodes(JsonElement parent, string name, int max, out HashSet<string> codes) { codes = new HashSet<string>(StringComparer.Ordinal); if (!parent.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array) { return false; } int num = 0; foreach (JsonElement item in value.EnumerateArray()) { if (++num > max) { return false; } if (item.ValueKind != JsonValueKind.String) { return false; } string text = item.GetString(); if (string.IsNullOrEmpty(text) || text.Length > 48 || !IsCanonicalCode(text)) { return false; } if (!codes.Add(text)) { return false; } } return true; } internal static bool IsCanonicalCode(string code) { for (int i = 0; i < code.Length; i++) { bool flag; switch (code[i]) { case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '_': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': flag = true; break; default: flag = false; break; } if (!flag) { return false; } } return true; } private static bool IsLowerHex(string value) { for (int i = 0; i < value.Length; i++) { bool flag; switch (value[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': flag = true; break; default: flag = false; break; } if (!flag) { return false; } } return true; } private static bool IsScope(JsonElement element, string expected) { if (element.TryGetProperty("scope", out var value) && value.ValueKind == JsonValueKind.String) { return value.GetString() == expected; } return false; } private static bool CheckExactKeys(JsonElement element, string[] allowed, bool requireAll, out string reason) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (JsonProperty item in element.EnumerateObject()) { if (Array.IndexOf(allowed, item.Name) < 0) { reason = "unknownField:" + Bound(item.Name); return false; } if (!hashSet.Add(item.Name)) { reason = "duplicateField:" + Bound(item.Name); return false; } } if (requireAll) { foreach (string text in allowed) { if (!hashSet.Contains(text)) { reason = "missingField:" + text; return false; } } } reason = "ok"; return true; } private static string Bound(string value) { if (value.Length > 32) { return value.Substring(0, 32); } return value; } private static bool TryBoundedString(JsonElement parent, string name, int minLength, int maxLength, out string value, out string reason) { value = ""; if (!parent.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.String) { reason = name; return false; } string text = value2.GetString() ?? ""; if (text.Trim().Length == 0 || text.Length < minLength || text.Length > maxLength) { reason = name; return false; } value = text; reason = "ok"; return true; } private static bool TryInt(JsonElement parent, string name, int minimum, int maximum, out int value, out string reason) { value = 0; if (!parent.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.Number || !value2.TryGetInt32(out var value3) || value3 < minimum || value3 > maximum) { reason = name; return false; } value = value3; reason = "ok"; return true; } private static bool TryLong(JsonElement parent, string name, long minimum, long maximum, out long value, out string reason) { value = 0L; if (!parent.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.Number || !value2.TryGetInt64(out var value3) || value3 < minimum || value3 > maximum) { reason = name; return false; } value = value3; reason = "ok"; return true; } } internal enum ScreenActivityScreen { PhoneRecipes, AdvisorDispatches } internal sealed class ScreenActivityDocument { public const int SchemaVersionCurrent = 1; public const int MaxSerializedBytes = 2048; [JsonPropertyName("schemaVersion")] public int SchemaVersion { get; set; } = 1; [JsonPropertyName("playthroughGeneration")] public string PlaythroughGeneration { get; set; } = ""; [JsonPropertyName("runtimeSession")] public string RuntimeSession { get; set; } = ""; [JsonPropertyName("tileInstanceId")] public string TileInstanceId { get; set; } = ""; [JsonPropertyName("sequence")] public long Sequence { get; set; } [JsonPropertyName("updatedAt")] public string UpdatedAt { get; set; } = ""; [JsonPropertyName("visibleScreen")] public string? VisibleScreen { get; set; } } internal static class ScreenActivitySerializer { private static readonly JsonSerializerOptions Options = new JsonSerializerOptions(); private static readonly string[] Required = new string[6] { "schemaVersion", "playthroughGeneration", "runtimeSession", "tileInstanceId", "sequence", "updatedAt" }; private static readonly string[] Allowed = new string[7] { "schemaVersion", "playthroughGeneration", "runtimeSession", "tileInstanceId", "sequence", "updatedAt", "visibleScreen" }; internal static bool TrySerialize(ScreenActivityDocument document, out byte[] utf8, out string? reason) { utf8 = JsonSerializer.SerializeToUtf8Bytes(document, Options); if (utf8.Length > 2048) { utf8 = Array.Empty<byte>(); reason = "overBudget"; return false; } reason = null; return true; } internal static bool TryParse(byte[] utf8, out ScreenActivityDocument? document, out string? reason) { document = null; if (utf8 == null || utf8.Length == 0) { reason = "empty"; return false; } if (utf8.Length > 2048) { reason = "overBudget"; return false; } try { using JsonDocument jsonDocument = JsonDocument.Parse(utf8); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object) { reason = "rootNotObject"; return false; } if (!ExactKeys(rootElement, out reason)) { return false; } if (!Int(rootElement, "schemaVersion", 1, 1, out var value) || !Text(rootElement, "playthroughGeneration", 1, 80, out string value2) || !IsGeneration(value2) || !Text(rootElement, "runtimeSession", 1, 64, out string value3) || !Text(rootElement, "tileInstanceId", 32, 32, out string value4) || !IsLowerHex(value4) || !Long(rootElement, "sequence", 1L, long.MaxValue, out var value5) || !Text(rootElement, "updatedAt", 1, 40, out string value6) || !DateTimeOffset.TryParse(value6, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var _)) { reason = "invalidField"; return false; } string visibleScreen = null; if (rootElement.TryGetProperty("visibleScreen", out var value7)) { if (value7.ValueKind == JsonValueKind.Null) { visibleScreen = null; } else { if (value7.ValueKind != JsonValueKind.String || !ParseScreen(value7.GetString()).HasValue) { reason = "visibleScreen"; return false; } visibleScreen = value7.GetString(); } } document = new ScreenActivityDocument { SchemaVersion = value, PlaythroughGeneration = value2, RuntimeSession = value3, TileInstanceId = value4, Sequence = value5, UpdatedAt = value6, VisibleScreen = visibleScreen }; reason = "ok"; return true; } catch (JsonException) { reason = "malformedJson"; return false; } } internal static ScreenActivityScreen? ParseScreen(string? value) { if (!(value == "phoneRecipes")) { if (value == "advisorDispatches") { return ScreenActivityScreen.AdvisorDispatches; } return null; } return ScreenActivityScreen.PhoneRecipes; } private static bool ExactKeys(JsonElement root, out string? reason) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (JsonProperty item2 in root.EnumerateObject()) { if (Array.IndexOf(Allowed, item2.Name) < 0) { reason = "unknownField"; return false; } if (!hashSet.Add(item2.Name)) { reason = "duplicateField"; return false; } } string[] required = Required; foreach (string item in required) { if (!hashSet.Contains(item)) { reason = "missingField"; return false; } } reason = null; return true; } private static bool Text(JsonElement root, string name, int min, int max, out string value) { value = ""; if (!root.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.String) { return false; } value = value2.GetString() ?? ""; if (value.Trim().Length >= min) { return value.Length <= max; } return false; } private static bool Int(JsonElement root, string name, int min, int max, out int value) { value = 0; if (root.TryGetProperty(name, out var value2) && value2.ValueKind == JsonValueKind.Number && value2.TryGetInt32(out value) && value >= min) { return value <= max; } return false; } private static bool Long(JsonElement root, string name, long min, long max, out long value) { value = 0L; if (root.TryGetProperty(name, out var value2) && value2.ValueKind == JsonValueKind.Number && value2.TryGetInt64(out value) && value >= min) { return value <= max; } return false; } private static bool IsGeneration(string value) { if (value.Length == 67 && value.StartsWith("v2-", StringComparison.Ordinal)) { return IsLowerHex(value.Substring(3, value.Length - 3)); } return false; } private static bool IsLowerHex(string value) { for (int i = 0; i < value.Length; i++) { bool flag; switch (value[i]) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': flag = true; break; default: flag = false; break; } if (!flag) { return false; } } return true; } } } namespace HylandHelper.BridgeContracts { public static class GameDatasetIds { private static readonly Dictionary<string, string> CanonicalAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["ogkush"] = "og_kush", ["sourdiesel"] = "sour_diesel", ["greencrack"] = "green_crack", ["granddaddypurple"] = "granddaddy_purple", ["shroom"] = "shrooms", ["energydrink"] = "energy_drink", ["motoroil"] = "motor_oil", ["megabean"] = "mega_bean", ["flumedicine"] = "flu_medicine", ["horsesemen"] = "horse_semen", ["viagor"] = "viagra" }; private static readonly Dictionary<string, string> FriendlyLabels = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["og_kush"] = "OG Kush", ["sour_diesel"] = "Sour Diesel", ["green_crack"] = "Green Crack", ["granddaddy_purple"] = "Granddaddy Purple", ["shrooms"] = "Shrooms", ["meth"] = "Meth", ["cocaine"] = "Cocaine", ["flu_medicine"] = "Flu Medicine", ["energy_drink"] = "Energy Drink", ["motor_oil"] = "Motor Oil", ["mega_bean"] = "Mega Bean", ["horse_semen"] = "Horse Semen", ["viagra"] = "Viagor" }; private static readonly HashSet<string> BaseProducts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "og_kush", "sour_diesel", "green_crack", "granddaddy_purple", "meth", "cocaine", "shrooms" }; public static string Canonicalize(string value) { if (string.IsNullOrWhiteSpace(value)) { return value; } string text = value.Trim(); if (!CanonicalAliases.TryGetValue(text, out string value2)) { return text; } return value2; } public static bool IsBaseProduct(string value) { return BaseProducts.Contains(Canonicalize(value)); } public static string FriendlyName(string value) { string text = Canonicalize(value); if (FriendlyLabels.TryGetValue(text, out string value2)) { return value2; } string[] array = text.Replace('_', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { if (array[i].Length != 0) { array[i] = char.ToUpperInvariant(array[i][0]) + array[i].Substring(1); } } return string.Join(" ", array); } } public static class PendingMixLimits { public const int MaxChainSteps = 64; public const int MaxEffectsPerStep = 12; public const int MaxStringLength = 64; public static string OverLimitReason(int stepCount) { return $"Save/Track unavailable: this mix has {stepCount} steps; the shared limit is {64}."; } } public static class PhoneRecipeLimits { public const int MaxRecipes = 50; public const int MaxChainSteps = 64; public const int MaxEffects = 8; public const int MaxStringLength = 64; public const int MaxBodyBytes = 73728; public const int StaleGraceHours = 24; } public sealed class BridgePhoneRecipe { [JsonPropertyName("recipeId")] public string RecipeId { get; set; } = ""; [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("productId")] public string ProductId { get; set; } = ""; [JsonPropertyName("ingredientIds")] public List<string> IngredientIds { get; set; } = new List<string>(); [JsonPropertyName("effects")] public List<string>? Effects { get; set; } [JsonPropertyName("effectsAvailable")] public bool EffectsAvailable { get; set; } [JsonPropertyName("sellPrice")] public double SellPrice { get; set; } [JsonPropertyName("profit")] public double Profit { get; set; } [JsonPropertyName("profitMargin")] public double ProfitMargin { get; set; } [JsonPropertyName("profileMode")] public string ProfileMode { get; set; } = "standard"; [JsonPropertyName("source")] public string Source { get; set; } = "app-saved"; [JsonPropertyName("publishedAt")] public string PublishedAt { get; set; } = ""; [JsonPropertyName("expiresAt")] public string ExpiresAt { get; set; } = ""; } public sealed class BridgePhoneRecipeSnapshot { [JsonPropertyName("schemaVersion")] public int SchemaVersion { get; set; } = 1; [JsonPropertyName("snapshotId")] public string SnapshotId { get; set; } = ""; [JsonPropertyName("publishedAt")] public string PublishedAt { get; set; } = ""; [JsonPropertyName("expiresAt")] public string ExpiresAt { get; set; } = ""; [JsonPropertyName("omittedCount")] public int OmittedCount { get; set; } [JsonPropertyName("recipes")] public List<BridgePhoneRecipe> Recipes { get; set; } = new List<BridgePhoneRecipe>(); } } namespace HylandHelper.PhoneTile { internal sealed record AdvisorDispatchesDestinationCard(GameObject Root, Text UnreadText); internal static class AdvisorDispatchesDestinationCardUi { public static AdvisorDispatchesDestinationCard Create(Transform parent, Action onClick) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_01d4: Unknown result type (might be due to invalid IL or missing references) GameObject val = UIFactory.Panel("AdvisorDispatchesDestination", parent, new Color(0.23f, 0.24f, 0.38f), (Vector2?)null, (Vector2?)null, false); Image component = val.GetComponent<Image>(); Button obj = val.AddComponent<Button>(); ((Selectable)obj).targetGraphic = (Graphic)(object)component; ButtonUtils.AddListener(obj, onClick); RectTransform component2 = ((Component)UIFactory.Text("AdvisorDispatchesDestinationTitle", "Advisor Dispatches", val.transform, 17, (TextAnchor)0, (FontStyle)0)).GetComponent<RectTransform>(); component2.anchorMin = Vector2.zero; component2.anchorMax = Vector2.one; component2.offsetMin = new Vector2(18f, 19f); component2.offsetMax = new Vector2(-72f, -5f); Text obj2 = UIFactory.Text("AdvisorDispatchesDestinationSubtitle", "Incoming only", val.transform, 11, (TextAnchor)6, (FontStyle)0); ((Graphic)obj2).color = new Color(0.68f, 0.72f, 0.79f); RectTransform component3 = ((Component)obj2).GetComponent<RectTransform>(); component3.anchorMin = Vector2.zero; component3.anchorMax = Vector2.one; component3.offsetMin = new Vector2(18f, 5f); component3.offsetMax = new Vector2(-72f, -23f); Text val2 = UIFactory.Text("AdvisorDispatchesDestinationUnread", "0 unread", val.transform, 11, (TextAnchor)5, (FontStyle)0); ((Graphic)val2).color = Color.white; RectTransform component4 = ((Component)val2).GetComponent<RectTransform>(); component4.anchorMin = Vector2.zero; component4.anchorMax = Vector2.one; component4.offsetMin = new Vector2(-86f, 0f); component4.offsetMax = new Vector2(-28f, 0f); RectTransform component5 = ((Component)UIFactory.Text("AdvisorDispatchesDestinationArrow", "›", val.transform, 20, (TextAnchor)5, (FontStyle)0)).GetComponent<RectTransform>(); component5.anchorMin = Vector2.zero; component5.anchorMax = Vector2.one; component5.offsetMin = Vector2.zero; component5.offsetMax = new Vector2(-12f, 0f); LayoutElement obj3 = val.GetComponent<LayoutElement>() ?? val.AddComponent<LayoutElement>(); obj3.minHeight = 52f; obj3.preferredHeight = 52f; obj3.flexibleHeight = 0f; return new AdvisorDispatchesDestinationCard(val, val2); } public static void SetUnreadCount(Text unreadText, int count) { unreadText.text = $"{Math.Clamp(count, 0, 20)} unread"; } } public sealed record AdvisorDispatchesHostView(AdvisorDispatchesRead Cache, AdvisorDispatchesUiSnapshot UiState, int UnreadCount, bool StorageAvailable, bool NotificationAttempted); public sealed class AdvisorDispatchesHostCoordinator { private readonly Func<DateTimeOffset, string?, AdvisorDispatchesRead> _readCache; private readonly AdvisorDispatchesUiState _uiState; private readonly AdvisorDispatchesNotificationCoordinator _notifications; private AdvisorDispatchesUiSnapshot? _lastKnownGoodUiState; private AdvisorDispatchesHostView? _lastKnownGoodView; private string? _activeSaveFingerprint; public AdvisorDispatchesHostCoordinator(Func<DateTimeOffset, string?, AdvisorDispatchesRead> readCache, AdvisorDispatchesUiState uiState, AdvisorDispatchesNotificationCoordinator notifications) { _readCache = readCache; _uiState = uiState; _notifications = notifications; } public void ObserveSaveScope(string? saveFingerprint) { string text = (SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) ? saveFingerprint : null); if (!string.Equals(_activeSaveFingerprint, text, StringComparison.Ordinal)) { _activeSaveFingerprint = text; _lastKnownGoodUiState = null; _lastKnownGoodView = null; } } public AdvisorDispatchesHostView Poll(DateTimeOffset now) { AdvisorDispatchesRead advisorDispatchesRead = _readCache(now, _activeSaveFingerprint); AdvisorDispatchesReadState state = advisorDispatchesRead.State; if ((uint)(state - 3) <= 1u) { return BuildUnusable(advisorDispatchesRead); } if (_activeSaveFingerprint == null || advisorDispatchesRead.ScopeGeneration == null) { return BuildWithoutScope(advisorDispatchesRead); } HashSet<string> validIds = advisorDispatchesRead.Messages.Select((AdvisorDispatchMessage message) => message.Id).ToHashSet<string>(StringComparer.Ordinal); AdvisorDispatchesUiLoad advisorDispatchesUiLoad = _uiState.Load(_activeSaveFingerprint, advisorDispatchesRead.ScopeGeneration, validIds); if (!advisorDispatchesUiLoad.StorageAvailable) { if (_lastKnownGoodView != null && string.Equals(_lastKnownGoodView.UiState.ScopeGeneration, advisorDispatchesRead.ScopeGeneration, StringComparison.Ordinal)) { return _lastKnownGoodView; } return BuildView(advisorDispatchesRead, new AdvisorDispatchesUiSnapshot(_activeSaveFingerprint, advisorDispatchesRead.ScopeGeneration, Array.Empty<string>(), Array.Empty<string>()), storageAvailable: false, notificationAttempted: false); } AdvisorDispatchesUiSnapshot state2 = advisorDispatchesUiLoad.State; AdvisorDispatchesHostView advisorDispatchesHostView = BuildView(advisorDispatchesRead, state2, storageAvailable: true, notificationAttempted: false); _lastKnownGoodUiState = state2; _lastKnownGoodView = advisorDispatchesHostView; if (!advisorDispatchesUiLoad.Repaired && advisorDispatchesRead.State == AdvisorDispatchesReadState.Fresh) { List<AdvisorDispatchMessage> newlyUnnotified = advisorDispatchesRead.Messages.Where((AdvisorDispatchMessage message) => !state2.NotifiedIds.Contains<string>(message.Id, StringComparer.Ordinal)).ToList(); bool notificationAttempted = _notifications.TryNotify(newlyUnnotified, now, delegate(IReadOnlyCollection<string> ids) { if (!_uiState.TryMarkNotified(state2, ids, validIds, out AdvisorDispatchesUiSnapshot next)) { return false; } state2 = next; _lastKnownGoodUiState = next; return true; }); advisorDispatchesHostView = (_lastKnownGoodView = BuildView(advisorDispatchesRead, state2, storageAvailable: true, notificationAttempted)); } return advisorDispatchesHostView; } public AdvisorDispatchesHostView OpenInbox(DateTimeOffset now) { if (_lastKnownGoodView == null) { return Poll(now); } AdvisorDispatchesHostView lastKnownGoodView = _lastKnownGoodView; if (lastKnownGoodView.Cache.ScopeGeneration == null) { return lastKnownGoodView; } HashSet<string> validIds = lastKnownGoodView.Cache.Messages.Select((AdvisorDispatchMessage message) => message.Id).ToHashSet<string>(StringComparer.Ordinal); string[] displayedValidatedIds = lastKnownGoodView.Cache.Messages.Select((AdvisorDispatchMessage message) => message.Id).ToArray(); if (!_uiState.TryMarkRead(lastKnownGoodView.UiState, (IReadOnlyCollection<string>)(object)displayedValidatedIds, validIds, out AdvisorDispatchesUiSnapshot next)) { return lastKnownGoodView; } AdvisorDispatchesHostView advisorDispatchesHostView = BuildView(lastKnownGoodView.Cache, next, lastKnownGoodView.StorageAvailable, notificationAttempted: false); _lastKnownGoodUiState = next; _lastKnownGoodView = advisorDispatchesHostView; return advisorDispatchesHostView; } private static AdvisorDispatchesHostView BuildView(AdvisorDispatchesRead cache, AdvisorDispatchesUiSnapshot state, bool storageAvailable, bool notificationAttempted) { return new AdvisorDispatchesHostView(cache, state, AdvisorDispatchesUiState.CountUnread(cache.Messages, state), storageAvailable, notificationAttempted); } private AdvisorDispatchesHostView BuildUnusable(AdvisorDispatchesRead cache) { AdvisorDispatchesUiSnapshot state = _lastKnownGoodUiState ?? new AdvisorDispatchesUiSnapshot(_activeSaveFingerprint ?? "unavailable", "unavailable", Array.Empty<string>(), Array.Empty<string>()); return BuildView(cache with { Messages = Array.Empty<AdvisorDispatchMessage>() }, state, _lastKnownGoodView?.StorageAvailable ?? false, notificationAttempted: false); } private AdvisorDispatchesHostView BuildWithoutScope(AdvisorDispatchesRead cache) { AdvisorDispatchesUiSnapshot state = _lastKnownGoodUiState ?? new AdvisorDispatchesUiSnapshot(_activeSaveFingerprint ?? "unscoped", "unscoped", Array.Empty<string>(), Array.Empty<string>()); return BuildView(cache with { Messages = Array.Empty<AdvisorDispatchMessage>() }, state, _lastKnownGoodView?.StorageAvailable ?? false, notificationAttempted: false); } } public interface IAdvisorDispatchNotificationSink { bool TryShow(string text); } public sealed class NoOpAdvisorDispatchNotificationSink : IAdvisorDispatchNotificationSink { public bool TryShow(string text) { return false; } } public sealed class AdvisorDispatchesNotificationCoordinator { public static readonly TimeSpan MinimumInterval = TimeSpan.FromSeconds(30.0); private readonly IAdvisorDispatchNotificationSink _sink; private DateTimeOffset? _lastNotificationAt; public AdvisorDispatchesNotificationCoordinator(IAdvisorDispatchNotificationSink sink) { _sink = sink; } public bool TryNotify(IReadOnlyList<AdvisorDispatchMessage> newlyUnnotified, DateTimeOffset now, Func<IReadOnlyCollection<string>, bool> persistNotifiedIds) { List<AdvisorDispatchMessage> list = (from @group in newlyUnnotified.Where((AdvisorDispatchMessage message) => message != null && !string.IsNullOrWhiteSpace(message.Id)).GroupBy<AdvisorDispatchMessage, string>((AdvisorDispatchMessage message) => message.Id, StringComparer.Ordinal) select @group.First()).Take(20).ToList(); if (list.Count == 0 || (_lastNotificationAt.HasValue && now - _lastNotificationAt.Value < MinimumInterval)) { return false; } string[] arg = list.Select((AdvisorDispatchMessage message) => message.Id).ToArray(); bool flag; try { flag = persistNotifiedIds((IReadOnlyCollection<string>)(object)arg); } catch { flag = false; } if (!flag) { return false; } _lastNotificationAt = now; string text = AdvisorDispatchNotificationCopy.ForBatch(list); try { _sink.TryShow(text); } catch { } return true; } } public static class AdvisorDispatchNotificationCopy { public static string ForBatch(IReadOnlyList<AdvisorDispatchMessage> messages) { if (messages.Count == 0) { return ""; } if (messages.Count != 1) { return $"{messages.Count} new Advisor Dispatches"; } return messages[0].Headline; } } public enum AdvisorDispatchesReadState { Missing, Fresh, Expired, Malformed, Unavailable } public sealed record AdvisorDispatchMessage(string Id, string PersonaLabel, string LocalTimestamp, string Headline, string Body); public sealed record AdvisorDispatchesRead(AdvisorDispatchesReadState State, string? ScopeGeneration, string? Revision, DateTimeOffset? CapturedAt, IReadOnlyList<AdvisorDispatchMessage> Messages, string StatusText); public sealed record AdvisorDispatchesViewState(AdvisorDispatchesReadState State, IReadOnlyList<AdvisorDispatchMessage> Messages, AdvisorDispatchesUiSnapshot? UiState, int UnreadCount, string StatusText, bool StorageAvailable); public sealed class AdvisorDispatchesReader { private sealed record ParsedDispatch(string Id, DateTimeOffset CreatedAt, string Headline, string Body, string PersonaId); public const int MaxCacheBytes = 65536; public const int MaxDispatches = 20; public const int MaxScopeGenerationLength = 128; public const int MaxRevisionLength = 128; public const int MaxDispatchIdLength = 64; public const int MaxHeadlineLength = 60; public const int MaxBodyLength = 512; public const int MaxPersonaLength = 64; public const int MaxTimestampLength = 128; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private static readonly HashSet<string> DocumentFields = new HashSet<string>(StringComparer.Ordinal) { "version", "saveFingerprint", "scopeGeneration", "revision", "capturedAt", "dispatches" }; private static readonly HashSet<string> DispatchFields = new HashSet<string>(StringComparer.Ordinal) { "id", "createdAt", "headline", "body", "personaId", "voiceVersion", "triggerId" }; private readonly string _path; private readonly Func<string, LocalFileSignature> _signatureReader; private readonly Func<string, byte[]> _readBytes; private readonly LocalFileRefreshGate _refreshGate; private AdvisorDispatchesRead? _lastRead; private string? _lastSaveFingerprint; public static string DefaultPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper", "advisor-dispatches-cache.json"); public AdvisorDispatchesReader(string path) : this(path, null, null, null) { } internal AdvisorDispatchesReader(string path, Func<string, LocalFileSignature>? signatureReader, Func<string, byte[]>? readBytes, TimeSpan? forcedRecovery) { _path = path; _signatureReader = signatureReader ?? new Func<string, LocalFileSignature>(ReadSignature); _readBytes = readBytes ?? new Func<string, byte[]>(File.ReadAllBytes); _refreshGate = new LocalFileRefreshGate(forcedRecovery ?? TimeSpan.FromSeconds(15.0)); } public AdvisorDispatchesRead Read(DateTimeOffset now, string? activeSaveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(activeSaveFingerprint)) { return Result(AdvisorDispatchesReadState.Missing, "No Advisor Dispatches available yet."); } LocalFileSignature signature = _signatureReader(_path); if (string.Equals(_lastSaveFingerprint, activeSaveFingerprint, StringComparison.Ordinal) && _lastRead != null && !_refreshGate.ShouldRefresh(signature, now, dirty: false, out var _)) { return _lastRead; } AdvisorDispatchesRead advisorDispatchesRead = (_lastRead = ReadCurrent(now, activeSaveFingerprint, signature)); _lastSaveFingerprint = activeSaveFingerprint; if (advisorDispatchesRead.State == AdvisorDispatchesReadState.Unavailable) { _refreshGate.RejectTransient(); } else { _refreshGate.Accept(signature, now); } return advisorDispatchesRead; } private AdvisorDispatchesRead ReadCurrent(DateTimeOffset now, string activeSaveFingerprint, LocalFileSignature signature) { if (!signature.Exists) { return Result(AdvisorDispatchesReadState.Missing, "No Advisor Dispatches available yet."); } byte[] array; try { if (signature.Length <= 0 || signature.Length > 65536) { return Result(AdvisorDispatchesReadState.Malformed, "Advisor Dispatches unavailable right now."); } array = _readBytes(_path); } catch { return Result(AdvisorDispatchesReadState.Unavailable, "Advisor Dispatches unavailable right now."); } if (array.Length == 0 || array.Length > 65536) { return Result(AdvisorDispatchesReadState.Malformed, "Advisor Dispatches unavailable right now."); } try { using JsonDocument jsonDocument = JsonDocument.Parse(StrictUtf8.GetBytes(StrictUtf8.GetString(array))); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object || !HasExactFields(rootElement, DocumentFields)) { return Malformed(); } if (!TryGetInt(rootElement, "version", out var value) || value != 2 || !TryGetBoundedString(rootElement, "saveFingerprint", 67, out string value2) || !SaveGenerationIdentity.IsV2Fingerprint(value2) || !string.Equals(value2, activeSaveFingerprint, StringComparison.Ordinal) || !TryGetBoundedString(rootElement, "scopeGeneration", 128, out string value3) || !TryGetBoundedString(rootElement, "revision", 128, out string value4) || !TryGetTimestamp(rootElement, "capturedAt", out var value5) || rootElement.GetProperty("dispatches").ValueKind != JsonValueKind.Array) { return Malformed(); } JsonElement property = rootElement.GetProperty("dispatches"); if (property.GetArrayLength() > 20) { return Malformed(); } HashSet<string> ids = new HashSet<string>(StringComparer.Ordinal); List<ParsedDispatch> list = new List<ParsedDispatch>(property.GetArrayLength()); foreach (JsonElement item in property.EnumerateArray()) { if (!TryParseDispatch(item, value5, ids, out ParsedDispatch dispatch)) { return Malformed(); } list.Add(dispatch); } List<AdvisorDispatchMessage> list2 = list.Where((ParsedDispatch parsedDispatch) => now < parsedDispatch.CreatedAt + TimeSpan.FromDays(7.0)).Select(Project).ToList(); int num = ((list.Count <= 0 || list2.Count != 0) ? 1 : 2); return new AdvisorDispatchesRead(StatusText: (num == 2) ? "Dispatches expired. New dispatches unavailable." : ((list2.Count == 0) ? "No Advisor Dispatches yet." : "Incoming only"), State: (AdvisorDispatchesReadState)num, ScopeGeneration: value3, Revision: value4, CapturedAt: value5, Messages: list2); } catch { return Malformed(); } } private static LocalFileSignature ReadSignature(string path) { try { FileInfo fileInfo = new FileInfo(path); return fileInfo.Exists ? new LocalFileSignature(Exists: true, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks) : LocalFileSignature.Missing; } catch { return LocalFileSignature.Missing; } } private static AdvisorDispatchMessage Project(ParsedDispatch dispatch) { return new AdvisorDispatchMessage(dispatch.Id, PersonaLabel(dispatch.PersonaId), dispatch.CreatedAt.ToLocalTime().ToString("g", CultureInfo.CurrentCulture), dispatch.Headline, dispatch.Body); } private static string PersonaLabel(string personaId) { return personaId switch { "default" => "Advisor", "roast" => "Roast Master", "kingpin" => "Hardened Kingpin", "accountant" => "Nervous Accountant", "chemist" => "Shady Chemist", _ => "Advisor", }; } private static bool TryParseDispatch(JsonElement element, DateTimeOffset capturedAt, HashSet<string> ids, out ParsedDispatch? dispatch) { dispatch = null; if (element.ValueKind != JsonValueKind.Object || !HasExactFields(element, DispatchFields) || !TryGetBoundedString(element, "id", 64, out string value) || !ids.Add(value) || !TryGetTimestamp(element, "createdAt", out var value2) || value2 > capturedAt || capturedAt - value2 > TimeSpan.FromDays(7.0) || !TryGetBoundedString(element, "headline", 60, out string value3) || !TryGetBoundedString(element, "body", 512, out string value4) || !TryGetBoundedString(element, "personaId", 64, out string value5) || !TryGetBoundedString(element, "voiceVersion", 64, out string value6) || !TryGetBoundedString(element, "triggerId", 64, out value6)) { return false; } dispatch = new ParsedDispatch(value, value2, value3, value4, value5); return true; } private static bool HasExactFields(JsonElement element, HashSet<string> allowed) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (JsonProperty item in element.EnumerateObject()) { if (!allowed.Contains(item.Name) || !hashSet.Add(item.Name)) { return false; } } return hashSet.SetEquals(allowed); } private static bool TryGetInt(JsonElement element, string name, out int value) { value = 0; if (element.TryGetProperty(name, out var value2) && value2.ValueKind == JsonValueKind.Number) { return value2.TryGetInt32(out value); } return false; } private static bool TryGetBoundedString(JsonElement element, string name, int maxLength, out string value) { value = ""; if (!element.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.String) { return false; } value = value2.GetString() ?? ""; if (!string.IsNullOrWhiteSpace(value)) { return value.Length <= maxLength; } return false; } private static bool TryGetTimestamp(JsonElement element, string name, out DateTimeOffset value) { value = default(DateTimeOffset); if (!TryGetBoundedString(element, name, 128, out string value2)) { return false; } return DateTimeOffset.TryParse(value2, CultureInfo.InvariantCulture, DateTimeStyles.None, out value); } private static AdvisorDispatchesRead Malformed() { return Result(AdvisorDispatchesReadState.Malformed, "Advisor Dispatches unavailable right now."); } private static AdvisorDispatchesRead Result(AdvisorDispatchesReadState state, string text) { return new AdvisorDispatchesRead(state, null, null, null, Array.Empty<AdvisorDispatchMessage>(), text); } } public sealed record AdvisorDispatchesUiSnapshot(string SaveFingerprint, string ScopeGeneration, IReadOnlyList<string> ReadIds, IReadOnlyList<string> NotifiedIds); public sealed record AdvisorDispatchesUiLoad(AdvisorDispatchesUiSnapshot State, bool StorageAvailable, bool Repaired); public sealed class AdvisorDispatchesUiState { private sealed class UiDocument { [JsonPropertyName("version")] public int Version { get; set; } [JsonPropertyName("saveFingerprint")] public string SaveFingerprint { get; set; } = ""; [JsonPropertyName("scopeGeneration")] public string ScopeGeneration { get; set; } = ""; [JsonPropertyName("readIds")] public string[] ReadIds { get; set; } = Array.Empty<string>(); [JsonPropertyName("notifiedIds")] public string[] NotifiedIds { get; set; } = Array.Empty<string>(); } public const int MaxStateBytes = 8192; public const int MaxIds = 20; public const int MaxIdLength = 64; public const int MaxScopeGenerationLength = 128; private static readonly HashSet<string> Fields = new HashSet<string>(StringComparer.Ordinal) { "version", "saveFingerprint", "scopeGeneration", "readIds", "notifiedIds" }; private readonly string _path; private readonly Action<string, string> _replaceFile; private readonly Action<string> _deleteFile; private readonly Func<string, byte[]> _readFile; private readonly Func<string, LocalFileSignature> _signatureReader; private readonly Func<DateTimeOffset> _clock; private readonly LocalFileRefreshGate _refreshGate; private AdvisorDispatchesUiLoad? _lastLoad; private string? _lastSaveFingerprint; private string? _lastScopeGeneration; private string? _lastValidIds; public static string DefaultPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "LocalLow", "TVGS", "Schedule I", "HylandHelper", "advisor-dispatches-ui.json"); public AdvisorDispatchesUiState(string path) : this(path, null, null) { } internal AdvisorDispatchesUiState(string path, Action<string, string>? replaceFile, Action<string>? deleteFile, Func<string, byte[]>? readFile = null, Func<string, LocalFileSignature>? signatureReader = null, Func<DateTimeOffset>? clock = null, TimeSpan? forcedRecovery = null) { _path = path; _replaceFile = replaceFile ?? ((Action<string, string>)delegate(string source, string destination) { File.Move(source, destination, overwrite: true); }); _deleteFile = deleteFile ?? new Action<string>(File.Delete); _readFile = readFile ?? new Func<string, byte[]>(File.ReadAllBytes); _signatureReader = signatureReader ?? new Func<string, LocalFileSignature>(ReadSignature); _clock = clock ?? ((Func<DateTimeOffset>)(() => DateTimeOffset.UtcNow)); _refreshGate = new LocalFileRefreshGate(forcedRecovery ?? TimeSpan.FromSeconds(15.0)); } public AdvisorDispatchesUiLoad Load(string saveFingerprint, string scopeGeneration, IReadOnlySet<string> validIds) { AdvisorDispatchesUiSnapshot advisorDispatchesUiSnapshot = Empty(saveFingerprint, scopeGeneration); if (!SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint) || !IsBounded(scopeGeneration, 128) || !ValidIds(validIds)) { return new AdvisorDispatchesUiLoad(advisorDispatchesUiSnapshot, StorageAvailable: false, Repaired: false); } LocalFileSignature signature = _signatureReader(_path); string text = string.Join("\u001f", validIds.OrderBy<string, string>((string id) => id, StringComparer.Ordinal)); if (string.Equals(_lastSaveFingerprint, saveFingerprint, StringComparison.Ordinal) && string.Equals(_lastScopeGeneration, scopeGeneration, StringComparison.Ordinal) && string.Equals(_lastValidIds, text, StringComparison.Ordinal) && _lastLoad != null && !_refreshGate.ShouldRefresh(signature, _clock(), dirty: false, out var _)) { return _lastLoad; } AdvisorDispatchesUiLoad result = (_lastLoad = LoadCurrent(advisorDispatchesUiSnapshot, saveFingerprint, scopeGeneration, validIds, signature)); _lastSaveFingerprint = saveFingerprint; _lastScopeGeneration = scopeGeneration; _lastValidIds = text; _refreshGate.Accept(signature, _clock()); return result; } private AdvisorDispatchesUiLoad LoadCurrent(AdvisorDispatchesUiSnapshot empty, string saveFingerprint, string scopeGeneration, IReadOnlySet<string> validIds, LocalFileSignature signature) { if (!signature.Exists) { return new AdvisorDispatchesUiLoad(empty, StorageAvailable: true, Repaired: false); } try { if (signature.Length <= 0 || signature.Length > 8192) { return new AdvisorDispatchesUiLoad(empty, StorageAvailable: false, Repaired: false); } if (!TryParse(_readFile(_path), out AdvisorDispatchesUiSnapshot snapshot)) { return new AdvisorDispatchesUiLoad(empty, StorageAvailable: false, Repaired: false); } if (!string.Equals(snapshot.SaveFingerprint, saveFingerprint, StringComparison.Ordinal) || !string.Equals(snapshot.ScopeGeneration, scopeGeneration, StringComparison.Ordinal)) { return new AdvisorDispatchesUiLoad(empty, StorageAvailable: true, Repaired: false); } AdvisorDispatchesUiSnapshot advisorDispatchesUiSnapshot = Reconcile(snapshot, saveFingerprint, scopeGeneration, validIds); if (Equivalent(snapshot, advisorDispatchesUiSnapshot)) { return new AdvisorDispatchesUiLoad(advisorDispatchesUiSnapshot, StorageAvailable: true, Repaired: false); } return TryPersist(advisorDispatchesUiSnapshot) ? new AdvisorDispatchesUiLoad(advisorDispatchesUiSnapshot, StorageAvailable: true, Repaired: true) : new AdvisorDispatchesUiLoad(advisorDispatchesUiSnapshot, StorageAvailable: false, Repaired: true); } catch { return new AdvisorDispatchesUiLoad(empty, StorageAvailable: false, Repaired: false); } } public bool TryMarkRead(AdvisorDispatchesUiSnapshot current, IReadOnlyCollection<string> displayedValidatedIds, IReadOnlySet<string> validIds, out AdvisorDispatchesUiSnapshot next) { return TryMark(current, displayedValidatedIds, validIds, read: true, out next); } public bool TryMarkNotified(AdvisorDispatchesUiSnapshot current, IReadOnlyCollection<string> notifiedValidatedIds, IReadOnlySet<string> validIds, out AdvisorDispatchesUiSnapshot next) { return TryMark(current, notifiedValidatedIds, validIds, read: false, out next); } public static AdvisorDispatchesUiSnapshot Reconcile(AdvisorDispatchesUiSnapshot? persisted, string saveFingerprint, string scopeGeneration, IReadOnlySet<string> validIds) { if (persisted == null || !string.Equals(persisted.SaveFingerprint, saveFingerprint, StringComparison.Ordinal) || !string.Equals(persisted.ScopeGeneration, scopeGeneration, StringComparison.Ordinal)) { return Empty(saveFingerprint, scopeGeneration); } return new AdvisorDispatchesUiSnapshot(saveFingerprint, scopeGeneration, Intersect(persisted.ReadIds, validIds), Intersect(persisted.NotifiedIds, validIds)); } public static int CountUnread(IReadOnlyCollection<AdvisorDispatchMessage> displayedMessages, AdvisorDispatchesUiSnapshot state) { return displayedMessages.Count((AdvisorDispatchMessage message) => !state.ReadIds.Contains<string>(message.Id, StringComparer.Ordinal)); } private bool TryMark(AdvisorDispatchesUiSnapshot current, IReadOnlyCollection<string> candidateIds, IReadOnlySet<string> validIds, bool read, out AdvisorDispatchesUiSnapshot next) { string[] second = candidateIds.Where((string id) => validIds.Contains(id) && IsBounded(id, 64)).Distinct<string>(StringComparer.Ordinal).Take(20) .ToArray(); string[] array = (read ? current.ReadIds : current.NotifiedIds).Concat(second).Distinct<string>(StringComparer.Ordinal).Take(20) .OrderBy<string, string>((string id) => id, StringComparer.Ordinal) .ToArray(); next = (read ? current with { ReadIds = array } : current with { NotifiedIds = array }); if (Equivalent(current, next)) { return true; } if (TryPersist(next)) { _refreshGate.Reset(); _lastLoad = null; return true; } next = current; return false; } private static LocalFileSignature ReadSignature(string path) { try { FileInfo fileInfo = new FileInfo(path); return fileInfo.Exists ? new LocalFileSignature(Exists: true, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks) : LocalFileSignature.Missing; } catch { return LocalFileSignature.Missing; } } private bool TryPersist(AdvisorDispatchesUiSnapshot state) { try { if (!SaveGenerationIdentity.IsV2Fingerprint(state.SaveFingerprint) || !IsBounded(state.ScopeGeneration, 128)) { return false; } byte[] array = JsonSerializer.SerializeToUtf8Bytes(new UiDocument { Version = 2, SaveFingerprint = state.SaveFingerprint, ScopeGeneration = state.ScopeGeneration, ReadIds = state.ReadIds.ToArray(), NotifiedIds = state.NotifiedIds.ToArray() }, new JsonSerializerOptions { WriteIndented = true }); if (array.Length > 8192) { return false; } if (File.Exists(_path) && _readFile(_path).SequenceEqual(array)) { return true; } string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrWhiteSpace(directoryName)) { Directory.CreateDirectory(directoryName); } string text = $"{_path}.{Guid.NewGuid():N}.tmp"; try { using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } _replaceFile(text, _path); text = ""; return true; } finally { if (!string.IsNullOrEmpty(text)) { try { File.Delete(text); } catch { } } } } catch { return false; } } private static bool TryParse(byte[] bytes, out AdvisorDispatchesUiSnapshot? snapshot) { snapshot = null; try { using JsonDocument jsonDocument = JsonDocument.Parse(bytes); JsonElement rootElement = jsonDocument.RootElement; if (rootElement.ValueKind != JsonValueKind.Object || !ExactFields(rootElement) || !rootElement.TryGetProperty("version", out var value) || value.ValueKind != JsonValueKind.Number || value.GetInt32() != 2 || !TrySaveFingerprint(rootElement, out string saveFingerprint) || !TryScope(rootElement, out string scope) || !TryIds(rootElement, "readIds", out IReadOnlyList<string> ids) || !TryIds(rootElement, "notifiedIds", out IReadOnlyList<string> ids2)) { return false; } snapshot = new AdvisorDispatchesUiSnapshot(saveFingerprint, scope, ids, ids2); return true; } catch { return false; } } private static bool ExactFields(JsonElement root) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (JsonProperty item in root.EnumerateObject()) { if (!Fields.Contains(item.Name) || !hashSet.Add(item.Name)) { return false; } } return hashSet.SetEquals(Fields); } private static bool TryScope(JsonElement root, out string scope) { scope = ""; if (!root.TryGetProperty("scopeGeneration", out var value) || value.ValueKind != JsonValueKind.String) { return false; } scope = value.GetString() ?? ""; return IsBounded(scope, 128); } private static bool TrySaveFingerprint(JsonElement root, out string saveFingerprint) { saveFingerprint = ""; if (!root.TryGetProperty("saveFingerprint", out var value) || value.ValueKind != JsonValueKind.String) { return false; } saveFingerprint = value.GetString() ?? ""; return SaveGenerationIdentity.IsV2Fingerprint(saveFingerprint); } private static bool TryIds(JsonElement root, string name, out IReadOnlyList<string> ids) { ids = Array.Empty<string>(); if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array || value.GetArrayLength() > 20) { return false; } List<string> list = new List<string>(); foreach (JsonElement item in value.EnumerateArray()) { if (item.ValueKind != JsonValueKind.String) { return false; } string text = item.GetString() ?? ""; if (!IsBounded(text, 64) || list.Contains<string>(text, StringComparer.Ordinal)) { return false; } list.Add(text); } ids = list.OrderBy<string, string>((string id) => id, StringComparer.Ordinal).ToArray(); return true; } private static IReadOnlyList<string> Intersect(IEnumerable<string> ids, IReadOnlySet<string> validIds) { return ids.Where(validIds.Contains).Distinct<string>(StringComparer.Ordinal).Take(20) .OrderBy<string, string>((string id) => id, StringComparer.Ordinal) .ToArray(); } private static bool ValidIds(IEnumerable<string> ids) { if (ids.Count() <= 20) { return ids.All((string id) => IsBounded(id, 64)); } return false; } private static bool IsBounded(string? value, int maxLength) { if (!string.IsNullOrWhiteSpace(value)) { return value.Length <= maxLength; } return false; } private static AdvisorDispatchesUiSnapshot Empty(string saveFingerprint, string scope) { return new AdvisorDispatchesUiSnapshot(saveFingerprint, scope, Array.Empty<string>(), Array.Empty<string>()); } private static bool Equivalent(AdvisorDispatchesUiSnapshot left, AdvisorDispatchesUiSnapshot right) { if (string.Equals(left.SaveFingerprint, right.SaveFingerprint, StringComparison.Ordinal) && string.Equals(left.ScopeGeneration, right.ScopeGeneration, StringComparison.Ordinal) && left.ReadIds.SequenceEqual<string>(right.ReadIds, StringComparer.Ordinal)) { return left.NotifiedIds.SequenceEqual<string>(right.NotifiedIds, StringComparer.Ordinal); } return false; } } internal static class BoundedHttpResponse { public static async Task<byte[]> ReadBytesAsync(HttpResponseMessage response, int maxBytes, CancellationToken cancellationToken) { if (maxBytes <= 0) { throw new ArgumentOutOfRangeException("maxBytes"); } long? contentLength = response.Content.Headers.ContentLength; if (contentLength.HasValue && contentLength.GetValueOrDefault() > 0 && response.Content.Headers.ContentLength > maxBytes) { throw new InvalidDataException("Backend response exceeded the local byte limit."); } byte[] result; await using (Stream source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) { using MemoryStream destination = new MemoryStream(); byte[] buffer = new byte[Math.Min(8192, maxBytes + 1)]; while (true) { int num = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (num == 0) { break; } if (destination.Length + num > maxBytes) { throw new InvalidDataException("Backend response exceeded the local byte limit."); } destination.Write(buffer, 0, num); } result = destination.ToArray(); } return result; } } internal sealed class BridgeCommandFileWriter { private readonly string _path; public BridgeCommandFileWriter(string path) { _path = path; } public bool TryWrite(object command) { string text = null; try { if (File.Exists(_path)) { return false; } string directoryName = Path.GetDirectoryName(_path); if (string.IsNullOrWhiteSpace(directoryName)) { return false; } Directory.CreateDirectory(directoryName); text = $"{_path}.{Guid.NewGuid():N}.tmp"; File.WriteAllText(text, JsonSerializer.Serialize(command)); File.Move(text, _path, overwrite: false); text = null; return true; } catch { return false; } finally { if (text != null) { try { File.Delete(text); } catch { } } } } } internal sealed record BridgeCommandResult(string RequestId, string Status); internal static class BridgeCommandResultReader { private const int MaxResultBytes = 4096; private static readonly HashSet<string> Fields = new HashSet<string>(StringComparer.Ordinal) { "version", "requestId", "status", "saveFingerprint" }; internal static IReadOnlyList<BridgeCommandResult> Take(string directory, string? activeSaveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(activeSaveFingerprint) || string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) { return Array.Empty<BridgeCommandResult>(); } try { return TakePaths(Directory.GetFiles(directory, "bridge-command-result-*.json"), activeSaveFingerprint); } catch { return Array.Empty<BridgeCommandResult>(); } } internal static IReadOnlyList<BridgeCommandResult> TakeKnown(string directory, IEnumerable<string> requestIds, string? activeSaveFingerprint) { if (!SaveGenerationIdentity.IsV2Fingerprint(activeSaveFingerprint) || string.IsNullOrWhiteSpace(directory)) { return Array.Empty<BridgeCommandResult>(); } List<string> list = new List<string>(); foreach (string requestId in requestIds) { if (IsSafeRequestId(requestId)) { string text = Path.Combine(directory, "bridge-command-result-" + requestId + ".json"); if (File.Exists(text)) { list.Add(text); } } } return TakePaths(list, activeSaveFingerprint); } internal static IReadOnlyList<BridgeCommandResult> TakePaths(IEnumerable<string> paths, string? activeSaveFingerprint) { List<BridgeCommandResult> list = new List<BridgeCommandResult>(); if (!SaveGenerationIdentity.IsV2Fingerprint(activeSaveFingerprint)) { return list; } foreach (string path in paths) { try { if (new FileInfo(path).Length <= 0 || new FileInfo(path).Length > 4096) { continue; } using JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllBytes(path)); JsonElement rootElement = jsonDocument.RootElement; if (HasExactFields(rootElement) && TryInt(rootElement, "version", out var value) && value == 1 && TryString(rootElement, "requestId", 64, out string value2) && IsSafeRequestId(value2) && TryString(rootElement, "status", 32, out string value3) && IsSafeStatus(value3) && TryString(rootElement, "saveFingerprint", 67, out string value4) && SaveGenerationIdentity.IsV2Fingerprint(value4) && string.Equals(value4, activeSaveFingerprint, StringComparison.Ordinal) && string.Equals(Path.GetFileNameWithoutExtension(path), "bridge-command-result-" + value2, StringComparison.Ordinal)) { list.Add(new BridgeCommandResult(value2, value3)); } } catch { } finally { try { File.Delete(path); } catch { } } } return list; } private static bool HasExactFields(JsonElement root) { if (root.ValueKind != JsonValueKind.Object) { return false; } HashSet<string> hashSet = new HashSet<string>(Fields, StringComparer.Ordinal); foreach (JsonProperty item in root.EnumerateObject()) { if (!hashSet.Remove(item.Name)) { return false; } } return hashSet.Count == 0; } private static bool TryInt(JsonElement root, string name, out int value) { value = 0; if (root.TryGetProperty(name, out var value2) && value2.ValueKind == JsonValueKind.Number) { return value2.TryGetInt32(out value); } return false; } private static bool TryString(JsonElement root, string name, int maxLength, out string value) { value = ""; if (!root.TryGetProperty(name, out var value2) || value2.ValueKind != JsonValueKind.String) { return false; } value = value2.GetString() ?? ""; if (value.Length > 0) { return value.Length <= maxLength; } return false; } private static bool IsSafeRequestId(string requestId) { foreach (char c in requestId) { if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') { return false; } } return true; } private static bool IsSafeStatus(string status) { switch (status) { case "sent": case "alreadySent": case "failed": case "unauthorized": return true; default: return false; } } } public sealed class BridgeStatus { public string? Status; public string? PairingCode; public string? SaveLine; public string? LatestLine; public long Sequence; public bool? CanUnlink; } internal sealed record BridgeStatusFileRead(BridgeStatus? Status, string? TileChallenge, LocalFileSignature Signature, bool RefreshAttempted, string? RuntimeSession = null); internal sealed class BridgeStatusFileReader { private const int MaxStatusBytes = 65536; private const int MaxRuntimeSessionLength = 64; private static readonly HashSet<string> ChallengeFields = new HashSet<string>(StringComparer.Ordinal) { "version", "challenge", "protocolVersion" }; private readonly string _path; private readonly Func<string, LocalFileSignature> _signatureReader; private readonly Func<string, byte[]> _readBytes; private readonly LocalFileRefreshGate _gate; private BridgeStatusFileRead _last; internal BridgeStatusFileReader(string path, Func<string, LocalFileSignature>? signatureReader = null, Func<string, byte[]>? readBytes = null, TimeSpan? forcedRecovery = null) { _path = path; _signatureReader = signatureReader ?? new Func<string, LocalFileSignature>(ReadSignature); _readBytes = readBytes ?? new Func<string, byte[]>(File.ReadAllBytes); _gate = new LocalFileRefreshGate(forcedRecovery ?? TimeSpan.FromSeconds(15.0)); _last = new BridgeStatusFileRead(null, null, LocalFileSignature.Missing, RefreshAttempted: false); } internal BridgeStatusFileRead ReadIfDue(DateTimeOffset now, bool dirty) { LocalFileSignature signature = _signatureReader(_path); if (!_gate.ShouldRefresh(signature, now, dirty, out var _)) { return _last with { RefreshAttempted = false }; } try { if (!signature.Exists || signature.Length <= 0 || signature.Length > 65536) { return Reject(signature); } byte[] array = _readBytes(_path); if (array.Length == 0 || array.Length > 65536) { return Reject(signature); } using JsonDocument jsonDocument = JsonDocument.Parse(array); if (jsonDocument.RootElement.ValueKind != JsonValueKind.Object) { return Reject(signature); } JsonElement rootElement = jsonDocument.RootElement; BridgeStatus status = new BridgeStatus { Status = GetString(rootElement, "status"), PairingCode = GetString(rootElement, "pairingCode"), SaveLine = GetString(rootElement, "saveLine"), LatestLine = GetString(rootElement, "latestLine"), Sequence = GetLong(rootElement, "sequence"), CanUnlink = GetBoolean(rootElement, "canUnlink") }; if (!TryReadChallenge(rootElement, out string challenge)) { challenge = null; } _gate.Accept(signature, now); _last = new BridgeStatusFileRead(status, challenge, signature, RefreshAttempted: true, ReadRuntimeSession(rootElement)); return _last; } catch { return Reject(signature); } } private BridgeStatusFileRead Reject(LocalFileSignature signature) { _gate.RejectTransient(); return new BridgeStatusFileRead(null, null, signature, RefreshAttempted: true); } private static LocalFileSignature ReadSignature(string path) { try { FileInfo fileInfo = new FileInfo(path); return fileInfo.Exists ? new LocalFileSignature(Exists: true, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks) : LocalFileSignature.Missing; } catch { return LocalFileSignature.Missing; } } internal static bool TryReadChallenge(JsonElement root, out string? challenge) { challenge = null; if (!root.TryGetProperty("tileChallenge", out var value) || value.ValueKind != JsonValueKind.Object) { return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (JsonProperty item in value.EnumerateObject()) { if (!ChallengeFields.Contains(item.Name) || !hashSet.Add(item.Name)) { return false; } } if (!hashSet.SetEquals(ChallengeFields) || !value.TryGetProperty("version", out var value2) || value2.ValueKind != JsonValueKind.Number || value2.GetInt32() != 1 || !value.TryGetProperty("protocolVersion", out var value3) || value3.ValueKind != JsonValueKind.Number || value3.GetInt32() != 1 || !value.TryGetProperty("challenge", out var value4) || value4.ValueKind != JsonValueKind.String) { return false; } string text = value4.GetString(); if (string.IsNullOrWhiteSpace(text) || text.Length > 128) { return false; } challenge = text; return true; } private static string? ReadRuntimeSession(JsonElement root) { string text = GetString(root, "runtimeSession")?.Trim(); if (!string.IsNullOrEmpty(text) && text.Length <= 64) { return text; } return null; } private static string? GetString(JsonElement root, string name) { if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String) { return null; } return value.GetString(); } private static long GetLong(JsonElement root, string name) { if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Number || !value.TryGetInt64(out var value2)) { return 0L; } return value2; } private static bool? GetBoolean(JsonElement root, string name) { if (!root.TryGetProperty(name, out var value)) { return null; } return value.ValueKind switch { JsonValueKind.True => true, JsonValueKind.False => false, _ => null, }; } } internal static class CurrentSaveSlotReader { private const string LoadManagerTypeName = "Il2CppScheduleOne.Persistence.LoadManager, Assembly-CSharp"; private static readonly BindingFlags PublicInstanceOrStatic = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public; private static readonly Regex SaveSlotPattern = new Regex("(?<![A-Za-z0-9_])SaveGame_[0-9]+(?![A-Za-z0-9_])", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); public static string? TryRead() { string source; return TryRead(out source); } public static string? TryRead(out string? source) { source = null; try { Type type = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly assembly) => string.Equals(assembly.GetName().Name, "Assembly-CSharp", StringComparison.Ordinal))?.GetType("Il2CppScheduleOne.Persistence.LoadManager, Assembly-CSharp".Split(',')[0].Trim(), throwOnError: false); if (type == null) { source = "Assembly-CSharp LoadManager type unavailable"; return null; } PropertyInfo propertyInfo = FindProperty(type, "Instance") ?? FindProperty(type, "instance"); object obj = propertyInfo?.GetValue(null); if (obj == null) { source = ((propertyInfo == null) ? "LoadManager singleton property unavailable" : "LoadManager singleton instance unavailable"); return null; } PropertyInfo propertyInfo2 = FindProperty(type, "ActiveSaveInfo"); object obj2 = propertyInfo2?.GetValue(obj); if (obj2 == null && propertyInfo2 == null) { source = "LoadManager.ActiveSaveInfo property unavailable"; } string text = ExtractSaveSlot(FindProperty(obj2?.GetType(), "SavePath")?.GetValue(obj2) as string); if (text != null) { source = "LoadManager.ActiveSaveInfo.SavePath"; return text; } if (FindProperty(obj2?.GetType(), "SaveSlotNumber")?.GetValue(obj2) is int slotNumber) { text = FormatSaveSlotNumber(slotNumber); if (text != null) { source = "LoadManager.ActiveSaveInfo.SaveSlotNumber"; return text; } } text = ExtractSaveSlot(FindProperty(type, "LoadedGameFolderPath")?.GetValue(obj) as string); if (text != null) { source = "LoadManager.LoadedGameFolderPath"; } if (text == null && source == null) { source = "LoadManager identity properties contained no SaveGame_N value"; } return text; } catch (Exception ex) { source = "LoadManager identity read failed: " + ex.GetType().Name; } return null; } public static bool TryReadActiveDirectory(out string? saveDirectory, out string? source) { saveDirectory = null; source = null; try { Type type = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly assembly) => string.Equals(assembly.GetName().Name, "Assembly-CSharp", StringComparison.Ordinal))?.GetType("Il2CppScheduleOne.Persistence.LoadManager, Assembly-CSharp".Split(',')[0].Trim(), throwOnError: false); object obj = FindProperty(type, "Instance")?.GetValue(null) ?? FindProperty(type, "instance")?.GetValue(null); if (obj == null) { return false; } object obj2 = FindProperty(type, "ActiveSaveInfo")?.GetValue(obj); if (TryValidateActiveDirectory(FindProperty(obj2?.GetType(), "SavePath")?.GetValue(obj2) as string, out saveDirectory)) { source = "active-save-path"; return true; } if (TryValidateActiveDirectory(FindProperty(type, "LoadedGameFolderPath")?.GetValue(obj) as string, out saveDirectory)) { source = "loaded-game-folder"; return true; } } catch { source = "runtime-save-read-failed"; } return false; } internal static bool TryValidateActiveDirectory(string? value, out string? saveDirectory) { saveDirectory = null; if (string.IsNullOrWhiteSpace(value)) { return false; } try { string text = Path.GetFullPath(value).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (!Directory.Exists(text) || ExtractSaveSlot(Path.GetFileName(text)) == null) { return false; } string directoryName = Path.GetDirectoryName(text); string path = (string.IsNullOrWhiteSpace(directoryName) ? null : Path.GetDirectoryName(directoryName)); if (string.IsNullOrWhiteSpace(directoryName) || string.IsNullOrWhiteSpace(Path.GetFileName(directoryName)) || !string.Equals(Path.GetFileName(path), "Saves", StringComparison.OrdinalIgnoreCase)) { return false; } saveDirectory = text; return true; } catch { return false; } } private static PropertyInfo? FindProperty(Type? type, string name) { Type type2 = type; while (type2 != null) { PropertyInfo property = type2.GetProperty(name, PublicInstanceOrStatic | BindingFlags.DeclaredOnly); if (property != null) { return property; } type2 = type2.BaseType; } return null; } public static string? FormatSaveSlotNumber(int slotNumber) { if (slotNumber < 1 || slotNumber > 99) { return null; } return $"SaveGame_{slotNumber}"; } public static string? ExtractSaveSlot(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } Match match = SaveSlotPattern.Match(value); if (!match.Success) { return null; } return match.Value; } } internal enum DiscoveredMixBaseFilter { All, Weed, Meth, Cocaine, Shrooms } internal sealed class DiscoveredMixFilterState { public IReadOnlyList<DiscoveredMixBaseFilter> BaseFilters { get; private set; } = new DiscoveredMixBaseFilter[1]; public IReadOnlyList<ObservedEffectFilter> EffectFilters { get; private set; } = new ObservedEffectFilter[1] { new ObservedEffectFilter { Key = "", Label = "All effects" } }; public DiscoveredMixBaseFilter SelectedBase { get; private set; } public string SelectedEffect { get; private set; } = ""; public bool PricedOnly { get; private set; } public void Refresh(IEnumerable<LocalMix> mixes) { List<LocalMix> mixes2 = mixes.ToList(); BaseFilters = DiscoveredMixFilterCatalog.GetBaseFilters(mixes2); EffectFilters = ObservedEffects.GetFilters(mixes2); if (!BaseFilters.Contains(SelectedBase)) { SelectedBase = DiscoveredMixBaseFilter.All; } if (!EffectFilters.Any((ObservedEffectFilter filter) => filter.Key == SelectedEffect)) { SelectedEffect = ""; } } public void SelectBase(DiscoveredMixBaseFilter filter) { if (BaseFilters.Contains(filter)) { SelectedBase = filter; } } public void SelectEffect(string filterKey) { if (EffectFilters.Any((ObservedEffectFilter filter) => filter.Key == filterKey)) { SelectedEffect = filterKey; } } public void SetPricedOnly(bool value) { PricedOnly = value; } public bool Matches(LocalMix mix, bool pricedAvailable) { if ((SelectedBase == DiscoveredMixBaseFilter.All || DiscoveredMixFilterCatalog.ClassifyBase(mix.Base) == SelectedBase) && ObservedEffects.Matches(mix, SelectedEffect)) { return !PricedOnly || pricedAvailable; } return false; } } internal static class DiscoveredMixFilterCatalog { public static List<DiscoveredMixBaseFilter> GetBaseFilters(IEnumerable<LocalMix> mixes) { List<DiscoveredMixBaseFilter> list = new List<DiscoveredMixBaseFilter> { DiscoveredMixBaseFilter.All }; foreach (LocalMix mix in mixes) { DiscoveredMixBaseFilter? discoveredMixBaseFilter = ClassifyBase(mix.Base); if (discoveredMixBaseFilter.HasValue && !list.Contains(discoveredMixBaseFilter.Value)) { list.Add(discoveredMixBaseFilter.Value); } } return list; } public static DiscoveredMixBaseFilter? ClassifyBase(string baseId) { switch (baseId.Replace("_", "", StringComparison.Ordinal).ToLowerInvariant()) { case "meth": return DiscoveredMixBaseFilter.Meth; case "cocaine": return DiscoveredMixBaseFilter.Cocaine; case "shrooms": case "shroom": return DiscoveredMixBaseFilter.Shrooms; case "ogkush": case "greencrack": case "sourdiesel": case "granddaddypurple": return DiscoveredMixBaseFilter.Weed; default: return null; } } public static string FilterLabel(DiscoveredMixBaseFilter filter) { return filter switch { DiscoveredMixBaseFilter.All => "All", DiscoveredMixBaseFilter.Weed => "Weed", DiscoveredMixBaseFilter.Meth => "Meth", DiscoveredMixBaseFilter.Cocaine => "Cocaine", DiscoveredMixBaseFilter.Shrooms => "Shrooms", _ => filter.ToString(), }; } } public sealed class EconomicsRequestQueue<T> { public const int MaxConcurrent = 3; private readonly Queue<(string Key, T Value)> _pending = new Queue<(string, T)>(); private readonly HashSet<string> _knownKeys = new HashSet<string>(); private int _active; public int ActiveCount => _active; public int PendingCount => _pending.Count; public bool Enqueue(string key, T value) { if (!_knownKeys.Add(key)) { return false; } _pending.Enqueue((key, value)); return true; } public bool TryStart(out T value) { if (_active >= 3 || _pending.Count == 0) { value = default(T); return false; } (string, T) tuple = _pending.Dequeue(); _active++; value = tuple.Item2; return true; } public void Complete(string key) { _knownKeys.Remove(key); if (_active > 0) { _active--; } } } public sealed class EconomicsResult { public string Key = ""; public bool Available; public string Error = "Price unavailable"; public double SellPrice; public double IngredientCost; public double BaseCostPerUnit; public double ProductionCost; public double Profit; public double ProfitMargin; } internal sealed class ObservedEconomicsRequest { public string Key = ""; public bool Valid; public string Base = ""; public List<string> Ingredients = new List<string>(); public List<string> Effects = new List<string>(); } internal static class EconomicsService { private const string Url = "https://hyland-helper-backend.vercel.app/api/calculator/mix-observed"; private const int MaxResponseBytes = 65536; private static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromSeconds(15.0) }; private static readonly object Gate = new object(); private static readonly EconomicsRequestQueue<ObservedEconomicsRequest> Queue = new EconomicsRequestQueue<ObservedEconomicsRequest>(); private static readonly Dictionary<string, EconomicsResult> Cache = new Dictionary<string, EconomicsResult>(); private static readonly Queue<EconomicsResult> Completed = new Queue<EconomicsResult>(); public static string CacheKey(LocalMix mix) { List<string> list; if (mix.Steps.Count != 0) { List<LocalMixStep> steps = mix.Steps; list = steps[steps.Count - 1].OutputEffects; } else { list = new List<string>(); } List<string> observedFinalEffects = list; return JsonSerializer.Serialize(new { baseId = mix.Base, ingredients = mix.Ingredients, observedFinalEffects = observedFinalEffects }); } public static bool TryGetCached(string key, out EconomicsResult result) { lock (Gate) { return Cache.TryGetValue(key, out result); } } public static void Enqueue(LocalMix mix) { ObservedEconomicsRequest observedEconomicsRequest = BuildRequest(mix); lock (Gate) { if (!Cache.ContainsKey(observedEconomicsRequest.Key)) { if (!observedEconomicsRequest.Valid) { EconomicsResult economicsResult = Unavailable(observedEconomicsRequest.Key); Cache[observedEconomicsRequest.Key] = economicsResult; Completed.Enqueue(economicsResult); } else if (Queue.Enqueue(observedEconomicsRequest.Key, observedEconomicsRequest)) { PumpLocked(); } } } } public static bool TryTakeResult(out EconomicsResult result) { lock (Gate) { if (Completed.Count == 0) { result = null; return false; } result = Completed.Dequeue(); return true; } } private static ObservedEconomicsRequest BuildRequest(LocalMix mix) { int num; if (mix.Steps.Count > 0) { List<LocalMixStep> ste