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? Invalidated; } internal sealed class LocalRefreshInvalidationHub : ILocalRefreshInvalidationSource { public LocalRefreshSurface Pending { get; private set; } public event Action? 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 Codes { get; set; } = new List(); [JsonPropertyName("businessCodes")] public List BusinessCodes { get; set; } = new List(); } 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(); 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 codes)) { reason = "properties.codes"; return false; } if (!TryCodes(element, "businessCodes", 32, out HashSet 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(codes), BusinessCodes = new List(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 codes) { codes = new HashSet(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 hashSet = new HashSet(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(); 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 hashSet = new HashSet(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 CanonicalAliases = new Dictionary(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 FriendlyLabels = new Dictionary(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 BaseProducts = new HashSet(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 IngredientIds { get; set; } = new List(); [JsonPropertyName("effects")] public List? 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 Recipes { get; set; } = new List(); } } 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(); Button obj = val.AddComponent