Decompiled source of SULFUR Content Lib v0.1.0
SULFUR Content Lib.dll
Decompiled an hour ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using I2.Loc; using PerfectRandom.Sulfur.Core; using PerfectRandom.Sulfur.Core.DataStorage; using PerfectRandom.Sulfur.Core.DevTools; using PerfectRandom.Sulfur.Core.Items; using PerfectRandom.Sulfur.Core.Stats; using PerfectRandom.Sulfur.Core.UI; using PerfectRandom.Sulfur.Core.UI.Inventory; using PerfectRandom.Sulfur.Core.Units; using Ryuka.Sulfur.ContentLib.Bootstrap; using Ryuka.Sulfur.ContentLib.Integration; using Ryuka.Sulfur.ContentLib.Patches; using Ryuka.Sulfur.ContentLib.Registry; using Ryuka.Sulfur.ContentLib.Saves; using Ryuka.Sulfur.ContentLib.Texts; using SULFURTogether.Api; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Ryuka")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+c560c9a5908d1987fd81732cacdd7bbc8ec2e2ff")] [assembly: AssemblyProduct("SULFUR Content Lib")] [assembly: AssemblyTitle("SULFUR Content Lib")] [assembly: InternalsVisibleTo("ContentLib.Tests")] [assembly: AssemblyVersion("0.1.0.0")] namespace Ryuka.Sulfur.ContentLib { public static class ContentManifest { public static bool IsSealed => LibraryState.Declarations.Phase == RegistryPhase.Sealed; public static bool IsFaulted => LibraryState.IsFaulted; public static event Action Sealed { add { LibraryState.SealedSignal.Add(value); } remove { LibraryState.SealedSignal.Remove(value); } } public static bool TryGetItem(string key, out ItemDefinition definition) { definition = null; ContentTables tables = LibraryState.Tables; if (key != null && tables != null) { return tables.ItemsByKey.TryGetValue(key, out definition); } return false; } public static bool TryGetEnchantment(string key, out EnchantmentDefinition definition) { definition = null; ContentTables tables = LibraryState.Tables; if (key != null && tables != null) { return tables.EnchantmentsByKey.TryGetValue(key, out definition); } return false; } public static bool TryGetRecipe(string key, out RecipeData recipe) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) recipe = default(RecipeData); if (key == null || !LibraryState.Recipes.TryGetValue(key, out recipe)) { return false; } if (recipe.itemsNeeded != null) { recipe.itemsNeeded = new List<IngredientDefinition>(recipe.itemsNeeded); } return true; } public static bool TryGetKey(ItemId id, out string key) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) key = null; return LibraryState.Tables?.LiveItemKeys.TryGetValue(id.value, out key) ?? false; } public static bool TryGetKey(EnchantmentDefinition enchantment, out string key) { key = null; return LibraryState.Tables?.TryGetEnchantmentKey(enchantment, out key) ?? false; } } public static class ContentRegistry { public static bool DeclareItem(string key, Func<ItemDefinition> factory) { if (LibraryState.Declarations.TryDeclareItem(key, factory, out var error)) { return true; } LibraryLog.Source.LogError((object)("Item '" + key + "' was not declared: " + error + ".")); return false; } public static bool DeclareEnchantment(string key, string appliedByItemKey, Func<EnchantmentDefinition> factory) { if (LibraryState.Declarations.TryDeclareEnchantment(key, appliedByItemKey, factory, out var error)) { return true; } LibraryLog.Source.LogError((object)("Enchantment '" + key + "' was not declared: " + error + ".")); return false; } public static bool DeclareRecipe(string key, Func<RecipeData> factory) { return DeclareRecipe(key, factory, knownFromStart: false); } public static bool DeclareRecipe(string key, Func<RecipeData> factory, bool knownFromStart) { if (LibraryState.Declarations.TryDeclareRecipe(key, factory, knownFromStart, out var error)) { return true; } LibraryLog.Source.LogError((object)("Recipe '" + key + "' was not declared: " + error + ".")); return false; } } public static class ContentTexts { public static int LoadFrom(BaseUnityPlugin plugin) { if ((Object)(object)plugin == (Object)null || plugin.Info == null || plugin.Info.Metadata == null) { LibraryLog.Source.LogError((object)"ContentTexts.LoadFrom: the plugin is null or not loaded by BepInEx."); return 0; } return LoadFrom(plugin, plugin.Info.Metadata.GUID); } public static int LoadFrom(BaseUnityPlugin plugin, string owner) { if ((Object)(object)plugin == (Object)null || plugin.Info == null || string.IsNullOrEmpty(owner)) { LibraryLog.Source.LogError((object)"ContentTexts.LoadFrom: the plugin or the owner is missing."); return 0; } string text = (string.IsNullOrEmpty(plugin.Info.Location) ? null : Path.GetDirectoryName(plugin.Info.Location)); List<string> usedDirectories; Dictionary<string, Dictionary<string, string>> dictionary = LangFiles.Read(text, TextStore.IsContentFileKey, delegate(string problem) { LibraryLog.Source.LogWarning((object)("Texts of '" + owner + "': " + problem + ".")); }, out usedDirectories); int num = 0; foreach (KeyValuePair<string, Dictionary<string, string>> item in dictionary) { LibraryState.Texts.MergeFile(owner, item.Key, item.Value); num += item.Value.Count; } if (num == 0) { LibraryLog.Source.LogWarning((object)("Texts of '" + owner + "': no 'item.*' or 'enchantment.*' entry in any <language code>.json in or next to '" + text + "'.")); } else { LibraryLog.Source.LogInfo((object)string.Format("Texts of '{0}': {1} text(s) in {2} language(s) from '{3}'.", owner, num, dictionary.Count, string.Join("', '", usedDirectories))); } ContentTextPublisher.TextsChanged(fromFile: true); return num; } public static bool SetItemName(string key, string languageCode, string text) { return Set(key, languageCode, TextStore.ItemTerm(key ?? string.Empty, TextKind.Name), text, "SetItemName"); } public static bool SetItemDescription(string key, string languageCode, string text) { return Set(key, languageCode, TextStore.ItemTerm(key ?? string.Empty, TextKind.Description), text, "SetItemDescription"); } public static bool SetItemFlavor(string key, string languageCode, string text) { return Set(key, languageCode, TextStore.ItemTerm(key ?? string.Empty, TextKind.Flavor), text, "SetItemFlavor"); } public static bool SetEnchantmentName(string key, string languageCode, string text) { return Set(key, languageCode, TextStore.EnchantmentTerm(key ?? string.Empty), text, "SetEnchantmentName"); } private static bool Set(string key, string languageCode, string term, string text, string method) { if (!ContentKey.TryValidate(key, out var error)) { LibraryLog.Source.LogError((object)("ContentTexts." + method + ": '" + key + "' is not a content key: " + error + ".")); return false; } if (string.IsNullOrEmpty(languageCode)) { LibraryLog.Source.LogError((object)("ContentTexts." + method + ": no language code for '" + key + "'.")); return false; } LibraryState.Texts.SetDirect(term, languageCode, text); ContentTextPublisher.TextsChanged(fromFile: false); return true; } } public static class ItemTemplates { private static readonly Dictionary<string, Sprite> Sprites = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase); private static MethodInfo _loadImage; public static T Clone<T>(T template) where T : ItemDefinition { if ((Object)(object)template == (Object)null) { throw new ArgumentNullException("template"); } T val = Object.Instantiate<T>(template); ((Object)(object)val).hideFlags = (HideFlags)61; return val; } public static Sprite LoadSprite(BaseUnityPlugin plugin, string fileName) { if ((Object)(object)plugin == (Object)null) { throw new ArgumentNullException("plugin"); } string text; List<string> list; try { string location = plugin.Info.Location; text = (string.IsNullOrEmpty(location) ? null : Path.GetDirectoryName(location)); list = SpriteCandidates(text, fileName, out var error); if (list == null) { LibraryLog.Source.LogError((object)("ItemTemplates.LoadSprite: '" + fileName + "' cannot be used: " + error + ".")); return null; } } catch (Exception ex) { LibraryLog.Source.LogError((object)("ItemTemplates.LoadSprite: '" + fileName + "' cannot be used: " + ex.Message)); return null; } foreach (string item in list) { if (Sprites.TryGetValue(item, out var value) && (Object)(object)value != (Object)null) { return value; } if (File.Exists(item)) { Sprite val = ReadSprite(item); if ((Object)(object)val != (Object)null) { Sprites[item] = val; } return val; } } LibraryLog.Source.LogError((object)("ItemTemplates.LoadSprite: no file '" + fileName + "' in '" + text + "'.")); return null; } internal static List<string> SpriteCandidates(string pluginDirectory, string fileName, out string error) { if (string.IsNullOrEmpty(pluginDirectory)) { error = "the plugin has no folder"; return null; } if (string.IsNullOrWhiteSpace(fileName)) { error = "the file name is empty"; return null; } if (Path.IsPathRooted(fileName) || fileName.IndexOf(':') >= 0) { error = "give a path relative to your plugin's folder"; return null; } string[] array = fileName.Split('/', '\\'); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { if (array2[i] == "..") { error = "the path must stay inside your plugin's folder"; return null; } } error = null; List<string> list = new List<string> { Path.Combine(pluginDirectory, Path.Combine(array)) }; if (array.Length > 1) { list.Add(Path.Combine(pluginDirectory, array[^1])); } return list; } private static bool LoadImage(Texture2D texture, byte[] bytes) { if (_loadImage == null) { _loadImage = Type.GetType("UnityEngine.ImageConversion, UnityEngine.ImageConversionModule")?.GetMethod("LoadImage", new Type[3] { typeof(Texture2D), typeof(byte[]), typeof(bool) }); if (_loadImage == null) { throw new MissingMethodException("UnityEngine.ImageConversion.LoadImage was not found"); } } return (bool)_loadImage.Invoke(null, new object[3] { texture, bytes, true }); } private static Sprite ReadSprite(string path) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) Texture2D val = null; try { byte[] bytes = File.ReadAllBytes(path); val = new Texture2D(2, 2, (TextureFormat)4, false); if (!LoadImage(val, bytes)) { Object.Destroy((Object)(object)val); LibraryLog.Source.LogError((object)("ItemTemplates.LoadSprite: '" + path + "' is not a PNG or JPG image.")); return null; } ((Object)val).name = Path.GetFileNameWithoutExtension(path); ((Texture)val).filterMode = (FilterMode)1; ((Texture)val).wrapMode = (TextureWrapMode)1; ((Object)val).hideFlags = (HideFlags)61; Sprite obj = Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f), 100f); ((Object)obj).name = ((Object)val).name; ((Object)obj).hideFlags = (HideFlags)61; return obj; } catch (Exception ex) { if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } LibraryLog.Source.LogError((object)("ItemTemplates.LoadSprite: could not read '" + path + "': " + (ex.InnerException ?? ex).Message)); return null; } } } [BepInPlugin("ryuka.sulfur.contentlib", "SULFUR Content Lib", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class ContentLibPlugin : BaseUnityPlugin { public const string PluginGuid = "ryuka.sulfur.contentlib"; public const string PluginName = "SULFUR Content Lib"; public const string PluginVersion = "0.1.0"; internal static ContentLibPlugin Instance { get; private set; } private void Awake() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown Instance = this; Harmony val = new Harmony("ryuka.sulfur.contentlib"); Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(ContentLibPlugin).Assembly); foreach (Type type in typesFromAssembly) { if (type.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0) { try { val.CreateClassProcessor(type).Patch(); } catch (Exception ex) { LibraryState.Fault("patch " + type.Name + " could not be applied: " + ex.Message); } } } DevSpawnMenuPatch.Install(val); RecipeKnownPatch.Install(val); SessionRequirementRegistration.TryRegister(); LibraryLog.Source.LogInfo((object)"SULFUR Content Lib 0.1.0 loaded."); } } internal static class KnownMods { internal const string CustomItemFrameworkHarmonyId = "com.customitemframework"; internal const string SulfurTogetherGuid = "com.ryuka.sulfur.together"; } internal static class LibraryLog { internal static readonly ManualLogSource Source = Logger.CreateLogSource("SULFUR Content Lib"); } internal static class LibraryState { internal static readonly DeclarationSet Declarations = new DeclarationSet(); internal static readonly TextStore Texts = new TextStore(); internal static readonly OnceSignal SealedSignal = new OnceSignal(delegate(Exception ex) { LibraryLog.Source.LogError((object)$"A ContentManifest.Sealed handler threw: {ex}"); }); internal static ContentTables Tables { get; private set; } internal static bool IsFaulted { get; private set; } internal static string FaultReason { get; private set; } internal static IReadOnlyDictionary<string, RecipeData> Recipes { get; private set; } = new Dictionary<string, RecipeData>(); internal static HashSet<string> RecipesKnownFromStart { get; private set; } = new HashSet<string>(StringComparer.Ordinal); internal static void PublishRecipes(Dictionary<string, RecipeData> recipes, HashSet<string> knownFromStart) { Recipes = recipes; RecipesKnownFromStart = knownFromStart; } internal static void PublishTables(ContentTables tables) { if (!IsFaulted) { Tables = tables; } } internal static void Fault(string reason) { if (IsFaulted) { LibraryLog.Source.LogError((object)("Further integrity fault: " + reason)); return; } IsFaulted = true; FaultReason = reason; LibraryLog.Source.LogError((object)("INTEGRITY FAULT: " + reason + ". Saves that contain library items will not be written this session, so nothing on disk is changed.")); } } } namespace Ryuka.Sulfur.ContentLib.Texts { internal static class ContentTextPublisher { internal readonly struct OriginalDescription { internal string Description { get; } internal bool HasCustomDescription { get; } internal OriginalDescription(string description, bool hasCustomDescription) { Description = description; HasCustomDescription = hasCustomDescription; } } private sealed class ByReference : IEqualityComparer<ItemDefinition> { internal static readonly ByReference Instance = new ByReference(); public bool Equals(ItemDefinition x, ItemDefinition y) { return x == y; } public int GetHashCode(ItemDefinition obj) { return RuntimeHelpers.GetHashCode(obj); } } private static readonly Dictionary<ItemDefinition, OriginalDescription> Originals = new Dictionary<ItemDefinition, OriginalDescription>(ByReference.Instance); private static bool _started; internal static void Start(MonoBehaviour host) { if (LibraryState.Tables != null) { _started = true; ReportUnmatched(); GameLocalization.Publish(BuildTerms, host); } } internal static void TextsChanged(bool fromFile) { if (_started) { if (fromFile) { ReportUnmatched(); } GameLocalization.Refresh(); } } private static Dictionary<string, Dictionary<string, string>> BuildTerms(string currentLanguageCode) { ContentTables tables = LibraryState.Tables; Dictionary<string, Dictionary<string, string>> dictionary = LibraryState.Texts.Collect(tables.ItemsByKey.Keys, tables.EnchantmentsByKey.Keys); foreach (KeyValuePair<string, ItemDefinition> item in tables.ItemsByKey) { if (item.Value != null) { AddDefault(dictionary, TextStore.ItemTerm(item.Key, TextKind.Name), item.Value.displayName); dictionary.TryGetValue(TextStore.ItemTerm(item.Key, TextKind.Description), out var value); ApplyDescription(item.Value, (value == null) ? null : TextStore.Resolve(value, currentLanguageCode)); } } foreach (KeyValuePair<string, EnchantmentDefinition> item2 in tables.EnchantmentsByKey) { if (item2.Value != null) { AddDefault(dictionary, TextStore.EnchantmentTerm(item2.Key), item2.Value.enchantmentName); } } return dictionary; } internal static void AddDefault(Dictionary<string, Dictionary<string, string>> terms, string term, string text) { if (!string.IsNullOrEmpty(text)) { if (!terms.TryGetValue(term, out var value)) { value = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); terms.Add(term, value); } if (!value.ContainsKey("en")) { value.Add("en", text); } } } internal static OriginalDescription DescriptionFields(OriginalDescription original, bool isWeapon, bool isMelee, string text) { if (string.IsNullOrEmpty(text) || (isWeapon && isMelee)) { return original; } if (!isWeapon) { return new OriginalDescription(original.Description, hasCustomDescription: true); } return new OriginalDescription(string.IsNullOrEmpty(original.Description) ? text : original.Description, original.HasCustomDescription); } private static void ApplyDescription(ItemDefinition item, string text) { if (!Originals.TryGetValue(item, out var value)) { if (string.IsNullOrEmpty(text)) { return; } value = new OriginalDescription(item.description, item.hasCustomDescription); Originals.Add(item, value); } WeaponSO val = (WeaponSO)(object)((item is WeaponSO) ? item : null); OriginalDescription originalDescription = DescriptionFields(value, val != null, val != null && val.IsMelee, text); item.description = originalDescription.Description; item.hasCustomDescription = originalDescription.HasCustomDescription; } private static void ReportUnmatched() { DeclarationSet declarations = LibraryState.Declarations; List<string> list = new List<string>(); foreach (ItemDeclaration item in declarations.Items) { list.Add(item.Key); } List<string> list2 = new List<string>(); foreach (EnchantmentDeclaration enchantment in declarations.Enchantments) { list2.Add(enchantment.Key); } List<string> list3 = LibraryState.Texts.UnmatchedFileKeys(list, list2); if (list3.Count > 0) { string arg = string.Join("; ", list3.GetRange(0, Math.Min(20, list3.Count))); LibraryLog.Source.LogWarning((object)($"{list3.Count} text(s) in language files name no declared item or enchantment and are not used: {arg}" + ((list3.Count > 20) ? "; ..." : string.Empty) + ". The name is the part of the content key after the colon.")); } } } internal static class GameLocalization { private const MissingTranslationAction LeaveNull = (MissingTranslationAction)4; private static LanguageSourceData _source; private static OnLanguageChange _handler; private static Func<string, Dictionary<string, Dictionary<string, string>>> _terms; private static MonoBehaviour _host; private static bool _refreshQueued; internal static void Publish(Func<string, Dictionary<string, Dictionary<string, string>>> terms, MonoBehaviour host) { _terms = terms; _host = host; Rebuild(); HookLanguageChange(); } internal static void Refresh() { if (_terms != null && !_refreshQueued) { if ((Object)(object)_host == (Object)null || !((Behaviour)_host).isActiveAndEnabled) { Rebuild(); return; } _refreshQueued = true; _host.StartCoroutine(RebuildNextFrame()); } } private static IEnumerator RebuildNextFrame() { yield return null; _refreshQueued = false; Rebuild(); } private static void Rebuild() { try { int count; LanguageSourceData val = Build(out count); if (_source != null) { LocalizationManager.RemoveSource(_source); _source = null; } if (val != null) { LocalizationManager.AddSource(val); _source = val; LocalizationManager.LocalizeAll(true); LibraryLog.Source.LogInfo((object)($"Texts published: {count} term(s) in {val.mLanguages.Count} language(s); " + "game language '" + LocalizationManager.CurrentLanguage + "' (" + LocalizationManager.CurrentLanguageCode + ").")); } } catch (Exception arg) { LibraryLog.Source.LogError((object)$"Could not publish item and enchantment texts; what was published before stays: {arg}"); } } private static LanguageSourceData Build(out int count) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown count = 0; Dictionary<string, Dictionary<string, string>> dictionary = _terms(LocalizationManager.CurrentLanguageCode); if (dictionary.Count == 0) { return null; } List<LanguageData> list = GameLanguages(); if (list.Count == 0) { LibraryLog.Source.LogWarning((object)"The game lists no language yet; item and enchantment texts are not published until the language changes."); return null; } LanguageSourceData val = new LanguageSourceData { mLanguages = list, mTerms = new List<TermData>(), OnMissingTranslation = (MissingTranslationAction)4 }; foreach (KeyValuePair<string, Dictionary<string, string>> item in dictionary) { TermData val2 = val.AddTerm(item.Key, (eTermType)0, true); val2.Languages = new string[list.Count]; val2.Flags = new byte[list.Count]; for (int i = 0; i < list.Count; i++) { val2.Languages[i] = TextStore.Resolve(item.Value, list[i].Code) ?? string.Empty; } count++; } val.UpdateDictionary(true); return val; } private static List<LanguageData> GameLanguages() { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Expected O, but got Unknown List<LanguageData> list = new List<LanguageData>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (LanguageSourceData source in LocalizationManager.Sources) { if (source == null || source == _source || source.mLanguages == null) { continue; } foreach (LanguageData mLanguage in source.mLanguages) { if (mLanguage != null && !string.IsNullOrEmpty(mLanguage.Name) && hashSet.Add(mLanguage.Name)) { list.Add(new LanguageData { Name = mLanguage.Name, Code = mLanguage.Code, Flags = mLanguage.Flags }); } } } return list; } private static void HookLanguageChange() { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown if (_handler == null) { AsyncAssetLoading instance = StaticInstance<AsyncAssetLoading>.Instance; if (!((Object)(object)instance == (Object)null)) { _handler = new OnLanguageChange(Rebuild); instance.onLanguageChange = (OnLanguageChange)Delegate.Combine((Delegate?)(object)_handler, (Delegate?)(object)instance.onLanguageChange); } } } } internal static class LangFiles { [DataContract] private sealed class LangEntry { [DataMember(Name = "key")] public string Key { get; set; } [DataMember(Name = "value")] public string Value { get; set; } } [DataContract] private sealed class LangFile { [DataMember(Name = "entries")] public LangEntry[] Entries { get; set; } } private static readonly HashSet<string> KnownCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "en", "sv", "fr", "it", "de", "es", "pt", "ru", "pl", "ja", "ko", "zh-CN", "tr", "zh-TW", "ar", "zh" }; internal static IEnumerable<string> SearchDirectories(string pluginDirectory) { if (!string.IsNullOrEmpty(pluginDirectory)) { yield return Path.Combine(pluginDirectory, "lang"); yield return pluginDirectory; string directoryName = Path.GetDirectoryName(pluginDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); if (!string.IsNullOrEmpty(directoryName)) { yield return Path.Combine(directoryName, "lang"); } } } internal static Dictionary<string, Dictionary<string, string>> Read(string pluginDirectory, Func<string, bool> wanted, Action<string> problem, out List<string> usedDirectories) { Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); usedDirectories = new List<string>(); foreach (string item in SearchDirectories(pluginDirectory)) { bool flag = false; foreach (KeyValuePair<string, Dictionary<string, string>> item2 in ReadDirectory(item, wanted, problem)) { if (!dictionary.TryGetValue(item2.Key, out var value)) { value = new Dictionary<string, string>(StringComparer.Ordinal); dictionary.Add(item2.Key, value); } foreach (KeyValuePair<string, string> item3 in item2.Value) { if (!value.ContainsKey(item3.Key)) { value.Add(item3.Key, item3.Value); flag = true; } } } if (flag) { usedDirectories.Add(item); } } return dictionary; } internal static bool LooksLikeLanguageCode(string name) { if (string.IsNullOrEmpty(name)) { return false; } int num = name.IndexOf('-'); string text = ((num < 0) ? name : name.Substring(0, num)); if (text.Length < 2 || text.Length > 3 || !AllLetters(text, digitsToo: false)) { return false; } if (num < 0) { return true; } string text2 = name.Substring(num + 1); if (text2.Length >= 2 && text2.Length <= 4) { return AllLetters(text2, digitsToo: true); } return false; } private static bool AllLetters(string text, bool digitsToo) { foreach (char c in text) { if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (!digitsToo || c < '0' || c > '9')) { return false; } } return true; } private static Dictionary<string, Dictionary<string, string>> ReadDirectory(string directory, Func<string, bool> wanted, Action<string> problem) { Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); if (!Directory.Exists(directory)) { return dictionary; } string[] files; try { files = Directory.GetFiles(directory, "*.json", SearchOption.TopDirectoryOnly); } catch (Exception ex) { problem("could not list '" + directory + "': " + ex.Message); return dictionary; } Array.Sort(files, (IComparer<string>?)StringComparer.OrdinalIgnoreCase); string[] array = files; foreach (string text in array) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text); if (LooksLikeLanguageCode(fileNameWithoutExtension)) { Dictionary<string, string> dictionary2 = ReadFile(text, wanted, KnownCodes.Contains(fileNameWithoutExtension) ? problem : ((Action<string>)delegate { })); if (dictionary2.Count > 0) { dictionary[fileNameWithoutExtension] = dictionary2; } } } return dictionary; } private static Dictionary<string, string> ReadFile(string file, Func<string, bool> wanted, Action<string> problem) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); try { byte[] array = File.ReadAllBytes(file); int num = ((array.Length >= 3 && array[0] == 239 && array[1] == 187 && array[2] == 191) ? 3 : 0); using MemoryStream memoryStream = new MemoryStream(array, num, array.Length - num, writable: false); if (((XmlObjectSerializer)new DataContractJsonSerializer(typeof(LangFile))).ReadObject((Stream)memoryStream) is LangFile { Entries: not null, Entries: var entries }) { foreach (LangEntry langEntry in entries) { if (langEntry != null && langEntry.Key != null && !string.IsNullOrEmpty(langEntry.Value) && wanted(langEntry.Key)) { dictionary[langEntry.Key] = langEntry.Value; } } } } catch (Exception ex) { problem("could not read '" + file + "': " + ex.Message); } return dictionary; } } internal enum TextKind { Name, Description, Flavor } internal sealed class TextStore { internal const string ItemTermPrefix = "Items/"; internal const string EnchantmentTermPrefix = "EnchantmentDefinitions/"; internal const string DescriptionSuffix = "_description"; internal const string FlavorSuffix = "_flavor"; internal const string EnglishCode = "en"; private static readonly TextKind[] ItemKinds = new TextKind[3] { TextKind.Name, TextKind.Description, TextKind.Flavor }; private readonly Dictionary<string, Dictionary<string, Dictionary<string, string>>> _files = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>(StringComparer.Ordinal); private readonly Dictionary<string, Dictionary<string, string>> _direct = new Dictionary<string, Dictionary<string, string>>(StringComparer.Ordinal); internal static string ItemTerm(string key, TextKind kind) { return kind switch { TextKind.Description => "Items/" + key + "_description", TextKind.Flavor => "Items/" + key + "_flavor", _ => "Items/" + key, }; } internal static string EnchantmentTerm(string key) { return "EnchantmentDefinitions/" + key; } internal static string ItemFileKey(string name, TextKind kind) { return kind switch { TextKind.Description => "item." + name + ".description", TextKind.Flavor => "item." + name + ".flavor", _ => "item." + name + ".name", }; } internal static string EnchantmentFileKey(string name) { return "enchantment." + name + ".name"; } internal static bool IsContentFileKey(string fileKey) { if (fileKey != null) { if (!fileKey.StartsWith("item.", StringComparison.Ordinal)) { return fileKey.StartsWith("enchantment.", StringComparison.Ordinal); } return true; } return false; } internal static bool EndsWithReservedSuffix(string key) { if (!key.EndsWith("_description", StringComparison.OrdinalIgnoreCase)) { return key.EndsWith("_flavor", StringComparison.OrdinalIgnoreCase); } return true; } internal static IReadOnlyList<string> FallbackChain(string code) { if (string.IsNullOrEmpty(code)) { return new string[1] { "en" }; } if (string.Equals(code, "zh", StringComparison.OrdinalIgnoreCase) || string.Equals(code, "zh-TW", StringComparison.OrdinalIgnoreCase)) { return new string[4] { "zh-TW", "zh-CN", "zh", "en" }; } List<string> list = new List<string> { code }; int num = code.IndexOf('-'); if (num > 0) { list.Add(code.Substring(0, num)); } if (!list.Contains("en")) { list.Add("en"); } return list; } internal void MergeFile(string owner, string code, IReadOnlyDictionary<string, string> entries) { if (!_files.TryGetValue(owner, out var value)) { value = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); _files.Add(owner, value); } if (!value.TryGetValue(code, out var value2)) { value2 = new Dictionary<string, string>(StringComparer.Ordinal); value.Add(code, value2); } foreach (KeyValuePair<string, string> entry in entries) { value2[entry.Key] = entry.Value; } } internal void SetDirect(string term, string code, string text) { if (!_direct.TryGetValue(term, out var value)) { if (string.IsNullOrEmpty(text)) { return; } value = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); _direct.Add(term, value); } if (string.IsNullOrEmpty(text)) { value.Remove(code); } else { value[code] = text; } } internal Dictionary<string, Dictionary<string, string>> Collect(IEnumerable<string> itemKeys, IEnumerable<string> enchantmentKeys) { Dictionary<string, Dictionary<string, string>> dictionary = new Dictionary<string, Dictionary<string, string>>(StringComparer.Ordinal); foreach (string itemKey in itemKeys) { TextKind[] itemKinds = ItemKinds; foreach (TextKind kind in itemKinds) { AddTerm(dictionary, ItemTerm(itemKey, kind), itemKey, (string name) => ItemFileKey(name, kind)); } } foreach (string enchantmentKey in enchantmentKeys) { AddTerm(dictionary, EnchantmentTerm(enchantmentKey), enchantmentKey, EnchantmentFileKey); } return dictionary; } internal List<string> UnmatchedFileKeys(IEnumerable<string> itemKeys, IEnumerable<string> enchantmentKeys) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (string itemKey in itemKeys) { if (TrySplit(itemKey, out var owner, out var name)) { TextKind[] itemKinds = ItemKinds; foreach (TextKind kind in itemKinds) { hashSet.Add(owner + ": " + ItemFileKey(name, kind)); } } } foreach (string enchantmentKey in enchantmentKeys) { if (TrySplit(enchantmentKey, out var owner2, out var name2)) { hashSet.Add(owner2 + ": " + EnchantmentFileKey(name2)); } } SortedSet<string> sortedSet = new SortedSet<string>(StringComparer.Ordinal); foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, string>>> file in _files) { foreach (Dictionary<string, string> value in file.Value.Values) { foreach (string key in value.Keys) { string item = file.Key + ": " + key; if (!hashSet.Contains(item)) { sortedSet.Add(item); } } } } return new List<string>(sortedSet); } internal static string Resolve(IReadOnlyDictionary<string, string> byCode, string code) { foreach (string item in FallbackChain(code)) { if (TryGetIgnoreCase(byCode, item, out var text) && !string.IsNullOrEmpty(text)) { return text; } } return null; } private static bool TrySplit(string key, out string owner, out string name) { int num = key.IndexOf(':'); if (num <= 0 || num == key.Length - 1) { owner = null; name = null; return false; } owner = key.Substring(0, num); name = key.Substring(num + 1); return true; } private static bool TryGetIgnoreCase(IReadOnlyDictionary<string, string> byCode, string code, out string text) { if (byCode.TryGetValue(code, out text)) { return true; } foreach (KeyValuePair<string, string> item in byCode) { if (string.Equals(item.Key, code, StringComparison.OrdinalIgnoreCase)) { text = item.Value; return true; } } text = null; return false; } private void AddTerm(Dictionary<string, Dictionary<string, string>> terms, string term, string key, Func<string, string> fileKey) { if (!terms.TryGetValue(term, out var value)) { value = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } if (TrySplit(key, out var owner, out var name) && _files.TryGetValue(owner, out var value2)) { string key2 = fileKey(name); foreach (KeyValuePair<string, Dictionary<string, string>> item in value2) { if (item.Value.TryGetValue(key2, out var value3) && !string.IsNullOrEmpty(value3)) { value[item.Key] = value3; } } } if (_direct.TryGetValue(term, out var value4)) { foreach (KeyValuePair<string, string> item2 in value4) { value[item2.Key] = item2.Value; } } if (value.Count > 0) { terms[term] = value; } } } } namespace Ryuka.Sulfur.ContentLib.Saves { internal static class InsuredHold { internal sealed class Swap { private readonly SulfurSaveState _state; private readonly List<InventoryData> _insured; private readonly SerializableDictionary<string, SerializableList<InventoryData>> _stash; internal bool HadHold { get; } internal SerializableList<InventoryData> OriginalHold { get; } internal Swap(SulfurSaveState state, List<InventoryData> insured, SerializableDictionary<string, SerializableList<InventoryData>> stash) { _state = state; _insured = insured; _stash = stash; HadHold = ((Dictionary<string, SerializableList<InventoryData>>)(object)stash)?.TryGetValue("contentlib|insured", out SerializableList<InventoryData> _) ?? false; OriginalHold = (HadHold ? ((Dictionary<string, SerializableList<InventoryData>>)(object)stash)["contentlib|insured"] : null); } internal void End() { _state.INSURED_ITEMS = _insured; if (_stash == null) { _state.STASH = null; return; } _state.STASH = _stash; if (HadHold) { ((Dictionary<string, SerializableList<InventoryData>>)(object)_stash)["contentlib|insured"] = OriginalHold; } else { ((Dictionary<string, SerializableList<InventoryData>>)(object)_stash).Remove("contentlib|insured"); } } } internal const string StashKey = "contentlib|insured"; internal static Swap Begin(SulfurSaveState state) { List<InventoryData> iNSURED_ITEMS = state.INSURED_ITEMS; if (iNSURED_ITEMS == null) { return null; } List<InventoryData> list = null; foreach (InventoryData item in iNSURED_ITEMS) { if (IsLibraryHost(item)) { (list ?? (list = new List<InventoryData>())).Add(item); } } if (list == null) { return null; } List<InventoryData> list2 = new List<InventoryData>(iNSURED_ITEMS.Count - list.Count); foreach (InventoryData item2 in iNSURED_ITEMS) { if (!IsLibraryHost(item2)) { list2.Add(item2); } } Swap swap = new Swap(state, iNSURED_ITEMS, state.STASH); List<InventoryData> list3 = new List<InventoryData>(); if (swap.HadHold && swap.OriginalHold?.list != null) { list3.AddRange(swap.OriginalHold.list); } list3.AddRange(list); if (state.STASH == null) { state.STASH = new SerializableDictionary<string, SerializableList<InventoryData>>(); } ((Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH)["contentlib|insured"] = new SerializableList<InventoryData>(list3); state.INSURED_ITEMS = list2; return swap; } internal static int PutBack(SulfurSaveState state, List<InventoryData> strays) { if (state.STASH == null || !((Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH).TryGetValue("contentlib|insured", out SerializableList<InventoryData> value)) { return 0; } int num = 0; if (value?.list != null && value.list.Count > 0) { List<InventoryData> list = ((state.INSURED_ITEMS == null) ? new List<InventoryData>(value.list.Count) : new List<InventoryData>(state.INSURED_ITEMS)); foreach (InventoryData item in value.list) { if (item != null) { if (item.id.value == ItemId.None.value) { strays.Add(item); continue; } list.Add(item); num++; } } state.INSURED_ITEMS = list; } ((Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH).Remove("contentlib|insured"); return num; } private static bool IsLibraryHost(InventoryData item) { if (item != null) { return Numbering.IsItemNumber(item.id.value); } return false; } } internal enum SaveStateStatus { PassedThrough, Normalized, Blocked } internal static class SaveStateTracker { private sealed class Entry { internal SaveStateStatus Status; internal string Reason; internal int BlockedWrites; internal bool Alerted; } private static readonly ConditionalWeakTable<SulfurSaveState, Entry> States = new ConditionalWeakTable<SulfurSaveState, Entry>(); internal static void Set(SulfurSaveState state, SaveStateStatus status, string reason = null) { Entry orCreateValue = States.GetOrCreateValue(state); orCreateValue.Status = status; orCreateValue.Reason = reason; } internal static bool TryGet(SulfurSaveState state, out SaveStateStatus status, out string reason) { if (States.TryGetValue(state, out var value)) { status = value.Status; reason = value.Reason; return true; } status = SaveStateStatus.PassedThrough; reason = null; return false; } internal static int CountBlockedWrite(SulfurSaveState state) { return ++States.GetOrCreateValue(state).BlockedWrites; } internal static bool WasAlerted(SulfurSaveState state) { if (States.TryGetValue(state, out var value)) { return value.Alerted; } return false; } internal static void MarkAlerted(SulfurSaveState state) { States.GetOrCreateValue(state).Alerted = true; } } internal sealed class NormalizingEnumerator : IEnumerator<SulfurSaveState>, IDisposable, IEnumerator { private readonly IEnumerator<SulfurSaveState> _inner; private readonly int _slot; private readonly bool _backup; private SulfurSaveState _handled; public SulfurSaveState Current { get; private set; } object IEnumerator.Current => Current; internal NormalizingEnumerator(IEnumerator<SulfurSaveState> inner, int slot, bool backup) { _inner = inner; _slot = slot; _backup = backup; } public bool MoveNext() { if (!_inner.MoveNext()) { return false; } SulfurSaveState current = _inner.Current; if (current != null && current != _handled) { _handled = current; SaveGuard.OnRead(current, _slot, _backup); } Current = current; return true; } public void Reset() { _inner.Reset(); _handled = null; Current = null; } public void Dispose() { _inner.Dispose(); } } internal enum WriteDecision { Pass, Stamp, Refuse } internal static class SaveGuard { internal static void OnRead(SulfurSaveState state, int slot, bool backup) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) try { if ((int)state.loadResult != 0) { return; } string text = (backup ? $"slot {slot} backup" : $"slot {slot}"); if (LibraryState.IsFaulted) { if (SaveNormalizer.ContainsLibraryNumbers(state)) { Block(state, text + " holds library items and the library is faulted (" + LibraryState.FaultReason + ")"); } else { SaveStateTracker.Set(state, SaveStateStatus.PassedThrough); } return; } if (LibraryState.Declarations.Phase != RegistryPhase.Sealed) { SaveStateTracker.Set(state, SaveStateStatus.PassedThrough); return; } NormalizeReport normalizeReport = SaveNormalizer.Normalize(state, LibraryState.Tables.LiveItemNumbers, LibraryState.Tables.LiveEnchantmentKeyByItemKey); SaveStateTracker.Set(state, SaveStateStatus.Normalized); if (normalizeReport.Changed) { LibraryLog.Source.LogInfo((object)$"Read {text}: {normalizeReport}."); } } catch (Exception ex) { Block(state, string.Format("slot {0}{1} could not be normalized: {2}", slot, backup ? " backup" : string.Empty, ex.Message)); LibraryLog.Source.LogDebug((object)ex.ToString()); } } internal static bool AllowWrite(SulfurSaveState state, Action<SaveResults> onCompleted, out InsuredHold.Swap swap) { swap = null; try { if (state == null) { return true; } SaveStateStatus status; string reason; bool flag = SaveStateTracker.TryGet(state, out status, out reason); string reason2; switch (DecideWrite(state, LibraryState.Declarations.Phase == RegistryPhase.Sealed, LibraryState.IsFaulted, LibraryState.FaultReason, flag ? new SaveStateStatus?(status) : ((SaveStateStatus?)null), reason, state == SulfurSaveState.active, LibraryState.Tables?.LiveItemNumbers, out reason2)) { case WriteDecision.Stamp: if (state.PLAYER_RECORDS == null) { state.PLAYER_RECORDS = new SerializableDictionary<string, int>(); } SaveMap.Stamp((IDictionary<string, int>)state.PLAYER_RECORDS, LibraryState.Tables.LiveItemNumbers); swap = InsuredHold.Begin(state); return true; case WriteDecision.Refuse: return Refuse(state, reason2, onCompleted); default: return true; } } catch (Exception ex) { return Refuse(state, "the write check threw " + ex.Message, onCompleted); } } internal static void AfterWrite(InsuredHold.Swap swap) { if (swap == null) { return; } try { swap.End(); } catch (Exception arg) { LibraryLog.Source.LogError((object)$"Could not put the insured items back after a save write: {arg}"); } } internal static WriteDecision DecideWrite(SulfurSaveState state, bool isSealed, bool isFaulted, string faultReason, SaveStateStatus? status, string blockedReason, bool isActive, IReadOnlyDictionary<string, ushort> live, out string reason) { reason = null; if (status == SaveStateStatus.Blocked) { if (!isActive) { return WriteDecision.Pass; } reason = blockedReason; return WriteDecision.Refuse; } if (!isSealed && !isFaulted) { return WriteDecision.Pass; } bool flag = SaveNormalizer.ContainsLibraryNumbers(state); if (isFaulted) { if (!flag) { return WriteDecision.Pass; } reason = "it holds library items and the library is faulted (" + faultReason + ")"; return WriteDecision.Refuse; } if (status == SaveStateStatus.PassedThrough) { if (!flag) { return WriteDecision.Pass; } reason = "it was read before the library was ready and now holds library items"; return WriteDecision.Refuse; } if (!status.HasValue && SaveMap.HasAnyRecord((IDictionary<string, int>)state.PLAYER_RECORDS) && !MapEquals(SaveMap.Read((IDictionary<string, int>)state.PLAYER_RECORDS), live)) { reason = "it was not read through the game's save loader and carries a content map from another run"; return WriteDecision.Refuse; } return WriteDecision.Stamp; } private static bool MapEquals(SavedMap saved, IReadOnlyDictionary<string, ushort> live) { if (saved.Status != SavedMapStatus.Valid || live == null || saved.KeyByNumber.Count != live.Count) { return false; } foreach (KeyValuePair<string, ushort> item in live) { if (!saved.KeyByNumber.TryGetValue(item.Value, out var value) || value != item.Key) { return false; } } return true; } private static void Block(SulfurSaveState state, string reason) { SaveStateTracker.Set(state, SaveStateStatus.Blocked, reason); LibraryLog.Source.LogError((object)("SAVE PROTECTED: " + reason + ". The game will run on the save as it is on disk, and nothing will be written to it this session, so no item is lost or changed.")); } private static bool Refuse(SulfurSaveState state, string reason, Action<SaveResults> onCompleted) { int num = SaveStateTracker.CountBlockedWrite(state); if (num == 1 || num % 20 == 0) { LibraryLog.Source.LogError((object)$"Save write for slot {state.slotIndex} blocked ({num} so far): {reason}. Progress in this session is not saved."); } if (!SaveStateTracker.WasAlerted(state) && Alert("SULFUR Content Lib: this session is NOT being saved (" + reason + "). See the BepInEx log.")) { SaveStateTracker.MarkAlerted(state); } try { onCompleted?.Invoke((SaveResults)2); } catch (Exception arg) { LibraryLog.Source.LogError((object)$"A save completion callback threw: {arg}"); } return false; } private static bool Alert(string text) { try { UIManager instance = StaticInstance<UIManager>.Instance; if ((Object)(object)instance == (Object)null) { return false; } instance.ShowSmallAlert(text, 10f, (UISounds)0); return true; } catch (Exception ex) { LibraryLog.Source.LogDebug((object)("Could not show the alert: " + ex.Message)); return false; } } } internal enum SavedMapStatus { Absent, Valid, Invalid } internal sealed class SavedMap { internal SavedMapStatus Status { get; } internal IReadOnlyDictionary<ushort, string> KeyByNumber { get; } internal string Problem { get; } private SavedMap(SavedMapStatus status, Dictionary<ushort, string> keyByNumber, string problem) { Status = status; KeyByNumber = keyByNumber; Problem = problem; } internal static SavedMap Absent() { return new SavedMap(SavedMapStatus.Absent, new Dictionary<ushort, string>(), null); } internal static SavedMap Invalid(string problem) { return new SavedMap(SavedMapStatus.Invalid, new Dictionary<ushort, string>(), problem); } internal static SavedMap Valid(Dictionary<ushort, string> keyByNumber) { return new SavedMap(SavedMapStatus.Valid, keyByNumber, null); } } internal static class SaveMap { internal const string RecordPrefix = "contentlib|"; internal const string ItemRecordPrefix = "contentlib|v1|item|"; internal const string ChecksumRecord = "contentlib|v1|checksum"; internal const string ExtensionRecordPrefix = "contentlib|v1|ext|"; internal static bool IsExtensionRecord(string recordName) { return recordName?.StartsWith("contentlib|v1|ext|", StringComparison.Ordinal) ?? false; } internal static bool IsLibraryRecord(string recordName) { return recordName?.StartsWith("contentlib|", StringComparison.Ordinal) ?? false; } internal static bool HasAnyRecord(IDictionary<string, int> records) { if (records == null) { return false; } foreach (string key in records.Keys) { if (IsLibraryRecord(key)) { return true; } } return false; } internal static SavedMap Read(IDictionary<string, int> records) { if (!HasAnyRecord(records)) { return SavedMap.Absent(); } Dictionary<ushort, string> dictionary = new Dictionary<ushort, string>(); List<KeyValuePair<string, ushort>> list = new List<KeyValuePair<string, ushort>>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); int? num = null; foreach (KeyValuePair<string, int> record in records) { if (!IsLibraryRecord(record.Key) || IsExtensionRecord(record.Key)) { continue; } if (string.Equals(record.Key, "contentlib|v1|checksum", StringComparison.Ordinal)) { num = record.Value; continue; } if (!record.Key.StartsWith("contentlib|v1|item|", StringComparison.Ordinal)) { return SavedMap.Invalid("unknown record '" + record.Key + "' (another version of the library wrote this save?)"); } string text = record.Key.Substring("contentlib|v1|item|".Length); if (!ContentKey.TryValidate(text, out var error)) { return SavedMap.Invalid("record '" + record.Key + "' has an invalid key: " + error); } if (!hashSet.Add(text)) { return SavedMap.Invalid("two records name keys that differ only by case ('" + text + "')"); } if (!Numbering.IsItemNumber(record.Value)) { return SavedMap.Invalid($"record '{record.Key}' holds {record.Value}, outside {32768}-{39999}"); } ushort num2 = (ushort)record.Value; if (dictionary.TryGetValue(num2, out var value)) { return SavedMap.Invalid($"number {num2} is mapped to both '{value}' and '{text}'"); } dictionary.Add(num2, text); list.Add(new KeyValuePair<string, ushort>(text, num2)); } if (!num.HasValue) { return SavedMap.Invalid("the checksum record is missing"); } int num3 = Checksum(list); if (num.Value != num3) { return SavedMap.Invalid($"the checksum is {num.Value}, the records give {num3}"); } return SavedMap.Valid(dictionary); } internal static void Stamp(IDictionary<string, int> records, IReadOnlyDictionary<string, ushort> liveNumbers) { List<string> list = new List<string>(); foreach (string key in records.Keys) { if (IsLibraryRecord(key) && !IsExtensionRecord(key)) { list.Add(key); } } foreach (string item in list) { records.Remove(item); } foreach (KeyValuePair<string, ushort> liveNumber in liveNumbers) { records["contentlib|v1|item|" + liveNumber.Key] = liveNumber.Value; } records["contentlib|v1|checksum"] = Checksum(liveNumbers); } internal static int Checksum(IEnumerable<KeyValuePair<string, ushort>> entries) { return (int)Fnv1a.Hash(ContentMapText.Utf8(entries)); } } internal static class QuarantineFormat { internal const string StashKey = "contentlib|quarantine"; internal const string KeyPrefix = "contentlib|v1|key|"; internal const string UnidentifiedPrefix = "contentlib|v1|unidentified|"; internal static string ForKey(string key) { return "contentlib|v1|key|" + key; } internal static ItemAttributeCollectionData NoAttributes() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return new ItemAttributeCollectionData { itemAttributes = Array.Empty<ItemAttributeData>() }; } internal static string ForUnidentified(int number) { return "contentlib|v1|unidentified|" + number.ToString(CultureInfo.InvariantCulture); } internal static bool TryGetKey(InventoryData entry, out string key) { key = null; if (entry == null || ((ItemId)(ref entry.id)).IsValid || entry.identifier == null || !entry.identifier.StartsWith("contentlib|v1|key|", StringComparison.Ordinal)) { return false; } string text = entry.identifier.Substring("contentlib|v1|key|".Length); if (!ContentKey.TryValidate(text, out var _)) { return false; } key = text; return true; } } internal enum ResolutionKind { NotLibrary, Mapped, Missing, Unidentified } internal readonly struct Resolution { internal ResolutionKind Kind { get; } internal ushort Original { get; } internal ushort Number { get; } internal string Key { get; } internal bool Lost { get { if (Kind != ResolutionKind.Missing) { return Kind == ResolutionKind.Unidentified; } return true; } } internal string QuarantineIdentifier { get { if (Kind != ResolutionKind.Unidentified) { return QuarantineFormat.ForKey(Key); } return QuarantineFormat.ForUnidentified(Original); } } internal Resolution(ResolutionKind kind, ushort original, ushort number, string key) { Kind = kind; Original = original; Number = number; Key = key; } } internal sealed class NumberResolver { private readonly SavedMap _map; private readonly IReadOnlyDictionary<string, ushort> _live; internal NumberResolver(SavedMap map, IReadOnlyDictionary<string, ushort> live) { if (map.Status == SavedMapStatus.Invalid) { throw new ArgumentException("An invalid map cannot resolve numbers.", "map"); } _map = map; _live = live; } internal Resolution Resolve(ItemId id) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) ushort value = id.value; if (!Numbering.IsItemNumber(value)) { return new Resolution(ResolutionKind.NotLibrary, value, value, null); } if (!_map.KeyByNumber.TryGetValue(value, out var value2)) { return new Resolution(ResolutionKind.Unidentified, value, 0, null); } if (!_live.TryGetValue(value2, out var value3)) { return new Resolution(ResolutionKind.Missing, value, 0, value2); } return new Resolution(ResolutionKind.Mapped, value, value3, value2); } } internal sealed class NormalizeReport { internal int Remapped; internal int Quarantined; internal int Detached; internal int Unbuilt; internal int Dropped; internal int QuestsRemoved; internal int InsuredReturned; internal SavedMapStatus MapStatus; internal bool Changed => Remapped + Quarantined + Detached + Unbuilt + Dropped + QuestsRemoved > 0; public override string ToString() { return $"saved map {MapStatus}; {Remapped} id(s) remapped, {Quarantined} item(s) and {Detached} attached item(s) " + $"quarantined, {Unbuilt} enchantment(s) that were not built taken off and their item(s) returned, " + $"{Dropped} dropped, {QuestsRemoved} quest(s) removed, {InsuredReturned} insured item(s) put back"; } } internal sealed class SaveNormalizeException : Exception { internal SaveNormalizeException(string message) : base(message) { } } internal static class SaveNormalizer { private sealed class Plan { private readonly NumberResolver _resolver; private readonly List<Action> _commits = new List<Action>(); private readonly IReadOnlyDictionary<string, string> _links; internal NormalizeReport Report { get; } = new NormalizeReport(); internal List<InventoryData> NewQuarantine { get; } = new List<InventoryData>(); internal Plan(NumberResolver resolver, IReadOnlyDictionary<string, string> links) { _resolver = resolver; _links = links; } internal void Commit() { foreach (Action commit in _commits) { commit(); } } internal void PlayerList(List<InventoryData> list, Action<List<InventoryData>> assign) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) if (list == null) { return; } List<InventoryData> kept = new List<InventoryData>(list.Count); bool flag = false; foreach (InventoryData item in list) { if (item == null) { kept.Add(null); continue; } Resolution resolution = _resolver.Resolve(item.id); if (resolution.Lost) { Quarantine(item, resolution); flag = true; } else { KeepItem(item, resolution, playerOwned: true); kept.Add(item); } } if (flag) { _commits.Add(delegate { assign(kept); }); } } internal void ShopList(List<InventoryData> list, Action<List<InventoryData>> assign) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) if (list == null) { return; } List<InventoryData> kept = new List<InventoryData>(list.Count); bool flag = false; foreach (InventoryData item in list) { if (item == null) { kept.Add(null); continue; } Resolution resolution = _resolver.Resolve(item.id); if (resolution.Lost) { Report.Dropped++; flag = true; } else { KeepItem(item, resolution, playerOwned: false); kept.Add(item); } } if (flag) { _commits.Add(delegate { assign(kept); }); } } internal void Sacrificed(List<ItemId> list, Action<List<ItemId>> assign) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) if (list == null) { return; } List<ItemId> kept = new List<ItemId>(list.Count); bool flag = false; foreach (ItemId item in list) { Resolution resolution = _resolver.Resolve(item); switch (resolution.Kind) { case ResolutionKind.NotLibrary: kept.Add(item); break; case ResolutionKind.Mapped: kept.Add(new ItemId(resolution.Number)); flag |= resolution.Number != item.value; if (resolution.Number != item.value) { Report.Remapped++; } break; default: Report.Dropped++; flag = true; break; } } if (flag) { _commits.Add(delegate { assign(kept); }); } } internal void PrinterCounters(SerializableDictionary<ItemId, int> counters, Action<SerializableDictionary<ItemId, int>> assign) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) if (counters == null) { return; } SerializableDictionary<ItemId, int> rebuilt = new SerializableDictionary<ItemId, int>(); bool flag = false; foreach (KeyValuePair<ItemId, int> item in (Dictionary<ItemId, int>)(object)counters) { Resolution resolution = _resolver.Resolve(item.Key); switch (resolution.Kind) { case ResolutionKind.NotLibrary: ((Dictionary<ItemId, int>)(object)rebuilt).Add(item.Key, item.Value); break; case ResolutionKind.Mapped: ((Dictionary<ItemId, int>)(object)rebuilt).Add(new ItemId(resolution.Number), item.Value); if (resolution.Number != item.Key.value) { Report.Remapped++; flag = true; } break; default: Report.Dropped++; flag = true; break; } } if (flag) { _commits.Add(delegate { assign(rebuilt); }); } } internal void Quests(SerializableDictionary<string, SerializableList<QuestRequirement>> quests, Action<SerializableDictionary<string, SerializableList<QuestRequirement>>> assign) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) if (quests == null) { return; } SerializableDictionary<string, SerializableList<QuestRequirement>> kept = new SerializableDictionary<string, SerializableList<QuestRequirement>>(); bool flag = false; foreach (KeyValuePair<string, SerializableList<QuestRequirement>> item in (Dictionary<string, SerializableList<QuestRequirement>>)(object)quests) { List<QuestRequirement> list = item.Value?.list; bool flag2 = false; List<KeyValuePair<QuestRequirement, ushort>> list2 = new List<KeyValuePair<QuestRequirement, ushort>>(); if (list != null) { foreach (QuestRequirement item2 in list) { if (item2 != null) { Resolution resolution = _resolver.Resolve(item2.ItemId); if (resolution.Lost) { flag2 = true; break; } if (resolution.Kind == ResolutionKind.Mapped && resolution.Number != item2.ItemId.value) { list2.Add(new KeyValuePair<QuestRequirement, ushort>(item2, resolution.Number)); } } } } if (flag2) { Report.QuestsRemoved++; flag = true; continue; } ((Dictionary<string, SerializableList<QuestRequirement>>)(object)kept).Add(item.Key, item.Value); foreach (KeyValuePair<QuestRequirement, ushort> item3 in list2) { QuestRequirement target = item3.Key; ushort number = item3.Value; _commits.Add(delegate { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) target.ItemId = new ItemId(number); }); Report.Remapped++; } } if (flag) { _commits.Add(delegate { assign(kept); }); } } private void KeepItem(InventoryData item, Resolution resolution, bool playerOwned) { //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) bool changed; ItemId[] attachments = Children(item.attachmentIds, playerOwned, detachLibraryChildren: false, enchantments: false, out changed); bool changed2; ItemId[] enchantments = Children(item.enchantmentIds, playerOwned, detachLibraryChildren: false, enchantments: true, out changed2); bool flag = resolution.Kind == ResolutionKind.Mapped && resolution.Number != item.id.value; if (flag) { Report.Remapped++; } if (flag || changed || changed2) { ItemId newId = (ItemId)(flag ? new ItemId(resolution.Number) : item.id); _commits.Add(delegate { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) item.id = newId; item.attachmentIds = attachments; item.enchantmentIds = enchantments; }); } } private void Quarantine(InventoryData item, Resolution resolution) { bool changed; ItemId[] attachments = Children(item.attachmentIds, playerOwned: true, detachLibraryChildren: true, enchantments: false, out changed); ItemId[] enchantments = Children(item.enchantmentIds, playerOwned: true, detachLibraryChildren: true, enchantments: true, out changed); string identifier = resolution.QuarantineIdentifier; _commits.Add(delegate { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) item.id = ItemId.None; item.identifier = identifier; item.attachments = null; item.enchantments = null; item.attachmentIds = attachments; item.enchantmentIds = enchantments; item.equipped = false; }); NewQuarantine.Add(item); Report.Quarantined++; } private ItemId[] Children(ItemId[] ids, bool playerOwned, bool detachLibraryChildren, bool enchantments, out bool changed) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) changed = false; if (ids == null || ids.Length == 0) { return ids; } List<ItemId> list = new List<ItemId>(ids.Length); foreach (ItemId val in ids) { Resolution resolution = _resolver.Resolve(val); if (resolution.Kind == ResolutionKind.NotLibrary) { list.Add(val); continue; } bool flag = enchantments && resolution.Kind == ResolutionKind.Mapped && !_links.ContainsKey(resolution.Key); if (resolution.Kind == ResolutionKind.Mapped && !detachLibraryChildren && !flag) { list.Add(new ItemId(resolution.Number)); if (resolution.Number != val.value) { Report.Remapped++; changed = true; } continue; } changed = true; if (playerOwned) { NewQuarantine.Add(DetachedEntry(resolution)); if (flag) { Report.Unbuilt++; } else { Report.Detached++; } } else { Report.Dropped++; } } if (!changed) { return ids; } return list.ToArray(); } private static InventoryData DetachedEntry(Resolution resolution) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown return new InventoryData(ItemId.None, 0, 0, 1, 0, (CaliberTypes)0, QuarantineFormat.NoAttributes(), Array.Empty<ItemId>(), Array.Empty<ItemId>(), 0, 1, 1, false, false) { identifier = resolution.QuarantineIdentifier }; } } internal static NormalizeReport Normalize(SulfurSaveState state, IReadOnlyDictionary<string, ushort> live, IReadOnlyDictionary<string, string> liveEnchantmentKeyByItemKey) { if (liveEnchantmentKeyByItemKey == null) { throw new ArgumentNullException("liveEnchantmentKeyByItemKey"); } SavedMap savedMap = SaveMap.Read((IDictionary<string, int>)state.PLAYER_RECORDS); if (savedMap.Status == SavedMapStatus.Invalid) { throw new SaveNormalizeException("the save's content map is invalid: " + savedMap.Problem); } Plan plan = new Plan(new NumberResolver(savedMap, live), liveEnchantmentKeyByItemKey); plan.Report.MapStatus = savedMap.Status; plan.PlayerList(state.INVENTORY, delegate(List<InventoryData> list) { state.INVENTORY = list; }); plan.PlayerList(state.INSURED_ITEMS, delegate(List<InventoryData> list) { state.INSURED_ITEMS = list; }); SerializableList<InventoryData> val = null; if (state.STASH != null) { foreach (KeyValuePair<string, SerializableList<InventoryData>> item in (Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH) { if (string.Equals(item.Key, "contentlib|quarantine", StringComparison.Ordinal)) { val = item.Value; continue; } SerializableList<InventoryData> holder = item.Value; if (holder != null) { plan.PlayerList(holder.list, delegate(List<InventoryData> list) { holder.list = list; }); } } } if (state.SHOPKEEPER_INVENTORY != null) { foreach (ShopKeeperInventoryData shop in ((Dictionary<string, ShopKeeperInventoryData>)(object)state.SHOPKEEPER_INVENTORY).Values) { if (shop != null) { plan.ShopList(shop.items, delegate(List<InventoryData> list) { shop.items = list; }); plan.ShopList(shop.itemsHidden, delegate(List<InventoryData> list) { shop.itemsHidden = list; }); } } } plan.Sacrificed(state.SACRIFICED_WEAPONS, delegate(List<ItemId> list) { state.SACRIFICED_WEAPONS = list; }); plan.PrinterCounters(state.WEAPON_PRINTER_COUNTER, delegate(SerializableDictionary<ItemId, int> counters) { state.WEAPON_PRINTER_COUNTER = counters; }); plan.Quests(state.QUESTGIVER_CURRENT_QUEST, delegate(SerializableDictionary<string, SerializableList<QuestRequirement>> quests) { state.QUESTGIVER_CURRENT_QUEST = quests; }); plan.Commit(); plan.Report.InsuredReturned = InsuredHold.PutBack(state, plan.NewQuarantine); if (plan.NewQuarantine.Count > 0) { if (state.STASH == null) { state.STASH = new SerializableDictionary<string, SerializableList<InventoryData>>(); } if (val?.list != null) { val.list.AddRange(plan.NewQuarantine); } else { ((Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH)["contentlib|quarantine"] = new SerializableList<InventoryData>(plan.NewQuarantine); } } if (state.PLAYER_RECORDS == null) { state.PLAYER_RECORDS = new SerializableDictionary<string, int>(); } SaveMap.Stamp((IDictionary<string, int>)state.PLAYER_RECORDS, live); return plan.Report; } internal static bool ContainsLibraryNumbers(SulfurSaveState state) { //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) if (AnyInList(state.INVENTORY) || AnyInList(state.INSURED_ITEMS)) { return true; } if (state.STASH != null) { foreach (SerializableList<InventoryData> value in ((Dictionary<string, SerializableList<InventoryData>>)(object)state.STASH).Values) { if (value != null && AnyInList(value.list)) { return true; } } } if (state.SHOPKEEPER_INVENTORY != null) { foreach (ShopKeeperInventoryData value2 in ((Dictionary<string, ShopKeeperInventoryData>)(object)state.SHOPKEEPER_INVENTORY).Values) { if (value2 != null && (AnyInList(value2.items) || AnyInList(value2.itemsHidden))) { return true; } } } if (state.SACRIFICED_WEAPONS != null) { foreach (ItemId sACRIFICED_WEAPON in state.SACRIFICED_WEAPONS) { if (Numbering.IsItemNumber(sACRIFICED_WEAPON.value)) { return true; } } } if (state.WEAPON_PRINTER_COUNTER != null) { foreach (ItemId key in ((Dictionary<ItemId, int>)(object)state.WEAPON_PRINTER_COUNTER).Keys) { if (Numbering.IsItemNumber(key.value)) { return true; } } } if (state.QUESTGIVER_CURRENT_QUEST != null) { foreach (SerializableList<QuestRequirement> value3 in ((Dictionary<string, SerializableList<QuestRequirement>>)(object)state.QUESTGIVER_CURRENT_QUEST).Values) { if (value3?.list == null) { continue; } foreach (QuestRequirement item in value3.list) { if (item != null && Numbering.IsItemNumber(item.ItemId.value)) { return true; } } } } return false; } private static bool AnyInList(List<InventoryData> list) { if (list == null) { return false; } foreach (InventoryData item in list) { if (item != null && (Numbering.IsItemNumber(item.id.value) || AnyInArray(item.attachmentIds) || AnyInArray(item.enchantmentIds))) { return true; } } return false; } private static bool AnyInArray(ItemId[] ids) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (ids == null) { return false; } for (int i = 0; i < ids.Length; i++) { if (Numbering.IsItemNumber(ids[i].value)) { return true; } } return false; } } internal static class StashRestore { internal const string MainStashKey = "Stash"; internal static List<KeyValuePair<InventoryData, string>> SelectRestorable(List<InventoryData> quarantine, IReadOnlyDictionary<string, ushort> live) { List<KeyValuePair<InventoryData, string>> list = new List<KeyValuePair<InventoryData, string>>(); if (quarantine == null) { return list; } foreach (InventoryData item in quarantine) { if (QuarantineFormat.TryGetKey(item, out var key) && live.ContainsKey(key)) { list.Add(new KeyValuePair<InventoryData, string>(item, key)); } } return list; } internal static void Run() { //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: 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_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) ContentTables tables = LibraryState.Tables; SulfurSaveState active = SulfurSaveState.active; if (tables == null || LibraryState.IsFaulted || active?.STASH == null || !SaveStateTracker.TryGet(active, out var status, out var _) || status != SaveStateStatus.Normalized || !((Dictionary<string, SerializableList<InventoryData>>)(object)active.STASH).TryGetValue("contentlib|quarantine", out SerializableList<InventoryData> value) || value?.list == null) { return; } List<KeyValuePair<InventoryData, string>> list = SelectRestorable(value.list, tables.LiveItemNumbers); if (list.Count == 0) { return; } UIManager instance = StaticInstance<UIManager>.Instance; object obj; if (instance == null) { obj = null; } else { InventoryUI inventoryUI = instance.InventoryUI; obj = ((inventoryUI != null) ? inventoryUI.GetStashItemGrid(string.Empty) : null); } ItemGrid val = (ItemGrid)obj; if ((Object)(object)val == (Object)null) { return; } int num = 0; int num2 = 0; foreach (KeyValuePair<InventoryData, string> item in list) { ItemDefinition val2 = tables.ItemsByKey[item.Value]; if (val2.deprecated || val2.unlocksCheckpointWhenBroughtToChurch) { continue; } Vector2Int inventorySize = val2.inventorySize; Vector2Int possibleSpace = val.GetPossibleSpace(inventorySize, false, false, false); if (((Vector2Int)(ref possibleSpace)).x < 0 || ((Vector2Int)(ref possibleSpace)).y < 0) { num2++; } else if (TryPlace(val, item.Key, val2, tables.LiveItemNumbers[item.Value], possibleSpace, inventorySize)) { value.list.Remove(item.Key); if (!((Dictionary<string, SerializableList<InventoryData>>)(object)active.STASH).TryGetValue("Stash", out SerializableList<InventoryData> value2) || value2?.list == null) { value2 = new SerializableList<InventoryData>(new List<InventoryData>()); ((Dictionary<string, SerializableList<InventoryData>>)(object)active.STASH)["Stash"] = value2; } value2.list.Add(item.Key); num++; } } if (num > 0 || num2 > 0) { LibraryLog.Source.LogInfo((object)($"Quarantine: restored {num} item(s) to the stash; {num2} did not fit and stay quarantined; " + $"{value.list.Count} entr(y/ies) remain.")); } } private static bool TryPlace(ItemGrid grid, InventoryData entry, ItemDefinition definition, ushort number, Vector2Int position, Vector2Int size) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_01f5: Unknown result type (might be due to invalid IL or missing references) ItemId oldId = entry.id; string oldIdentifier = entry.identifier; int[] oldPosition = entry.position; bool oldEquipped = entry.equipped; bool oldRotated = entry.rotated; int oldX = entry.xSize; int oldY = entry.ySize; string[] oldAttachments = entry.attachments; string[] oldEnchantments = entry.enchantments; ItemAttributeCollectionData oldAttributes = entry.attributes; entry.id = new ItemId(number); entry.identifier = null; entry.attachments = null; entry.enchantments = null; entry.position = new int[2] { ((Vector2Int)(ref position)).x, ((Vector2Int)(ref position)).y }; entry.equipped = false; entry.rotated = false; entry.xSize = ((Vector2Int)(ref size)).x; entry.ySize = ((Vector2Int)(ref size)).y; if (entry.attributes?.itemAttributes == null) { entry.attributes = QuarantineFormat.NoAttributes(); } try { grid.AddItem(entry); } catch (Exception arg) { LibraryLog.Source.LogError((object)$"Quarantine: restoring '{((Object)definition).name}' threw; it stays quarantined: {arg}"); Revert(); return false; } GridSlot slotAt = grid.GetSlotAt(((Vector2Int)(ref position)).x, ((Vector2Int)(ref position)).y); if (slotAt == null || (Object)(object)slotAt.ItemInSlot == (Object)null || slotAt.ItemInSlot.itemDefinition != definition) { LibraryLog.Source.LogError((object)($"Quarantine: '{((Object)definition).name}' was handed to the game but is not in the stash grid at {position}; " + "the game may have dropped it on the floor.")); } return true; void Revert() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) entry.id = oldId; entry.identifier = oldIdentifier; entry.position = oldPosition; entry.equipped = oldEquipped; entry.rotated = oldRotated; entry.xSize = oldX; entry.ySize = oldY; entry.attachments = oldAttachments; entry.enchantments = oldEnchantments; entry.attributes = oldAttributes; } } } } namespace Ryuka.Sulfur.ContentLib.Registry { internal static class ContentDigest { internal const string VersionPrefix = "v1."; private const int HashBytesKept = 20; private static ContentTables _cachedFor; private static string _cached; internal static string Compute(IEnumerable<KeyValuePair<string, ushort>> liveItemNumbers, IEnumerable<KeyValuePair<string, string>> liveEnchantmentKeyByItemKey) { if (liveItemNumbers == null) { throw new ArgumentNullException("liveItemNumbers"); } if (liveEnchantmentKeyByItemKey == null) { throw new ArgumentNullException("liveEnchantmentKeyByItemKey"); } byte[] array3; using (MemoryStream memoryStream = new MemoryStream()) { byte[] array = ContentMapText.Utf8(liveItemNumbers); memoryStream.Write(array, 0, array.Length); byte[] array2 = LinksUtf8(liveEnchantmentKeyByItemKey); memoryStream.Write(array2, 0, array2.Length); using SHA256 sHA = SHA256.Create(); array3 = sHA.ComputeHash(memoryStream.ToArray()); } StringBuilder stringBuilder = new StringBuilder("v1.", "v1.".Length + 40); for (int i = 0; i < 20; i++) { stringBuilder.Append("0123456789abcdef"[array3[i] >> 4]).Append("0123456789abcdef"[array3[i] & 0xF]); } return stringBuilder.ToString(); } internal static string For(RegistryPhase phase, bool isFaulted, ContentTables tables) { return For(phase, isFaulted, tables, Hash); } private static string Hash(ContentTables tables) { return Compute(tables.LiveItemNumbers, tables.LiveEnchantmentKeyByItemKey); } internal static string Current() { return For(LibraryState.Declarations.Phase, LibraryState.IsFaulted, LibraryState.Tables, ComputeOnce); } private static string For(RegistryPhase phase, bool isFaulted, ContentTables tables, Func<ContentTables, string> hash) { if (isFaulted) { throw new InvalidOperationException("the library is in the faulted state; its item ids are not trusted"); } if (phase != RegistryPhase.Sealed) { throw new InvalidOperationException("the registry is not sealed yet; item numbers are not final"); } if (tables == null) { throw new InvalidOperationException("the content tables were not published"); } return hash(tables); } private static string ComputeOnce(ContentTables tables) { if (tables != _cachedFor) { _cached = Hash(tables); _cachedFor = tables; } return _cached; } private static byte[] LinksUtf8(IEnumerable<KeyValuePair<string, string>> links) { List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>(links); list.Sort((KeyValuePair<string, string> a, KeyValuePair<string, string> b) => string.CompareOrdinal(a.Key, b.Key)); StringBuilder stringBuilder = new StringBuilder("links\n"); stringBuilder.Append(list.Count.ToString(CultureInfo.InvariantCulture)).Append('\n'); foreach (KeyValuePair<string, string> item in list) { stringBuilder.Append(item.Key).Append('>').Append(item.Value) .Append('\n'); } return Encoding.UTF8.GetBytes(stringBuilder.ToString()); } } internal static class ContentKey { internal const int MaxLength = 128; internal const char Separator = ':'; internal static bool TryValidate(string key, out string error) { if (key == null) { error = "the key is null"; return false; } if (key.Length == 0) { error = "the key is empty"; return false; } if (key.Length > 128) { error = $"the key is {key.Length} characters long; the limit is {128}"; return false; } int num = -1; for (int i = 0; i < key.Length; i++) { char c = key[i]; if (c == ':') { if (num >= 0) { error = "the key has more than one ':'"; return false; } num = i; } else if (!IsAllowed(c)) { error = $"the key contains '{c}' at position {i}; only A-Z, a-z, 0-9, '.', '_' and '-' are allowed"; return false; } } if (num < 0) { error = "the key has no ':' between the owner GUID and the name"; return false; } if (num == 0) { error = "the owner GUID before ':' is empty"; return false; } if (num == key.Length - 1) { error = "the name after ':' is empty"; return false; } error = null; return true; } private static bool IsAllowed(char c) { if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_') { return c == '-'; } return true; } } internal static class ContentMapText { internal static byte[] Utf8(IEnumerable<KeyValuePair<string, ushort>> entries) { List<KeyValuePair<string, ushort>> list = new List<KeyValuePair<string, ushort>>(entries); list.Sort((KeyValuePair<string, ushort> a, KeyValuePair<string, ushort> b) => string.CompareOrdinal(a.Key, b.Key)); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(list.Count.ToString(CultureInfo.InvariantCulture)).Append('\n'); foreach (KeyValuePair<string, ushort> item in list) { stringBuilder.Append(item.Key).Append('=').Append(item.Value.ToString(CultureInfo.InvariantCulture)) .Append('\n'); } return Encoding.UTF8.GetBytes(stringBuilder.ToString()); } } internal sealed class ContentTables { private sealed class ByReference : IEqualityComparer<EnchantmentDefinition> { internal static readonly ByReference Instance = new ByReference(); public bool Equals(EnchantmentDefinition x, EnchantmentDefinition y) { return x == y; } public int GetHashCode(EnchantmentDefinition obj) { return RuntimeHelpers.GetHashCode(obj); } } private readonly ItemDefinition[] _itemsBySlot = (ItemDefinition[])(object)new ItemDefinition[7232]; private readonly EnchantmentDefinition[] _enchantmentsBySlot; private readonly Dictionary<string, ushort> _liveItemNumbers = new Dictionary<string, ushort>(StringComparer.Ordinal); private readonly Dictionary<ushort, string> _liveItemKeys = new Dictionary<ushort, string>(); private readonly Dictionary<string, ItemDefinition> _itemsByKey = new Dictionary<string, ItemDefinition>(StringComparer.Ordinal); private readonly Dictionary<string, EnchantmentDefinition> _enchantmentsByKey = new Dictionary<string, EnchantmentDefinition>(StringComparer.Ordinal); private readonly Dictionary<string, ushort> _enchantmentNumbers = new Dictionary<string, ushort>(StringComparer.Ordinal); private readonly Dictionary<string, string> _liveEnchantmentKeyByItemKey = new Dictionary<string, string>(StringComparer.Ordinal); private readonly Dictionary<EnchantmentDefinition, string> _enchantmentKeys = new Dictionary<EnchantmentDefinition, string>(ByReference.Instance); internal int EnchantmentWindowStart { get; } internal IReadOnlyDictionary<string, ushort> LiveItemNumbers => _liveItemNumbers; internal IReadOnlyDictionary<ushort, string> LiveItemKeys => _liveItemKeys; internal IReadOnlyDictionary<string, ItemDefinition> ItemsByKey => _itemsByKey; internal IReadOnlyDictionary<string, EnchantmentDefinition> EnchantmentsByKey => _enchantmentsByKey; internal IReadOnlyDictionary<string, ushort> EnchantmentNumbers => _enchantmentNumbers; internal IReadOnlyDictionary<string, string> LiveEnchantmentKeyByItemKey => _liveEnchantmentKeyByItemKey; internal ContentTables(int enchantmentWindowSize) { if (enchantmentWindowSize < 0 || enchantmentWindowSize > 499) { throw new ArgumentOutOfRangeException("enchantmentWindowSize"); } EnchantmentWindowStart = Numbering.EnchantmentWindowStart(enchantmentWindowSize); _enchantmentsBySlot = (EnchantmentDefinition[])(object)new EnchantmentDefinition[enchantmentWindowSize]; } internal bool OwnsEnchantmentNumber(int value) { if (value >= EnchantmentWindowStart) { return value < 500; } return false; } internal ItemDefinition ResolveItem(int value) { return _itemsBySlot[value - 32768]; } internal EnchantmentDefinition ResolveEnchantment(int value) { return _enchantmentsBySlot[value - EnchantmentWindowStart]; } internal void AddItem(string key, ushort number, ItemDefinition definition) { if (!Numbering.IsItemNumber(number)) { throw new ArgumentOutOfRangeException("number"); } int num = number - 32768; if (_itemsBySlot[num] != null || _liveItemKeys.ContainsKey(number) || _liveItemNumbers.ContainsKey(key)) { throw new InvalidOperationException($"Item number {number} or key '{key}' is already live."); } _itemsBySlot[num] = definition; _liveItemNumbers.Add(key, number); _liveItemKeys.Add(number, key); _itemsByKey.Add(key, definition); } internal void AddEnchantment(string key, string appliedByItemKey, ushort number, EnchantmentDefinition definition) { if (!OwnsEnchantmentNumber(number)) { throw new ArgumentOutOfRangeException("number"); } int num = number - EnchantmentWindowStart; if (_enchantmentsBySlot[num] != null || _enchantmentsByKey.ContainsKey(key)) { throw new InvalidOperationException($"Enchantment number {number} or key '{key}' is already live."); } if (appliedByItemKey == null || !_liveItemNumbers.ContainsKey(appliedByItemKey)) { throw new InvalidOperationException("Enchantment '" + key + "' names an item that is not live: '" + appliedByItemKey + "'."); } if (_liveEnchantmentKeyByItemKey.ContainsKey(appliedByItemKey)) { throw new InvalidOperationException("Item '" + appliedByItemKey + "' already applies an enchantment."); } _enchantmentsBySlot[num] = definition; _enchantmentsByKey.Add(key, definition); _enchantmentNumbers.Add(key, number); _liveEnchantmentKeyByItemKey.Add(appliedByItemKey, key); if (definition != null) { _enchantmentKeys.Add(definition, key); } } internal bool TryGetEnchantmentKey(EnchantmentDefinition definition, out string key) { key = null; if (definition != null) { return _enchantmentKeys.TryGetValue(definition, out key); } return false; } } internal enum RegistryPhase { Declaring, Bootstrapping, Sealed } internal sealed class ItemDeclaration { internal string Key { get; } internal Func<ItemDefinition> Factory { get; } internal ItemDeclaration(string key, Func<ItemDefinition> factory) { Key = key; Factory = factory; } } internal sealed class EnchantmentDeclaration { internal string Key { get; } internal string AppliedByItemKey { get; } internal Func<EnchantmentDefinition> Factory { get; } internal EnchantmentDeclaration(string key, string appliedByItemKey, Func<EnchantmentDefinition> factory) { Key = key; AppliedByItemKey = appliedByItemKey; Factory = factory; } } internal sealed class RecipeDeclaration { internal string Key { get; } internal bool KnownFromStart { get; } internal Func<RecipeData> Factory { get; } internal RecipeDeclaration(string key, Func<RecipeData> factory, bool knownFromStart) { Key = key; Factory = factory; KnownFromStart = knownFromStart; } } internal sealed class DeclarationSet { private readonly Dictionary<string, ItemDeclaration> _items = new Dictionary<string, ItemDeclaration>(StringComparer.Ordinal); private readonly Dictionary<string, EnchantmentDeclaration> _enchantments = new Dictionary<string, EnchantmentDeclaration>(StringComparer.Ordinal); private readonly Dictionary<string, string> _keysByFoldedCase = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _enchantmentByAppliedBy = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, RecipeDeclaration> _recipes = new Dictionary<string, RecipeDeclaration>(StringComparer.Ordinal); internal RegistryPhase Phase { get; private set; } internal IEnumerable<RecipeDeclaration> Recipes => _recipes.Values; internal int RecipeCount => _recipes.Count; internal IEnumerable<ItemDeclaration> Items => _items.Values; internal IEnumerable<EnchantmentDeclaration> Enchantments => _enchantments.Values; internal int ItemCount => _items.Count; internal int EnchantmentCount => _enchantments.Count; internal bool TryGetItem(string key, out ItemDeclaration declaration) { return _items.TryGetValue(key, out declaration); } internal bool TryDeclareItem(string key, Func<ItemDefinition> factory, out string error) { if (!CheckCommon(key, factory, out error)) { return false; } if (TextStore.EndsWithReservedSuffix(key)) { error = "an item key cannot end with '_description' or '_flavor'; the game uses those names for another item's texts"; return false; } if (_enchantmentByAppliedBy.TryGetValue(key, out var value) && !HasExactAppliedBy(key)) { error = "enchantment '" + value + "' names its item with different letter case; keys that differ only by case are one key"; return false; } _items.Add(key, new ItemDeclaration(key, factory)); _keysByFoldedCase.Add(key, key); return true; } internal bool TryDeclareEnchantment(string key, string appliedByItemKey, Func<EnchantmentDefinition> factory, out string error) { if (!CheckCommon(key, factory, out error)) { return false; } if (_enchantments.Count >= 499) { error = $"{499} enchantments are already declared; no more ids exist below {500}"; return false; } if (_enchantmentByAppliedBy.TryGetValue(key, out var value)) { error = "the key is already named as the item that applies enchantment '" + value + "'"; return false; } if (!ContentKey.TryValidate(appliedByItemKey, out var error2)) { error = "the applying item key is invalid: " + error2; return false; } if (string.Equals(key, appliedByItemKey, StringComparison.OrdinalIgnoreCase)) { error = "an enchantment cannot be applied by its own key"; return false; } if (_enchantmentByAppliedBy.TryGetValue(appliedByItemKey, out var value2)) { error = "item '" + appliedByItemKey + "' already applies enchantment '" + value2 + "'; one item applies one enchantment"; return false; } if (_keysByFoldedCase.TryGetValue(appliedByItemKey, out var value3)) { if (_enchantments.ContainsKey(value3)) { error = "'" + appliedByItemKey + "' is an enchantment, not an item"; return false; } if (_recipes.ContainsKey(value3)) { error = "'" + appliedByItemKey + "' is a recipe, not an item"; return false; } if (!string.Equals(value3, appliedByItemKey, StringComparison.Ordinal)) { error = "the applying item key differs only by case from the declared item '" + value3 + "'"; return false; } } _enchantments.Add(key, new EnchantmentDeclaration(key, appliedByItemKey, factory)); _keysByFoldedCase.Add(key, key); _enchantmentByAppliedBy.Add(appliedByItemKey, key); return true; } internal bool TryDeclareRecipe(string key, Func<RecipeData> factory, out string error) { return TryDeclareRecipe(key, factory, knownFromStart: false, out error); } internal bool TryDeclareRecipe(string key, Func<RecipeData> factory, bool knownFromStart, out string error) { if (!CheckCommon(key, factory, out error)) { return false; } if (_enchantmentByAppliedBy.TryGetValue(key, out var value)) { error = "the key is already named as the item that applies enchantment '" + value + "'"; return false; } _recipes.Add(key, new RecipeDeclaration(key, factory, knownFromStart)); _keysByFoldedCase.Add(key, key); return true; } private bool HasExactAppliedBy(string itemKey) { foreach (EnchantmentDeclaration value in _enchantments.Values) { if (string.Equals(value.AppliedByItemKey, itemKey, StringComparison.Ordinal)) { return true; } } return false; } internal bool TryBeginBootstrap() { if (Phase != RegistryPhase.Declaring) { return false; } Phase = RegistryPhase.Bootstrapping; return true; } internal void Seal() { if (Phase != RegistryPhase.Bootstrapping) { throw new InvalidOperationException($"Cannot seal from phase {Phase}."); } Phase = RegistryPhase.Sealed; } private bool CheckCommon(string key, Delegate factory, out string error) { if (Phase != RegistryPhase.Declaring) { error = ((Phase == RegistryPhase.Sealed) ? "the registry is sealed; declare content in your plugin's Awake" : "the bootstrap has already started; declare content in your plugin's Awake"); return false; } if (!ContentKey.TryValidate(key, out error)) { return false; } if ((object)factory == null) { error = "the factory is null"; return false; } if (_keysByFoldedCase.TryGetValue(key, out var value)) { error = (string.Equals(value, key, StringComparison.Ordinal) ? "the key is already declared" : ("the key differs only by case from the declared key '" + value + "'")); return false; } error = null; return true; } } internal static class Fnv1a { private const uint OffsetBasis = 2166136261u; private const uint Prime = 16777619u; internal static uint Hash(string text) { return Hash(Encoding.UTF8.GetBytes(text)); } internal static uint Hash(byte[] bytes) { uint num = 2166136261u; for (int i = 0; i < bytes.Length; i++) { num ^= bytes[i]; num *= 16777619; } return num; } } internal static class Numbering { internal const int ItemFirst = 32768; internal const int ItemEndExclusive = 40000; internal const int ItemCapacity = 7232; internal const int EnchantmentEndExclusive = 500; internal const int EnchantmentMaxCapacity = 499; internal static bool IsItemNumber(int value) { if (value >= 32768) { return value < 40000; } return false; } internal static int EnchantmentWindowStart(int count) { return 500 - count; } internal static Dictionary<string, ushort> NumberItems(IEnumerable<string> keys, out List<string> overflow) { List<string> list = SortDistinct(keys); Dictionary<string, ushort> dictionary = new Dictionary<string, ushort>(list.Count, StringComparer.Ordinal); bool[] array = new bool[7232]; overflow = new List<string>(); foreach (string item in list) { if (dictionary.Count == 7232) { overflow.Add(item); continue; } int num = (int)(Fnv1a.Hash(item) % 7232); while (array[num]) { num = ((num + 1 != 7232) ? (num + 1) : 0); } array[num] = true; dictionary.Add(item, (ushort)(32768 + num)); } return dictionary; } internal static Dictionary<string, ushort> NumberEnchantments(IEnumerable<string> keys) { List<string> list = SortDistinct(keys); if (list.Count > 499) { throw new ArgumentException($"{list.Count} enchantments cannot fit below {500}.", "keys"); } int num = EnchantmentWindowStart(list.Count); Dictionary<string, ushort> dictionary = new Dictionary<string, ushort>(list.Count, StringComparer.Ordinal); for (int i = 0; i < list.Count; i++) { dictionary.Add(list[i], (ushort)(num + i)); } return dictionary; } private static List<string> SortDistinct(IEnumerable<string> keys) { List<stri