Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of DataForge v1.2.0
DataForge.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Timers; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.ObjectPool; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("DataForge")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("DataForge")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("4358610B-F3F4-4843-B7AF-98B7BC60DCDE")] [assembly: AssemblyFileVersion("1.2.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DataForge { internal static class LocalizationOverrideManager { private sealed class TranslationLease { internal bool OriginalExisted { get; } internal string? OriginalValue { get; } internal string LastAppliedValue { get; set; } = ""; internal TranslationLease(bool originalExisted, string? originalValue) { OriginalExisted = originalExisted; OriginalValue = originalValue; } } internal sealed class LocalizationPayload { public int Version { get; set; } public Dictionary<string, Dictionary<string, string>>? Languages { get; set; } } private const string DomainName = "localization"; private const string DefaultLanguageFileName = "English.yml"; private const string KoreanLanguageFileName = "Korean.yml"; private const string SyncedPayloadKey = "localization"; private const long ReloadDelayTicks = 10000000L; private const int PayloadVersion = 1; private const int MaxPayloadBytes = 2097152; private const int MaxLanguageCount = 32; private const int MaxLanguageNameLength = 64; private const int MaxTokensPerLanguage = 8192; private const int MaxTokenLength = 128; private const int MaxTextLength = 4096; private static readonly (string Token, string Text)[] BuiltInEnglishTranslations = new(string, string)[6] { ("$df_se_tooltip_attack_damage", "{0} attack damage: <color=orange>x{1}%</color>"), ("$df_se_tooltip_raise_skill", "{0} skill XP: <color=orange>{1}</color>"), ("$df_se_tooltip_max_health", "Max health: <color=orange>{0}</color>"), ("$df_se_tooltip_max_stamina", "Max stamina: <color=orange>{0}</color>"), ("$df_se_tooltip_max_eitr", "Max eitr: <color=orange>{0}</color>"), ("$df_skill_all", "All") }; private static readonly (string Token, string Text)[] BuiltInKoreanTranslations = new(string, string)[6] { ("$df_se_tooltip_attack_damage", "{0} 공격 피해: <color=orange>x{1}%</color>"), ("$df_se_tooltip_raise_skill", "{0} 기술 경험치: <color=orange>{1}</color>"), ("$df_se_tooltip_max_health", "최대 체력: <color=orange>{0}</color>"), ("$df_se_tooltip_max_stamina", "최대 스태미나: <color=orange>{0}</color>"), ("$df_se_tooltip_max_eitr", "최대 에이트르: <color=orange>{0}</color>"), ("$df_skill_all", "전체") }; private static readonly object StateLock = new object(); private static readonly IDeserializer Deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).WithDuplicateKeyChecking().Build(); private static readonly ISerializer Serializer = new SerializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).DisableAliases().Build(); private static LocalizationPayload ActivePayload = CreateEmptyPayload(); private static ConfigSync? ConfigSyncInstance; private static CustomSyncedValue<string>? SyncedPayload; private static FileSystemWatcher? Watcher; private static DataForgeFileWatcher.DebouncedAction? ReloadDebouncer; private static string? LastParsedPayload; private static bool LocalFileModeReady; private static bool WatcherResetPending; private static readonly Dictionary<string, TranslationLease> AppliedTranslations = new Dictionary<string, TranslationLease>(StringComparer.Ordinal); private static Localization? AppliedLocalization; private static string AppliedLanguage = ""; private static string ConfigDirectory => Path.Combine(Paths.ConfigPath, "DataForge"); private static string LocalizationDirectory => Path.Combine(ConfigDirectory, "localization"); private static bool HasLocalAuthority => ConfigSyncInstance?.IsSourceOfTruth ?? false; internal static void Initialize(ConfigSync configSync) { ConfigSyncInstance = configSync; SyncedPayload = new CustomSyncedValue<string>(configSync, "localization", "", 100); SyncedPayload.ValueChanged += OnSyncedPayloadChanged; configSync.SourceOfTruthChanged += OnSourceOfTruthChanged; EnsureSourceOfTruthFileMode(); } internal static void Dispose() { RestoreAppliedTranslations(); if (SyncedPayload != null) { SyncedPayload.ValueChanged -= OnSyncedPayloadChanged; SyncedPayload = null; } if (ConfigSyncInstance != null) { ConfigSyncInstance.SourceOfTruthChanged -= OnSourceOfTruthChanged; ConfigSyncInstance = null; } Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; LocalFileModeReady = false; WatcherResetPending = false; lock (StateLock) { ActivePayload = CreateEmptyPayload(); LastParsedPayload = null; } } internal static void EnsureSourceOfTruthFileMode() { if (!HasLocalAuthority || LocalFileModeReady) { return; } LocalFileModeReady = true; try { SetupFileWatcher(); if (!ReloadFromDiskAndSync()) { NotifyLocalizationChanged(); } } catch (Exception arg) { LocalFileModeReady = false; Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; DataForgePlugin.Log.LogError((object)$"Failed to initialize server localization files: {arg}"); } } private static void SetupFileWatcher() { Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; WatcherResetPending = false; if (HasLocalAuthority) { EnsureConfigDirectoryAndDefaultOverride(); ReloadDebouncer = DataForgeFileWatcher.CreateDebouncedAction(10000000L, ReloadYamlValues); CreateLocalizationWatcher(); } } private static void CreateLocalizationWatcher() { Directory.CreateDirectory(LocalizationDirectory); FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(LocalizationDirectory, "*.*"); fileSystemWatcher.IncludeSubdirectories = false; fileSystemWatcher.SynchronizingObject = ThreadingHelper.SynchronizingObject; fileSystemWatcher.Changed += ReadYamlValues; fileSystemWatcher.Created += ReadYamlValues; fileSystemWatcher.Deleted += ReadYamlValues; fileSystemWatcher.Renamed += delegate(object sender, RenamedEventArgs args) { ReadYamlValues(sender, args); }; fileSystemWatcher.Error += OnWatcherError; fileSystemWatcher.EnableRaisingEvents = true; Watcher = fileSystemWatcher; } private static bool ReloadFromDiskAndSync() { if (!HasLocalAuthority) { return false; } EnsureConfigDirectoryAndDefaultOverride(); if (!TryLoadPayloadFromDisk(out LocalizationPayload payload)) { return false; } if (!TrySerializeAndVerifyPayload(payload, out string serialized, out LocalizationPayload verified)) { return false; } lock (StateLock) { ActivePayload = verified; LastParsedPayload = serialized; } PublishPayload(serialized); ApplyCurrentLocalization(); NotifyLocalizationChanged(); return true; } internal static void ApplyCurrentLocalization() { Localization instance = Localization.m_instance; if (instance != null) { ApplyCurrentLocalization(instance, instance.GetSelectedLanguage()); } } internal static void ApplyCurrentLocalization(Localization localization, string? language) { if (!IsLiveLocalization(localization)) { return; } string text = NormalizeLanguage(language); if (AppliedLocalization != localization || !AppliedLanguage.Equals(text, StringComparison.OrdinalIgnoreCase)) { RestoreAppliedTranslations(); AppliedLocalization = localization; AppliedLanguage = text; } Dictionary<string, string> dictionary; lock (StateLock) { dictionary = BuildTranslationsForLanguage(ActivePayload, text); } bool flag = RestoreRemovedTranslations(localization, dictionary.Keys); foreach (KeyValuePair<string, string> item in dictionary) { flag |= ApplyTranslation(localization, item.Key, item.Value); } if (flag || dictionary.Count > 0) { localization.m_cache.EvictAll(); } } internal static void BeforeLanguageSetup(Localization localization) { if (IsLiveLocalization(localization)) { RestoreAppliedTranslations(localization); } } internal static void OnWorldShutdown() { ConfigSync? configSyncInstance = ConfigSyncInstance; if (configSyncInstance != null && !configSyncInstance.IsSourceOfTruth) { LocalFileModeReady = false; ClearActivePayloadAndRestore(); } else { RestoreAppliedTranslations(); } } private static bool IsLiveLocalization(Localization localization) { if (localization != null && Localization.m_instance != null) { return localization == Localization.m_instance; } return false; } private static void ReadYamlValues(object sender, FileSystemEventArgs e) { if (ShouldReloadForFileEvent(e)) { ReloadDebouncer?.Schedule(); } } private static void ReloadYamlValues() { try { DataForgePlugin.Log.LogDebug((object)"Reloading localization YAML files..."); if (ReloadFromDiskAndSync()) { DataForgePlugin.Log.LogInfo((object)"Localization YAML reload complete."); } } catch (Exception arg) { DataForgePlugin.Log.LogError((object)$"Error reloading localization YAML files: {arg}"); } finally { if (WatcherResetPending && HasLocalAuthority) { WatcherResetPending = false; try { Watcher?.Dispose(); Watcher = null; CreateLocalizationWatcher(); } catch (Exception arg2) { DataForgePlugin.Log.LogError((object)$"Failed to rebuild the localization file watcher: {arg2}"); } } } } private static void OnWatcherError(object sender, ErrorEventArgs e) { if (HasLocalAuthority) { DataForgePlugin.Log.LogWarning((object)("Localization file watcher lost events and will be rebuilt: " + e.GetException().Message)); WatcherResetPending = true; ReloadDebouncer?.Schedule(); } } private static bool ShouldReloadForFileEvent(FileSystemEventArgs e) { if (!HasLocalAuthority) { return false; } if (IsLocalizationFile(e.FullPath)) { return true; } if (e is RenamedEventArgs e2) { return IsLocalizationFile(e2.OldFullPath); } return false; } private static void OnSyncedPayloadChanged() { if (!HasLocalAuthority) { ApplySyncedPayload(SyncedPayload?.Value ?? ""); } } private static void OnSourceOfTruthChanged(bool isSourceOfTruth) { LocalFileModeReady = false; if (isSourceOfTruth) { ClearActivePayloadAndRestore(); if (HasLocalAuthority) { EnsureSourceOfTruthFileMode(); } else { NotifyLocalizationChanged(); } } else { Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; WatcherResetPending = false; ClearActivePayloadAndRestore(); NotifyLocalizationChanged(); } } private static void ApplySyncedPayload(string payload) { if (!string.Equals(LastParsedPayload, payload, StringComparison.Ordinal)) { if (!TryDeserializePayload(payload, "synced localization payload", out LocalizationPayload localizationPayload)) { return; } lock (StateLock) { ActivePayload = localizationPayload; LastParsedPayload = payload; } } ApplyCurrentLocalization(); NotifyLocalizationChanged(); } private static void PublishPayload(string payload) { DataForgeSync.PublishPayload(SyncedPayload, "localization", payload); } private static LocalizationPayload LoadPayloadFromDisk() { LocalizationPayload localizationPayload = CreateEmptyPayload(); if (!Directory.Exists(LocalizationDirectory)) { return localizationPayload; } string[] array = Directory.GetFiles(LocalizationDirectory, "*.yml").Concat(Directory.GetFiles(LocalizationDirectory, "*.yaml")).OrderBy<string, string>((string path) => path, StringComparer.OrdinalIgnoreCase) .ToArray(); if (array.Length > 32) { throw new InvalidDataException($"Localization contains {array.Length} language files; the limit is {32}."); } long num = 0L; HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array2 = array; foreach (string obj in array2) { FileInfo fileInfo = new FileInfo(obj); num += fileInfo.Length; if (fileInfo.Length > 2097152 || num > 2097152) { throw new InvalidDataException($"Localization files exceed the {2097152}-byte safety limit."); } string text = Path.GetFileNameWithoutExtension(obj).Trim(); if (text.Length == 0 || text.Length > 64) { throw new InvalidDataException($"Localization language names must contain from 1 to {64} characters."); } if (!hashSet.Add(text)) { throw new InvalidDataException("Localization has more than one file for language '" + text + "' when compared case-insensitively."); } Dictionary<string, string> value = LoadTranslationMap(obj, text + " localization"); localizationPayload.Languages[text] = value; } return localizationPayload; } private static bool TryLoadPayloadFromDisk(out LocalizationPayload payload) { try { payload = LoadPayloadFromDisk(); return true; } catch (Exception ex) { DataForgePlugin.Log.LogError((object)("Localization reload failed; keeping the last-known-good configuration. " + ex.Message)); payload = CreateEmptyPayload(); return false; } } private static Dictionary<string, string> LoadTranslationMap(string path, string source) { if (!File.Exists(path)) { return new Dictionary<string, string>(StringComparer.Ordinal); } if (new FileInfo(path).Length > 2097152) { throw new InvalidDataException($"{source} exceeds the {2097152}-byte safety limit."); } string text = File.ReadAllText(path); if (string.IsNullOrWhiteSpace(text)) { return new Dictionary<string, string>(StringComparer.Ordinal); } try { YamlStream yamlStream = new YamlStream(); using StringReader input = new StringReader(text); yamlStream.Load(input); if (yamlStream.Documents.Count == 0) { return new Dictionary<string, string>(StringComparer.Ordinal); } if (yamlStream.Documents.Count != 1) { throw new FormatException(source + " must contain exactly one YAML document."); } if (!(yamlStream.Documents[0].RootNode is YamlMappingNode yamlMappingNode)) { throw new FormatException(source + " must be a flat token-to-text mapping."); } if (yamlMappingNode.Children.Count > 8192) { throw new FormatException($"{source} contains {yamlMappingNode.Children.Count} tokens; the limit is {8192}."); } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<YamlNode, YamlNode> child in yamlMappingNode.Children) { if (!TryNormalizeToken(((child.Key as YamlScalarNode) ?? throw new FormatException(source + " has an invalid token: token keys must be scalar strings.")).Value, out string token, out string error)) { throw new FormatException(source + " has an invalid token: " + error); } if (!hashSet.Add(token)) { throw new FormatException(source + " has duplicate normalized token '" + token + "' when compared case-insensitively."); } if (!(child.Value is YamlScalarNode yamlScalarNode) || string.IsNullOrEmpty(yamlScalarNode.Value)) { throw new FormatException(source + " token '" + token + "' must have a non-empty scalar string value."); } string value = yamlScalarNode.Value; if (value.Length > 4096) { throw new FormatException($"{source} token '{token}' exceeds the {4096}-character text limit."); } dictionary[token] = value; } return dictionary; } catch (Exception ex) { throw new InvalidDataException("Failed to parse " + source + " from '" + path + "': " + ex.Message, ex); } } private static bool TrySerializeAndVerifyPayload(LocalizationPayload payload, out string serialized, out LocalizationPayload verified) { serialized = ""; verified = CreateEmptyPayload(); try { serialized = Serializer.Serialize(payload); if (Encoding.UTF8.GetByteCount(serialized) > 2097152) { DataForgePlugin.Log.LogError((object)$"Localization payload exceeds the {2097152}-byte safety limit; keeping the last-known-good configuration."); return false; } } catch (Exception ex) { DataForgePlugin.Log.LogError((object)("Failed to serialize localization payload: " + ex.Message)); return false; } return TryDeserializePayload(serialized, "local localization payload round-trip", out verified); } private static bool TryDeserializePayload(string payload, string source, out LocalizationPayload localizationPayload) { localizationPayload = CreateEmptyPayload(); if (string.IsNullOrWhiteSpace(payload)) { DataForgePlugin.Log.LogError((object)(source + " was rejected because the payload is empty.")); return false; } if (Encoding.UTF8.GetByteCount(payload) > 2097152) { DataForgePlugin.Log.LogError((object)$"{source} was rejected because it exceeds the {2097152}-byte safety limit."); return false; } try { LocalizationPayload payload2 = Deserializer.Deserialize<LocalizationPayload>(payload); localizationPayload = NormalizePayload(payload2, source); return true; } catch (Exception ex) { DataForgePlugin.Log.LogError((object)(source + " was rejected; keeping the last-known-good configuration. " + ex.Message)); return false; } } private static LocalizationPayload NormalizePayload(LocalizationPayload? payload, string source) { if (payload == null || payload.Version != 1 || payload.Languages == null) { string arg = ((payload == null) ? "missing" : payload.Version.ToString()); throw new InvalidDataException($"{source} must have version {1} and a languages mapping; received version {arg}."); } if (payload.Languages.Count > 32) { throw new InvalidDataException($"{source} contains {payload.Languages.Count} languages; the limit is {32}."); } LocalizationPayload localizationPayload = CreateEmptyPayload(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, Dictionary<string, string>> language in payload.Languages) { string text = language.Key?.Trim() ?? ""; if (text.Length == 0 || text.Length > 64) { throw new InvalidDataException($"{source} language names must contain from 1 to {64} characters."); } if (!hashSet.Add(text)) { throw new InvalidDataException(source + " has duplicate language '" + text + "' when compared case-insensitively."); } if (language.Value == null || language.Value.Count > 8192) { string text2 = ((language.Value == null) ? "null" : language.Value.Count.ToString()); throw new InvalidDataException($"{source} language '{text}' has {text2} tokens; the limit is {8192}."); } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> item in language.Value) { if (!TryNormalizeToken(item.Key, out string token, out string error)) { throw new InvalidDataException(source + " language '" + text + "' has an invalid token: " + error); } if (!hashSet2.Add(token)) { throw new InvalidDataException(source + " language '" + text + "' has duplicate normalized token '" + token + "' when compared case-insensitively."); } if (string.IsNullOrEmpty(item.Value) || item.Value.Length > 4096) { throw new InvalidDataException($"{source} language '{text}' token '{token}' must be non-empty and no longer than {4096} characters."); } dictionary[token] = item.Value; } localizationPayload.Languages[text] = dictionary; } return localizationPayload; } private static Dictionary<string, string> BuildTranslationsForLanguage(LocalizationPayload payload, string language) { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal); if (payload.Languages != null && payload.Languages.TryGetValue("English", out Dictionary<string, string> value)) { MergeTranslations(dictionary, value); } if (!language.Equals("English", StringComparison.OrdinalIgnoreCase) && payload.Languages != null && payload.Languages.TryGetValue(language, out Dictionary<string, string> value2)) { MergeTranslations(dictionary, value2); } return dictionary; } private static void MergeTranslations(Dictionary<string, string> target, Dictionary<string, string> source) { foreach (KeyValuePair<string, string> item in source) { target[item.Key] = item.Value; } } private static bool ApplyTranslation(Localization localization, string token, string text) { string value; bool flag = localization.m_translations.TryGetValue(token, out value); if (!AppliedTranslations.TryGetValue(token, out TranslationLease value2) || !flag || !string.Equals(value, value2.LastAppliedValue, StringComparison.Ordinal)) { value2 = new TranslationLease(flag, value); AppliedTranslations[token] = value2; } bool result = !flag || !string.Equals(value, text, StringComparison.Ordinal); localization.m_translations[token] = text; value2.LastAppliedValue = text; return result; } private static bool RestoreRemovedTranslations(Localization localization, IEnumerable<string> currentTokens) { HashSet<string> current = new HashSet<string>(currentTokens, StringComparer.Ordinal); bool flag = false; string[] array = AppliedTranslations.Keys.Where((string token) => !current.Contains(token)).ToArray(); foreach (string text in array) { flag |= RestoreTranslationIfOwned(localization, text); AppliedTranslations.Remove(text); } return flag; } private static void RestoreAppliedTranslations() { RestoreAppliedTranslations(AppliedLocalization); } private static void RestoreAppliedTranslations(Localization? localization) { if (localization == null) { ClearAppliedTranslationState(); return; } bool flag = false; string[] array = AppliedTranslations.Keys.ToArray(); foreach (string token in array) { flag |= RestoreTranslationIfOwned(localization, token); } if (flag) { localization.m_cache.EvictAll(); } ClearAppliedTranslationState(); } private static bool RestoreTranslationIfOwned(Localization localization, string token) { if (!AppliedTranslations.TryGetValue(token, out TranslationLease value) || !localization.m_translations.TryGetValue(token, out var value2) || !string.Equals(value2, value.LastAppliedValue, StringComparison.Ordinal)) { return false; } if (value.OriginalExisted) { localization.m_translations[token] = value.OriginalValue ?? ""; } else { localization.m_translations.Remove(token); } return true; } private static void ClearAppliedTranslationState() { AppliedLocalization = null; AppliedLanguage = ""; AppliedTranslations.Clear(); } private static void ClearActivePayloadAndRestore() { lock (StateLock) { ActivePayload = CreateEmptyPayload(); LastParsedPayload = null; } RestoreAppliedTranslations(); } private static void EnsureConfigDirectoryAndDefaultOverride() { Directory.CreateDirectory(ConfigDirectory); Directory.CreateDirectory(LocalizationDirectory); string path = Path.Combine(LocalizationDirectory, "English.yml"); if (!File.Exists(path)) { File.WriteAllText(path, DefaultEnglishLocalizationTemplate()); } EnsureBuiltInTranslations(path, (IReadOnlyCollection<(string Token, string Text)>)(object)BuiltInEnglishTranslations, "# Built-in DataForge tooltip tokens. You can edit these texts."); string path2 = Path.Combine(LocalizationDirectory, "Korean.yml"); if (!File.Exists(path2)) { File.WriteAllText(path2, DefaultKoreanLocalizationTemplate()); } EnsureBuiltInTranslations(path2, (IReadOnlyCollection<(string Token, string Text)>)(object)BuiltInKoreanTranslations, "# DataForge 기본 툴팁 토큰입니다. 원하는 문구로 수정할 수 있습니다."); } private static string DefaultEnglishLocalizationTemplate() { return string.Join(Environment.NewLine, "# DataForge server-synced localization.", "#", "# Put language files in this folder using Valheim language names:", "# English.yml, Korean.yml, Turkish.yml, German.yml, etc.", "#", "# English.yml is the fallback file. If a client uses another language,", "# DataForge first applies English.yml and then applies that client's language file.", "#", "# To use a localization key, put a token like $df_item_meadhealthtest in an override field.", "# To override text directly, put plain text in the field instead of a $ token.", "#", "# Example localization entry:", "# $df_item_meadhealthtest: \"Test item\"", "# $df_item_meadhealthtest_description: \"A test item cloned from major healing mead.\"", "", "# Built-in DataForge tooltip tokens. You can edit these texts.", FormatBuiltInTranslationLines(BuiltInEnglishTranslations), "#", "# Example item override:", "# - item: MeadHealthtest", "# cloneFrom: MeadHealthMajor", "# name: $df_item_meadhealthtest", "# description: Direct text override without localization", ""); } private static string FormatBuiltInTranslationLines(IEnumerable<(string Token, string Text)> translations) { return string.Join(Environment.NewLine, translations.Select<(string, string), string>(((string Token, string Text) entry) => entry.Token + ": " + QuoteYaml(entry.Text))); } private static string DefaultKoreanLocalizationTemplate() { return string.Join(Environment.NewLine, "# DataForge 서버 동기화 localization 파일입니다.", "#", "# 이 폴더에는 Valheim 언어 이름을 파일명으로 사용합니다:", "# English.yml, Korean.yml, Turkish.yml, German.yml 등.", "#", "# English.yml은 기본 fallback 파일입니다. 클라이언트 언어가 한국어이면", "# DataForge는 English.yml을 먼저 적용한 뒤 Korean.yml을 덮어씁니다.", "#", "# override 필드에 $df_item_meadhealthtest 같은 토큰을 넣으면 이 파일의 번역을 사용합니다.", "# $ 토큰을 쓰지 않고 필드에 직접 텍스트를 넣어도 그대로 표시됩니다.", "#", "# 예시 localization 항목:", "# $df_item_meadhealthtest: \"테스트 아이템\"", "# $df_item_meadhealthtest_description: \"대형 체력 벌꿀주를 복제한 테스트 아이템입니다.\"", "", "# DataForge 기본 툴팁 토큰입니다. 원하는 문구로 수정할 수 있습니다.", FormatBuiltInTranslationLines(BuiltInKoreanTranslations), "#", "# 예시 item override:", "# - item: MeadHealthtest", "# cloneFrom: MeadHealthMajor", "# name: $df_item_meadhealthtest", "# description: localization 토큰 없이 직접 입력한 설명", ""); } private static void EnsureBuiltInTranslations(string path, IReadOnlyCollection<(string Token, string Text)> translations, string header) { Dictionary<string, string> dictionary; string text; try { dictionary = LoadTranslationMap(path, "built-in localization file '" + Path.GetFileName(path) + "'"); text = File.ReadAllText(path); } catch { return; } HashSet<string> existingTokens = new HashSet<string>(dictionary.Keys, StringComparer.OrdinalIgnoreCase); string token; string error; List<(string, string)> list = translations.Where<(string, string)>(((string Token, string Text) entry) => TryNormalizeToken(entry.Token, out token, out error) && !existingTokens.Contains(token)).ToList(); if (list.Count == 0) { return; } using StreamWriter streamWriter = File.AppendText(path); if (text.Length > 0 && !text.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { streamWriter.WriteLine(); } streamWriter.WriteLine(); streamWriter.WriteLine(header); foreach (var (text2, value) in list) { streamWriter.WriteLine(text2 + ": " + QuoteYaml(value)); } } private static string QuoteYaml(string value) { return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } private static bool IsLocalizationFile(string path) { string extension = Path.GetExtension(path); if (!extension.Equals(".yml", StringComparison.OrdinalIgnoreCase) && !extension.Equals(".yaml", StringComparison.OrdinalIgnoreCase)) { return false; } string fullPath = Path.GetFullPath(path); return string.Equals(b: Path.GetFullPath(LocalizationDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), a: Path.GetDirectoryName(fullPath)?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), comparisonType: StringComparison.OrdinalIgnoreCase); } private static bool TryNormalizeToken(string? value, out string token, out string error) { token = value?.Trim() ?? ""; error = ""; if (token.StartsWith("$", StringComparison.Ordinal)) { token = token.Substring(1); } if (token.Length == 0 || token.Length > 128) { error = $"token names must contain from 1 to {128} characters after an optional leading '$'."; return false; } if (token.IndexOf('$') >= 0 || token.Any((char character) => char.IsControl(character) || " (){}[]+-!?/\\&%,.:-=<>\r\n\t".IndexOf(character) >= 0)) { error = "token '" + token + "' contains a character that terminates Valheim localization tokens."; return false; } return true; } private static string NormalizeLanguage(string? language) { string text = language?.Trim() ?? ""; if (text.Length != 0) { return text; } return "English"; } private static void NotifyLocalizationChanged() { if (Localization.m_instance == null || Localization.OnLanguageChange == null) { return; } Delegate[] invocationList = Localization.OnLanguageChange.GetInvocationList(); foreach (Delegate obj in invocationList) { try { ((Action)obj)(); } catch (Exception ex) { DataForgePlugin.Log.LogWarning((object)("A localized UI subscriber failed after a DataForge localization update: " + ex.Message)); } } } private static LocalizationPayload CreateEmptyPayload() { return new LocalizationPayload { Version = 1, Languages = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase) }; } } [HarmonyPatch(typeof(FejdStartup), "SetupGui")] internal static class DataForgeLocalizationFejdStartupPatch { [HarmonyPriority(0)] private static void Postfix() { try { LocalizationOverrideManager.ApplyCurrentLocalization(); } catch (Exception ex) { DataForgePlugin.Log.LogWarning((object)("Failed to apply DataForge localization to the startup UI: " + ex.Message)); } } } [HarmonyPatch(typeof(Localization), "SetupLanguage")] internal static class DataForgeLocalizationSetupLanguagePatch { [HarmonyPriority(800)] private static void Prefix(Localization __instance) { try { LocalizationOverrideManager.BeforeLanguageSetup(__instance); } catch (Exception ex) { DataForgePlugin.Log.LogWarning((object)("Failed to prepare DataForge localization before a language change: " + ex.Message)); } } [HarmonyPriority(0)] private static void Postfix(Localization __instance, string language) { try { LocalizationOverrideManager.ApplyCurrentLocalization(__instance, language); } catch (Exception ex) { DataForgePlugin.Log.LogWarning((object)("Failed to apply DataForge localization for '" + language + "': " + ex.Message)); } } } [BepInPlugin("sighsorry.DataForge", "DataForge", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class DataForgePlugin : BaseUnityPlugin { public enum Toggle { On = 1, Off = 0 } public enum UpgradeMaterialScalingMode { Vanilla, Flat, Reduced } private sealed class ConfigurationManagerAttributes { [UsedImplicitly] public int? Order; [UsedImplicitly] public bool? Browsable; [UsedImplicitly] public string? Category; [UsedImplicitly] public Action<ConfigEntryBase>? CustomDrawer; } internal const string ModName = "DataForge"; internal const string ModVersion = "1.2.0"; internal const string Author = "sighsorry"; internal const string ModGUID = "sighsorry.DataForge"; private static readonly string ConfigFileName = "sighsorry.DataForge.cfg"; private static readonly string ConfigFileFullPath = Path.Combine(Paths.ConfigPath, ConfigFileName); private static readonly ConfigSync ConfigSync = new ConfigSync("sighsorry.DataForge") { DisplayName = "DataForge", CurrentVersion = "1.2.0", MinimumRequiredVersion = "1.2.0", ModRequired = true }; private readonly Harmony _harmony = new Harmony("sighsorry.DataForge"); private readonly object _reloadLock = new object(); private FileSystemWatcher? _watcher; private DataForgeFileWatcher.DebouncedAction? _configReloadDebouncer; private string? _lastConfigFileText; private bool _configReloadInProgress; private static bool _sourceOfTruthFileModeReady; internal static readonly ManualLogSource Log = Logger.CreateLogSource("DataForge"); private const long ReloadDelayTicks = 10000000L; private const int MaxStoredFireplaceFuelLimit = 9999; private static ConfigEntry<Toggle> _serverConfigLocked = null; private static ConfigEntry<Toggle> _enableItemOverrides = null; private static ConfigEntry<Toggle> _enableRecipeOverrides = null; private static ConfigEntry<Toggle> _enableStatusEffectOverrides = null; private static ConfigEntry<Toggle> _enablePieceOverrides = null; private static ConfigEntry<int> _stackableStackMultiplier = null; private static ConfigEntry<float> _itemWeightMultiplier = null; private static ConfigEntry<UpgradeMaterialScalingMode> _upgradeMaterialScaling = null; private static ConfigEntry<Toggle> _showPieceComfortInHammer = null; private static ConfigEntry<Toggle> _highlightStationExtensionsInHammer = null; private static ConfigEntry<Toggle> _ignoreStationExtensionSpacing = null; private static ConfigEntry<int> _maxStoredFireplaceFuel = null; internal static bool IsSourceOfTruth => ConfigSync.IsSourceOfTruth; internal static bool UsesLocalAuthorityFiles { get { if (IsSourceOfTruth) { return !IsRemoteServerClient; } return false; } } internal static bool IsRemoteServerClient { get { try { return ZNet.HasServerHost() && ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()); } catch { return false; } } } internal static bool ItemOverridesEnabled => _enableItemOverrides.Value.IsOn(); internal static bool RecipeOverridesEnabled => _enableRecipeOverrides.Value.IsOn(); internal static bool StatusEffectOverridesEnabled => _enableStatusEffectOverrides.Value.IsOn(); internal static bool PieceOverridesEnabled => _enablePieceOverrides.Value.IsOn(); internal static int StackableStackMultiplier => Math.Min(10, Math.Max(1, _stackableStackMultiplier.Value)); internal static float ItemWeightMultiplier => Math.Min(2f, Math.Max(0f, _itemWeightMultiplier.Value)); internal static UpgradeMaterialScalingMode UpgradeMaterialScaling => _upgradeMaterialScaling.Value; internal static bool ShowPieceComfortInHammer => _showPieceComfortInHammer.Value.IsOn(); internal static bool HighlightStationExtensionsInHammer => _highlightStationExtensionsInHammer.Value.IsOn(); internal static bool IgnoreStationExtensionSpacing => _ignoreStationExtensionSpacing.Value.IsOn(); internal static int MaxStoredFireplaceFuel => Math.Min(9999, Math.Max(0, _maxStoredFireplaceFuel.Value)); public void Awake() { //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Expected O, but got Unknown bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; _serverConfigLocked = ConfigEntry("1 - General", "Lock Configuration", Toggle.On, "If on, the configuration is locked and can be changed by server admins only.", synchronizedSetting: true, 1000); ConfigSync.AddLockingConfigEntry<Toggle>(_serverConfigLocked); _enableItemOverrides = ConfigEntry("1 - General", "Enable Item Overrides", Toggle.On, "If on, item YAML overrides are applied to matching ObjectDB item prefabs.", synchronizedSetting: true, 900); _enableItemOverrides.SettingChanged += delegate { ApplyConfigChange(ItemOverrideManager.ApplyCurrentConfiguration); }; _enableRecipeOverrides = ConfigEntry("1 - General", "Enable Recipe Overrides", Toggle.On, "If on, recipe YAML overrides are applied to ObjectDB recipes.", synchronizedSetting: true, 800); _enableRecipeOverrides.SettingChanged += delegate { ApplyConfigChange(RecipeOverrideManager.ApplyCurrentConfiguration); }; _enableStatusEffectOverrides = ConfigEntry("1 - General", "Enable Status Effect Overrides", Toggle.On, "If on, status effect YAML overrides are applied to ObjectDB status effects.", synchronizedSetting: true, 700); _enableStatusEffectOverrides.SettingChanged += delegate { ApplyConfigChange(StatusEffectOverrideManager.ApplyCurrentConfiguration); }; _enablePieceOverrides = ConfigEntry("1 - General", "Enable Piece Overrides", Toggle.On, "If on, piece YAML overrides are applied to matching prefabs and loaded pieces.", synchronizedSetting: true, 600); _enablePieceOverrides.SettingChanged += delegate { ApplyConfigChange(PieceOverrideManager.ApplyCurrentConfiguration); }; _stackableStackMultiplier = ConfigEntry("2 - Misc", "Stackable Stack Multiplier", 1, new ConfigDescription("Integer multiplier applied to baseline max stack size for stackable items unless maxStackSize is explicitly set in item YAML. 1 disables this feature.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10), Array.Empty<object>()), synchronizedSetting: true, 500); _stackableStackMultiplier.SettingChanged += delegate { ApplyConfigChange(ItemOverrideManager.ApplyCurrentConfiguration); }; _itemWeightMultiplier = ConfigEntry("2 - Misc", "Item Weight Multiplier", 1f, new ConfigDescription("Multiplier applied to baseline item weight for all items unless weight is explicitly set in item YAML. 1 disables this feature; 0 makes affected items weightless.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()), synchronizedSetting: true, 400); _itemWeightMultiplier.SettingChanged += delegate { ApplyConfigChange(ItemOverrideManager.ApplyCurrentConfiguration); }; _upgradeMaterialScaling = ConfigEntry("2 - Misc", "Upgrade Material Scaling", UpgradeMaterialScalingMode.Vanilla, "Controls standard per-level upgrade material costs globally. Vanilla uses amount x (target quality - 1), Flat always uses the configured upgrade amount, and Reduced uses ceil(amount x target quality / 2). Exact-quality requirements are unchanged.", synchronizedSetting: true, 350); _upgradeMaterialScaling.SettingChanged += delegate { ApplyConfigChange(RecipeOverrideManager.RefreshComputedRequirementState); }; _showPieceComfortInHammer = ConfigEntry("2 - Misc", "Show Comfort In Hammer", Toggle.On, "If on, hammer build icons show an orange comfort value badge for pieces with comfort 1 or higher.", synchronizedSetting: false, 300); _showPieceComfortInHammer.SettingChanged += delegate { PieceComfortHudBadges.RefreshVisibleHud(); }; _highlightStationExtensionsInHammer = ConfigEntry("2 - Misc", "Highlight Station Extensions In Hammer", Toggle.On, "If on, hovering a crafting station or station extension in the hammer tab highlights related station/extension pieces in pale cyan. This setting is client-side only.", synchronizedSetting: false, 250); _highlightStationExtensionsInHammer.SettingChanged += delegate { PieceComfortHudBadges.RefreshVisibleHud(); }; _ignoreStationExtensionSpacing = ConfigEntry("2 - Misc", "Ignore Station Extension Spacing", Toggle.On, "If on, station extensions ignore the vanilla spacing check against other station extensions, allowing close or overlapping extension placement. Other placement restrictions remain unchanged.", synchronizedSetting: true, 200); _maxStoredFireplaceFuel = ConfigEntry("2 - Misc", "maxStoredFuel", 100, $"Maximum stored fuel allowed in fireplaces without changing each fireplace's displayed max fuel. 0 disables this feature. Values are clamped to 0-{9999}. If this value is not greater than a fireplace's max fuel, that fireplace uses vanilla behavior.", synchronizedSetting: true, 100); _maxStoredFireplaceFuel.SettingChanged += delegate { ClampMaxStoredFireplaceFuel(); }; ClampMaxStoredFireplaceFuel(); LocalizationOverrideManager.Initialize(ConfigSync); StatusEffectOverrideManager.Initialize(ConfigSync); ItemOverrideManager.Initialize(ConfigSync); RecipeOverrideManager.Initialize(ConfigSync); PieceOverrideManager.Initialize(ConfigSync); VneiRefreshManager.Initialize(); ConfigSync.SourceOfTruthChanged += OnSourceOfTruthChanged; DataForgeConsoleCommands.Register(); Assembly executingAssembly = Assembly.GetExecutingAssembly(); _harmony.PatchAll(executingAssembly); SetupWatcher(); ((BaseUnityPlugin)this).Config.Save(); _lastConfigFileText = ReadFileTextIfExists(ConfigFileFullPath); if (saveOnConfigSet) { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; } } private void OnDestroy() { SaveWithRespectToConfigSet(); _watcher?.Dispose(); _watcher = null; _configReloadDebouncer?.Dispose(); _configReloadDebouncer = null; DataForgeRuntimeCleanup.RunOnce(); LocalizationOverrideManager.Dispose(); StatusEffectOverrideManager.Dispose(); ItemOverrideManager.Dispose(); RecipeOverrideManager.Dispose(); PieceOverrideManager.Dispose(); VneiRefreshManager.Dispose(); ConfigSync.SourceOfTruthChanged -= OnSourceOfTruthChanged; _harmony.UnpatchSelf(); } private static void OnSourceOfTruthChanged(bool isSourceOfTruth) { if (isSourceOfTruth) { EnsureSourceOfTruthFileMode(); return; } _sourceOfTruthFileModeReady = false; StatusEffectOverrideManager.SetupFileWatcher(); ItemOverrideManager.SetupFileWatcher(); RecipeOverrideManager.SetupFileWatcher(); PieceOverrideManager.SetupFileWatcher(); } internal static void EnsureSourceOfTruthFileMode() { if (UsesLocalAuthorityFiles && !_sourceOfTruthFileModeReady) { _sourceOfTruthFileModeReady = true; LocalizationOverrideManager.EnsureSourceOfTruthFileMode(); StatusEffectOverrideManager.SetupFileWatcher(); ItemOverrideManager.SetupFileWatcher(); RecipeOverrideManager.SetupFileWatcher(); PieceOverrideManager.SetupFileWatcher(); StatusEffectOverrideManager.ReloadFromDiskAndSync(); ItemOverrideManager.ReloadFromDiskAndSync(); RecipeOverrideManager.ReloadFromDiskAndSync(); PieceOverrideManager.ReloadFromDiskAndSync(); } } private void Update() { VneiPrefabCleanupGuard.TryPatchVneiIndexAll(_harmony); } private static void ClampMaxStoredFireplaceFuel() { int num = Math.Min(9999, Math.Max(0, _maxStoredFireplaceFuel.Value)); if (_maxStoredFireplaceFuel.Value != num) { _maxStoredFireplaceFuel.Value = num; } } private void SetupWatcher() { _watcher?.Dispose(); _configReloadDebouncer?.Dispose(); _configReloadDebouncer = DataForgeFileWatcher.CreateDebouncedAction(10000000L, ReloadConfigValues); _watcher = DataForgeFileWatcher.Create(Paths.ConfigPath, ConfigFileName, includeSubdirectories: false, ReadConfigValues); } private void ReadConfigValues(object sender, FileSystemEventArgs e) { _configReloadDebouncer?.Schedule(); } private void ReloadConfigValues() { lock (_reloadLock) { if (!File.Exists(ConfigFileFullPath)) { Log.LogWarning((object)"Config file does not exist. Skipping reload."); return; } try { string b = ReadFileTextIfExists(ConfigFileFullPath); if (string.Equals(_lastConfigFileText, b, StringComparison.Ordinal)) { Log.LogDebug((object)"Skipping configuration reload because the config file content did not change."); return; } Log.LogDebug((object)"Reloading configuration..."); bool itemOverridesEnabled = ItemOverridesEnabled; bool recipeOverridesEnabled = RecipeOverridesEnabled; bool statusEffectOverridesEnabled = StatusEffectOverridesEnabled; bool pieceOverridesEnabled = PieceOverridesEnabled; int stackableStackMultiplier = StackableStackMultiplier; float itemWeightMultiplier = ItemWeightMultiplier; UpgradeMaterialScalingMode upgradeMaterialScaling = UpgradeMaterialScaling; _configReloadInProgress = true; try { SaveWithRespectToConfigSet(reload: true); } finally { _configReloadInProgress = false; } _lastConfigFileText = ReadFileTextIfExists(ConfigFileFullPath); if (statusEffectOverridesEnabled != StatusEffectOverridesEnabled) { StatusEffectOverrideManager.ApplyCurrentConfiguration(); } if (itemOverridesEnabled != ItemOverridesEnabled || stackableStackMultiplier != StackableStackMultiplier || Math.Abs(itemWeightMultiplier - ItemWeightMultiplier) > 0.0001f) { ItemOverrideManager.ApplyCurrentConfiguration(); } if (recipeOverridesEnabled != RecipeOverridesEnabled) { RecipeOverrideManager.ApplyCurrentConfiguration(); } else if (upgradeMaterialScaling != UpgradeMaterialScaling) { RecipeOverrideManager.RefreshComputedRequirementState(); } if (pieceOverridesEnabled != PieceOverridesEnabled) { PieceOverrideManager.ApplyCurrentConfiguration(); } Log.LogInfo((object)"Configuration reload complete."); } catch (Exception arg) { Log.LogError((object)$"Error reloading configuration: {arg}"); } } } private void ApplyConfigChange(Action apply) { if (!_configReloadInProgress) { apply(); } } private void SaveWithRespectToConfigSet(bool reload = false) { bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; if (reload) { ((BaseUnityPlugin)this).Config.Reload(); } else { ((BaseUnityPlugin)this).Config.Save(); } if (saveOnConfigSet) { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; } } private static string? ReadFileTextIfExists(string path) { try { return File.Exists(path) ? File.ReadAllText(path) : null; } catch (IOException) { return null; } catch (UnauthorizedAccessException) { return null; } } private ConfigEntry<T> ConfigEntry<T>(string group, string name, T value, ConfigDescription description, bool synchronizedSetting = true, int? order = null) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown object[] array = description.Tags ?? Array.Empty<object>(); if (order.HasValue) { object[] array2 = new object[array.Length + 1]; Array.Copy(array, array2, array.Length); array2[array.Length] = new ConfigurationManagerAttributes { Order = order.Value }; array = array2; } ConfigDescription val = new ConfigDescription(description.Description + (synchronizedSetting ? " [Synced with Server]" : " [Not Synced with Server]"), description.AcceptableValues, array); ConfigEntry<T> val2 = ((BaseUnityPlugin)this).Config.Bind<T>(group, name, value, val); ConfigSync.AddConfigEntry<T>(val2).SynchronizedConfig = synchronizedSetting; return val2; } private ConfigEntry<T> ConfigEntry<T>(string group, string name, T value, string description, bool synchronizedSetting = true, int? order = null) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown return ConfigEntry(group, name, value, new ConfigDescription(description, (AcceptableValueBase)null, Array.Empty<object>()), synchronizedSetting, order); } } public static class ToggleExtensions { public static bool IsOn(this DataForgePlugin.Toggle value) { return value == DataForgePlugin.Toggle.On; } public static bool IsOff(this DataForgePlugin.Toggle value) { return value == DataForgePlugin.Toggle.Off; } } internal static class DataForgeConsoleCommands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__WriteFullScaffoldFiles; public static ConsoleOptionsFetcher <1>__GetFullTabOptions; } private const string WriteFullCommandName = "dataforge:full"; private static readonly List<string> FullTabOptions = new List<string> { "item", "recipe", "effect", "piece", "all" }; private static bool _registered; internal static void Register() { //IL_0028: 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_0033: Expected O, but got Unknown //IL_0056: 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_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown if (!_registered) { _registered = true; object obj = <>O.<0>__WriteFullScaffoldFiles; if (obj == null) { ConsoleEvent val = WriteFullScaffoldFiles; <>O.<0>__WriteFullScaffoldFiles = val; obj = (object)val; } object obj2 = <>O.<1>__GetFullTabOptions; if (obj2 == null) { ConsoleOptionsFetcher val2 = GetFullTabOptions; <>O.<1>__GetFullTabOptions = val2; obj2 = (object)val2; } new ConsoleCommand("dataforge:full", "Write DataForge full scaffold YAML files with explicit defaults. Usage: dataforge:full [item|recipe|effect|piece|all]", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)obj2, false, false, false); } } private static List<string> GetFullTabOptions() { return FullTabOptions; } private static void WriteFullScaffoldFiles(ConsoleEventArgs args) { if (!TryParseScope(args, out var includeItem, out var includeRecipe, out var includeEffect, out var includePiece)) { return; } if (includeItem) { if (ItemOverrideManager.TryWriteFullScaffoldConfigurationFile(out string path, out string error)) { Terminal context = args.Context; if (context != null) { context.AddString("Wrote item full scaffold to " + path); } } else { Terminal context2 = args.Context; if (context2 != null) { context2.AddString(error); } } } if (includeRecipe) { if (RecipeOverrideManager.TryWriteFullScaffoldConfigurationFile(out string path2, out string error2)) { Terminal context3 = args.Context; if (context3 != null) { context3.AddString("Wrote recipe full scaffold to " + path2); } } else { Terminal context4 = args.Context; if (context4 != null) { context4.AddString(error2); } } } if (includeEffect) { if (StatusEffectOverrideManager.TryWriteFullScaffoldConfigurationFile(out string path3, out string error3)) { Terminal context5 = args.Context; if (context5 != null) { context5.AddString("Wrote effect full scaffold to " + path3); } } else { Terminal context6 = args.Context; if (context6 != null) { context6.AddString(error3); } } } if (!includePiece) { return; } if (PieceOverrideManager.TryWriteFullScaffoldConfigurationFile(out string path4, out string error4)) { Terminal context7 = args.Context; if (context7 != null) { context7.AddString("Wrote piece full scaffold to " + path4); } } else { Terminal context8 = args.Context; if (context8 != null) { context8.AddString(error4); } } } private static bool TryParseScope(ConsoleEventArgs args, out bool includeItem, out bool includeRecipe, out bool includeEffect, out bool includePiece) { string text = ((args.Length >= 2) ? (args[1] ?? "").Trim().ToLowerInvariant() : "all"); if (text.Length == 0) { text = "all"; } switch (text) { case "all": includeItem = true; includeRecipe = true; includeEffect = true; includePiece = true; return true; case "items": case "item": includeItem = true; includeRecipe = false; includeEffect = false; includePiece = false; return true; case "recipe": case "recipes": includeItem = false; includeRecipe = true; includeEffect = false; includePiece = false; return true; case "effect": case "effects": includeItem = false; includeRecipe = false; includeEffect = true; includePiece = false; return true; case "piece": case "pieces": includeItem = false; includeRecipe = false; includeEffect = false; includePiece = true; return true; default: { includeItem = false; includeRecipe = false; includeEffect = false; includePiece = false; Terminal context = args.Context; if (context != null) { context.AddString("Syntax: dataforge:full [item|recipe|effect|piece|all]"); } return false; } } } } internal static class DataForgeLogContext { private sealed class PopWhenDisposed : IDisposable { private readonly string? previous; private bool disposed; internal PopWhenDisposed(string? previous) { this.previous = previous; } public void Dispose() { if (!disposed) { CurrentContext = previous; disposed = true; } } } [ThreadStatic] private static string? CurrentContext; internal static IDisposable Push(string? context) { string currentContext = CurrentContext; CurrentContext = (string.IsNullOrWhiteSpace(context) ? currentContext : context); return new PopWhenDisposed(currentContext); } internal static string FormatSource(string source, int entryIndex) { return FormatSource(source, entryIndex, null); } internal static string FormatSource(string source, int entryIndex, long? lineNumber) { string displaySource = GetDisplaySource(source); if (!lineNumber.HasValue || lineNumber.GetValueOrDefault() <= 0 || !IsLocalAuthorityFile(source)) { return $"{displaySource}#{entryIndex}"; } return $"{displaySource}:{lineNumber.Value} (#{entryIndex})"; } internal static string FormatSourceLine(string source, long lineNumber) { string displaySource = GetDisplaySource(source); if (lineNumber <= 0 || !IsLocalAuthorityFile(source)) { return displaySource; } return $"{displaySource}:{lineNumber}"; } internal static IReadOnlyList<long> GetLocalTopLevelEntryLines(string yaml, string source) { if (!IsLocalAuthorityFile(source) || string.IsNullOrWhiteSpace(yaml)) { return Array.Empty<long>(); } YamlStream yamlStream = new YamlStream(); using StringReader input = new StringReader(yaml); yamlStream.Load(input); if (yamlStream.Documents.Count == 0 || !(yamlStream.Documents[0].RootNode is YamlSequenceNode yamlSequenceNode)) { return Array.Empty<long>(); } List<long> list = new List<long>(yamlSequenceNode.Children.Count); foreach (YamlNode child in yamlSequenceNode.Children) { list.Add(child.Start.Line); } return list; } internal static long? GetEntryLine(IReadOnlyList<long> lines, int entryIndex) { int num = entryIndex - 1; if (num < 0 || num >= lines.Count) { return null; } return lines[num]; } internal static void Warning(string message) { DataForgePlugin.Log.LogWarning((object)WithContext(message)); } private static string WithContext(string message) { if (!string.IsNullOrWhiteSpace(CurrentContext)) { return CurrentContext + ": " + message; } return message; } private static bool IsLocalAuthorityFile(string source) { if (DataForgePlugin.UsesLocalAuthorityFiles && !string.IsNullOrWhiteSpace(source)) { return File.Exists(source); } return false; } private static string GetDisplaySource(string source) { string text = source?.Trim() ?? ""; string text2 = ((text.Length == 0) ? "unknown source" : Path.GetFileName(text)); if (text2.Length != 0) { return text2; } return text; } } internal static class DataForgeReferenceSections { private sealed class GroupedEntry<TSource> { public TSource Entry { get; set; } public string SortKey { get; set; } = ""; public string OwnerName { get; set; } = "Unknown / Untracked"; } internal const string VanillaOwnerName = "Valheim"; internal const string UnknownOwnerName = "Unknown / Untracked"; internal static string SerializeReferenceSections<TSource, TOutput>(IEnumerable<TSource> entries, Func<TSource, string> getSortKey, Func<TSource, string> getOwnerName, Func<TSource, TOutput> getOutput, ISerializer serializer) { List<IGrouping<string, GroupedEntry<TSource>>> list = (from entry in entries.Select(delegate(TSource entry) { string text = (getOwnerName(entry) ?? "").Trim(); return new GroupedEntry<TSource> { Entry = entry, SortKey = (getSortKey(entry) ?? "").Trim(), OwnerName = ((text.Length > 0) ? text : "Unknown / Untracked") }; }) orderby GetOwnerSortBucket(entry.OwnerName) select entry).ThenBy<GroupedEntry<TSource>, string>((GroupedEntry<TSource> entry) => entry.OwnerName, StringComparer.OrdinalIgnoreCase).ThenBy<GroupedEntry<TSource>, string>((GroupedEntry<TSource> entry) => entry.SortKey, StringComparer.OrdinalIgnoreCase).GroupBy<GroupedEntry<TSource>, string>((GroupedEntry<TSource> entry) => entry.OwnerName, StringComparer.OrdinalIgnoreCase) .ToList(); StringBuilder stringBuilder = new StringBuilder(); bool flag = false; foreach (IGrouping<string, GroupedEntry<TSource>> item in list) { if (flag) { stringBuilder.AppendLine(); } AppendSectionHeaderComment(stringBuilder, item.Key); foreach (GroupedEntry<TSource> item2 in item) { string value = CollapseScalarBlockListsToInlineLists(serializer.Serialize(new TOutput[1] { getOutput(item2.Entry) }).TrimEnd('\r', '\n')); stringBuilder.AppendLine(value); } flag = true; } if (!flag) { return "[]" + Environment.NewLine; } return stringBuilder.ToString(); } private static void AppendSectionHeaderComment(StringBuilder builder, string ownerName) { builder.Append("# ===== "); builder.Append(string.IsNullOrWhiteSpace(ownerName) ? "Unknown / Untracked" : ownerName.Trim()); builder.AppendLine(" ====="); } private static int GetOwnerSortBucket(string ownerName) { if (string.Equals(ownerName, "Valheim", StringComparison.OrdinalIgnoreCase)) { return 0; } if (!string.Equals(ownerName, "Unknown / Untracked", StringComparison.OrdinalIgnoreCase)) { return 1; } return 2; } private static string CollapseScalarBlockListsToInlineLists(string yaml) { if (string.IsNullOrWhiteSpace(yaml) || yaml.IndexOf("- ", StringComparison.Ordinal) < 0) { return yaml; } string[] array = yaml.Replace("\r\n", "\n").Split(new char[1] { '\n' }); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < array.Length; i++) { if (TryCollapseScalarBlockList(array, ref i, out string collapsedLine)) { stringBuilder.AppendLine(collapsedLine); } else { stringBuilder.AppendLine(array[i]); } } return stringBuilder.ToString().TrimEnd('\r', '\n'); } private static bool TryCollapseScalarBlockList(string[] lines, ref int index, out string collapsedLine) { collapsedLine = ""; string text = lines[index]; int num = text.IndexOf(':'); if (num < 0 || num != text.Length - 1) { return false; } int num2 = index + 1; if (num2 >= lines.Length) { return false; } int firstNonWhitespaceIndex = GetFirstNonWhitespaceIndex(text); int firstNonWhitespaceIndex2 = GetFirstNonWhitespaceIndex(lines[num2]); if (firstNonWhitespaceIndex < 0 || firstNonWhitespaceIndex2 <= firstNonWhitespaceIndex || !lines[num2].TrimStart(Array.Empty<char>()).StartsWith("- ", StringComparison.Ordinal)) { return false; } List<string> list = new List<string>(); int i; for (i = num2; i < lines.Length; i++) { string text2 = lines[i]; if (GetFirstNonWhitespaceIndex(text2) != firstNonWhitespaceIndex2 || !text2.TrimStart(Array.Empty<char>()).StartsWith("- ", StringComparison.Ordinal)) { break; } string text3 = text2.TrimStart(Array.Empty<char>()).Substring(2).Trim(); if (text3.Length == 0 || Enumerable.Contains(text3, ':') || Enumerable.Contains(text3, ',')) { return false; } list.Add(text3); } if (list.Count == 0) { return false; } collapsedLine = text + " [" + string.Join(", ", list) + "]"; index = i - 1; return true; } private static int GetFirstNonWhitespaceIndex(string line) { for (int i = 0; i < line.Length; i++) { if (!char.IsWhiteSpace(line[i])) { return i; } } return -1; } } internal static class DataForgeOwnerResolver { internal static string GetPrefabOwnerName(string? prefabName) { string text = NormalizeName(prefabName); if (text.Length == 0) { return "Unknown / Untracked"; } foreach (string item in EnumerateLookupCandidates(text)) { if (DataForgeVanillaAssetCatalog.IsVanillaPrefab(item)) { return "Valheim"; } } return DataForgeAssetOwnerCatalog.GetOwnerName(text); } internal static string GetAssetOwnerName(string? assetName) { string text = NormalizeName(assetName); if (text.Length == 0) { return "Unknown / Untracked"; } foreach (string item in EnumerateLookupCandidates(text)) { if (DataForgeVanillaAssetCatalog.IsVanillaAsset(item)) { return "Valheim"; } } return DataForgeAssetOwnerCatalog.GetOwnerName(text); } private static IEnumerable<string> EnumerateLookupCandidates(string normalizedName) { HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); AddIfNew(normalizedName); int num = normalizedName.IndexOf(':'); if (num > 0) { AddIfNew(normalizedName.Substring(0, num)); } foreach (string item in seen) { yield return item; } void AddIfNew(string candidate) { string text = NormalizeName(candidate); if (text.Length > 0) { seen.Add(text); } } } private static string NormalizeName(string? name) { return (name ?? "").Replace("(Clone)", "").Trim(); } } internal static class DataForgeVanillaAssetCatalog { private enum CatalogState { Uninitialized, Loaded, Unavailable } private static readonly object Sync = new object(); private static readonly HashSet<string> PrefabNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<string> AssetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static CatalogState _state; internal static bool IsVanillaPrefab(string prefabName) { EnsureLoaded(); if (_state == CatalogState.Loaded && !string.IsNullOrWhiteSpace(prefabName)) { return PrefabNames.Contains(prefabName); } return false; } internal static bool IsVanillaAsset(string assetName) { EnsureLoaded(); if (_state == CatalogState.Loaded && !string.IsNullOrWhiteSpace(assetName)) { return AssetNames.Contains(assetName); } return false; } private static void EnsureLoaded() { if (_state != CatalogState.Uninitialized) { return; } lock (Sync) { if (_state != CatalogState.Uninitialized) { return; } string text = Path.Combine(Application.dataPath, "StreamingAssets", "SoftRef", "manifest_extended"); if (!File.Exists(text)) { _state = CatalogState.Unavailable; DataForgePlugin.Log.LogWarning((object)("Vanilla asset manifest was not found at '" + text + "'. Reference owner sections may place unmapped entries under 'Unknown / Untracked'.")); return; } foreach (string item in File.ReadLines(text)) { int num = item.IndexOf("path in bundle:", StringComparison.OrdinalIgnoreCase); if (num < 0) { continue; } string text2 = item.Substring(num + "path in bundle:".Length).Trim(); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text2); if (!string.IsNullOrWhiteSpace(fileNameWithoutExtension)) { if (text2.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { PrefabNames.Add(fileNameWithoutExtension); } else if (text2.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) { AssetNames.Add(fileNameWithoutExtension); } } } _state = CatalogState.Loaded; DataForgePlugin.Log.LogDebug((object)$"Loaded {PrefabNames.Count} vanilla prefab names and {AssetNames.Count} vanilla asset names from '{text}'."); } } } internal static class DataForgeAssetOwnerCatalog { private sealed class PluginResourceSnapshot { public string OwnerName { get; set; } = ""; public string PluginName { get; set; } = ""; public string PluginGuid { get; set; } = ""; public string AssemblyName { get; set; } = ""; public string[] ResourceNames { get; set; } = Array.Empty<string>(); } private static readonly object Sync = new object(); private static readonly Dictionary<string, string> AssetOwners = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private static string _loadedSignature = ""; internal static string GetOwnerName(string assetName) { EnsureMappingsLoaded(); foreach (string item in EnumerateLookupCandidates(assetName)) { if (AssetOwners.TryGetValue(item, out string value) && !string.IsNullOrWhiteSpace(value)) { return value; } } return "Unknown / Untracked"; } private static void EnsureMappingsLoaded() { string text = BuildSignature(); if (string.Equals(text, _loadedSignature, StringComparison.Ordinal)) { return; } lock (Sync) { if (string.Equals(text, _loadedSignature, StringComparison.Ordinal)) { return; } AssetOwners.Clear(); List<PluginResourceSnapshot> pluginResources = GetPluginResources(); foreach (AssetBundle allLoadedAssetBundle in AssetBundle.GetAllLoadedAssetBundles()) { string text2 = ((Object)allLoadedAssetBundle).name ?? ""; if (text2.Length == 0) { continue; } string value = ResolveOwnerName(text2, pluginResources); if (string.IsNullOrWhiteSpace(value)) { continue; } string[] allAssetNames = allLoadedAssetBundle.GetAllAssetNames(); foreach (string text3 in allAssetNames) { if (text3.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) || text3.EndsWith(".asset", StringComparison.OrdinalIgnoreCase)) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text3); if (!string.IsNullOrWhiteSpace(fileNameWithoutExtension)) { AssetOwners[fileNameWithoutExtension] = value; } } } } _loadedSignature = text; DataForgePlugin.Log.LogDebug((object)$"Tracked {AssetOwners.Count} mod asset owner mapping(s) for reference sections."); } } private static IEnumerable<string> EnumerateLookupCandidates(string assetName) { string normalizedName = (assetName ?? "").Replace("(Clone)", "").Trim(); if (normalizedName.Length != 0) { yield return normalizedName; int num = normalizedName.IndexOf(':'); if (num > 0) { yield return normalizedName.Substring(0, num); } } } private static List<PluginResourceSnapshot> GetPluginResources() { return (from plugin in Chainloader.PluginInfos.Values.Select(delegate(PluginInfo pluginInfo) { string text = (pluginInfo.Metadata.Name ?? "").Trim(); string text2 = (pluginInfo.Metadata.GUID ?? "").Trim(); string assemblyName = ""; string[] resourceNames = Array.Empty<string>(); try { assemblyName = ((object)pluginInfo.Instance)?.GetType().Assembly.GetName().Name ?? ""; resourceNames = ((object)pluginInfo.Instance)?.GetType().Assembly.GetManifestResourceNames() ?? Array.Empty<string>(); } catch { } return new PluginResourceSnapshot { OwnerName = ((text.Length > 0) ? text : text2), PluginName = text, PluginGuid = text2, AssemblyName = assemblyName, ResourceNames = resourceNames }; }) where plugin.OwnerName.Length > 0 select plugin).ToList(); } private static string ResolveOwnerName(string bundleName, List<PluginResourceSnapshot> plugins) { PluginResourceSnapshot pluginResourceSnapshot = plugins.FirstOrDefault((PluginResourceSnapshot plugin) => plugin.ResourceNames.Any((string resourceName) => resourceName.EndsWith(bundleName, StringComparison.OrdinalIgnoreCase))); if (pluginResourceSnapshot != null) { return pluginResourceSnapshot.OwnerName; } string normalizedBundleName = NormalizeToken(Path.GetFileNameWithoutExtension(bundleName)); if (normalizedBundleName.Length == 0) { return ""; } return plugins.FirstOrDefault(delegate(PluginResourceSnapshot plugin) { string pluginToken = NormalizeToken(plugin.PluginName); string pluginToken2 = NormalizeToken(plugin.PluginGuid); string pluginToken3 = NormalizeToken(plugin.AssemblyName); return IsTokenMatch(normalizedBundleName, pluginToken) || IsTokenMatch(normalizedBundleName, pluginToken2) || IsTokenMatch(normalizedBundleName, pluginToken3); })?.OwnerName ?? ""; } private static bool IsTokenMatch(string bundleName, string pluginToken) { if (pluginToken.Length > 0) { if (bundleName.IndexOf(pluginToken, StringComparison.OrdinalIgnoreCase) < 0) { return pluginToken.IndexOf(bundleName, StringComparison.OrdinalIgnoreCase) >= 0; } return true; } return false; } private static string NormalizeToken(string value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } StringBuilder stringBuilder = new StringBuilder(); foreach (char c in value) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } private static string BuildSignature() { IEnumerable<string> values = (from bundle in AssetBundle.GetAllLoadedAssetBundles() select ((Object)bundle).name ?? "" into name where name.Length > 0 select name).OrderBy<string, string>((string name) => name, StringComparer.OrdinalIgnoreCase); IEnumerable<string> values2 = Chainloader.PluginInfos.Values.Select(delegate(PluginInfo pluginInfo) { string text = pluginInfo.Metadata.Name ?? ""; string text2 = pluginInfo.Metadata.GUID ?? ""; string text3 = ""; try { text3 = ((object)pluginInfo.Instance)?.GetType().Assembly.GetName().Name ?? ""; } catch { } return text2 + ":" + text + ":" + text3; }).OrderBy<string, string>((string token) => token, StringComparer.OrdinalIgnoreCase); return string.Join("|", values) + "||" + string.Join("|", values2); } } internal readonly struct DataForgeItemSortGroup { internal int BigGroupRank { get; } internal int SubGroupRank { get; } internal int DetailRank { get; } internal DataForgeItemSortGroup(int bigGroupRank, int subGroupRank, int detailRank) { BigGroupRank = bigGroupRank; SubGroupRank = subGroupRank; DetailRank = detailRank; } } internal static class DataForgeItemSortClassifier { private const int Melee = 0; private const int Ranged = 1; private const int Magic = 2; private const int Equipment = 3; private const int Food = 4; private const int Consumable = 5; private const int Meadbase = 6; private const int Misc = 7; private const int Unknown = 8; internal static DataForgeItemSortGroup Classify(string? itemName, SharedData? shared) { //IL_0223: Unknown result type (might be due to invalid IL or missing references) //IL_022a: Invalid comparison between Unknown and I4 if (shared == null) { return ClassifyFallback(itemName); } if (MatchElementalMagic(shared)) { return Group(2, 0); } if (MatchBloodMagic(shared)) { return Group(2, 1); } if (MatchBow(itemName, shared)) { return Group(1, 0); } if (MatchArrow(itemName, shared)) { return Group(1, 1); } if (MatchCrossbow(itemName, shared)) { return Group(1, 2); } if (MatchBolt(itemName, shared)) { return Group(1, 3); } if (MatchAmmo(shared)) { return Group(1, 4); } if (MatchBomb(shared)) { return Group(1, 5); } if (MatchSword(itemName, shared)) { return Group(0, 0, SwordDetail(itemName, shared)); } if (MatchAxe(itemName, shared)) { return Group(0, 1, AxeDetail(itemName, shared)); } if (MatchClub(itemName, shared)) { return Group(0, 2, ClubDetail(itemName, shared)); } if (MatchKnife(itemName, shared)) { return Group(0, 3); } if (MatchSpear(itemName, shared)) { return Group(0, 4); } if (MatchPolearm(itemName, shared)) { return Group(0, 5); } if (MatchFists(itemName, shared)) { return Group(0, 6); } if (MatchShield(itemName, shared)) { return Group(0, 7, ShieldDetail(itemName)); } if (MatchPickaxe(itemName, shared)) { return Group(0, 8); } if (MatchTool(itemName, shared)) { return Group(0, 9); } if (MatchHelmet(shared)) { return Group(3, 0); } if (MatchChest(shared)) { return Group(3, 1); } if (MatchLegs(shared)) { return Group(3, 2); } if (MatchCape(shared)) { return Group(3, 3); } if (MatchUtility(shared)) { return Group(3, 4); } if (MatchTrinket(shared)) { return Group(3, 5); } if (MatchFeast(itemName)) { return Group(4, 3); } if (MatchNativeFood(shared, out var subGroupRank)) { return Group(4, subGroupRank); } if (MatchMeadbase(itemName, shared)) { return Group(6, 0); } if (MatchMead(itemName, shared)) { return Group(5, 0); } if (MatchPotion(itemName, shared)) { return Group(5, 1); } if ((int)shared.m_itemType == 13) { return Group(7, 0); } if (shared.m_value > 0) { return Group(7, 1); } return ClassifyFallback(itemName); } private static DataForgeItemSortGroup ClassifyFallback(string? itemName) { if (HasToken(itemName, "staff", "magic", "elemental")) { return Group(2, 0); } if (HasToken(itemName, "bloodmagic")) { return Group(2, 1); } if (HasToken(itemName, "bow") && !HasToken(itemName, "crossbow")) { return Group(1, 0); } if (HasToken(itemName, "arrow")) { return Group(1, 1); } if (HasToken(itemName, "crossbow", "arbalest")) { return Group(1, 2); } if (HasToken(itemName, "bolt")) { return Group(1, 3); } if (HasToken(itemName, "bomb")) { return Group(1, 5); } if (HasToken(itemName, "sword")) { return Group(0, 0, SwordDetail(itemName, null)); } if (HasToken(itemName, "axe", "battleaxe")) { return Group(0, 1, AxeDetail(itemName, null)); } if (HasToken(itemName, "club", "mace", "sledge")) { return Group(0, 2, ClubDetail(itemName, null)); } if (HasToken(itemName, "knife")) { return Group(0, 3); } if (HasToken(itemName, "spear")) { return Group(0, 4); } if (HasToken(itemName, "atgeir", "polearm")) { return Group(0, 5); } if (HasToken(itemName, "fist")) { return Group(0, 6); } if (HasToken(itemName, "shield", "buckler")) { return Group(0, 7, ShieldDetail(itemName)); } if (HasToken(itemName, "pickaxe")) { return Group(0, 8); } if (HasToken(itemName, "hammer", "hoe", "cultivator", "torch", "fishingrod", "tankard")) { return Group(0, 9); } if (HasToken(itemName, "helmet", "hood", "helm")) { return Group(3, 0); } if (HasToken(itemName, "chest", "cuirass", "tunic", "robe")) { return Group(3, 1); } if (HasToken(itemName, "legs", "leg", "pants", "greaves")) { return Group(3, 2); } if (HasToken(itemName, "cape", "shoulder")) { return Group(3, 3); } if (HasToken(itemName, "trinket")) { return Group(3, 5); } if (HasToken(itemName, "feast")) { return Group(4, 3); } if (HasToken(itemName, "meadbase")) { return Group(6, 0); } if (HasToken(itemName, "mead")) { return Group(5, 0); } if (HasToken(itemName, "potion")) { return Group(5, 1); } if (HasToken(itemName, "trophy")) { return Group(7, 0); } return Group(8, 0); } private static bool MatchSword(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)1)) { return HasToken(itemName, shared, "sword"); } return true; } private static bool MatchAxe(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)7)) { return HasToken(itemName, shared, "axe", "battleaxe"); } return true; } private static bool MatchClub(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)3)) { return HasToken(itemName, shared, "club", "mace", "sledge"); } return true; } private static bool MatchKnife(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)2)) { return HasToken(itemName, shared, "knife"); } return true; } private static bool MatchSpear(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)5)) { return HasToken(itemName, shared, "spear"); } return true; } private static bool MatchPolearm(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)4) && !IsItemTypeOrAttach(shared, (ItemType)20)) { return HasToken(itemName, shared, "atgeir", "polearm"); } return true; } private static bool MatchFists(string? itemName, SharedData shared) { if (!IsSkillAttack(shared, (SkillType)11)) { return HasToken(itemName, shared, "fist"); } return true; } private static bool MatchShield(string? itemName, SharedData shared) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 if (!IsItemTypeOrAttach(shared, (ItemType)5) && (int)shared.m_skillType != 6) { return HasToken(itemName, shared, "shield", "buckler"); } return true; } private static bool MatchPickaxe(string? itemName, SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 if ((int)shared.m_skillType != 12) { return HasToken(itemName, shared, "pickaxe"); } return true; } private static bool MatchTool(string? itemName, SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if ((int)shared.m_itemType != 19 && !IsItemTypeOrAttach(shared, (ItemType)15) && (int)shared.m_skillType != 104 && (int)shared.m_skillType != 106) { return HasToken(itemName, shared, "hammer", "hoe", "cultivator", "torch", "fishingrod", "tankard"); } return true; } private static bool MatchBow(string? itemName, SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Invalid comparison between Unknown and I4 if ((int)shared.m_skillType != 14 && (int)shared.m_skillType != 9 && (int)shared.m_skillType != 10) { if ((int)shared.m_itemType != 4 && !IsSkillAttack(shared, (SkillType)8)) { if (HasToken(itemName, shared, "bow")) { return !HasToken(itemName, shared, "crossbow"); } return false; } return true; } return false; } private static bool MatchCrossbow(string? itemName, SharedData shared) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Invalid comparison between Unknown and I4 if (!MatchAmmo(shared) && ((int)shared.m_skillType == 14 || HasToken(itemName, shared, "crossbow", "arbalest"))) { if ((int)shared.m_itemType != 4 && !HasAttackAnimation(shared)) { return HasToken(itemName, shared, "crossbow", "arbalest"); } return true; } return false; } private static bool MatchArrow(string? itemName, SharedData shared) { if (!HasAttackAnimation(shared) && (string.Equals(shared.m_ammoType, "$ammo_arrows", StringComparison.Ordinal) || HasToken(itemName, shared, "arrow"))) { return GetTotalDamage(shared) > 0f; } return false; } private static bool MatchBolt(string? itemName, SharedData shared) { if (!HasAttackAnimation(shared) && (string.Equals(shared.m_ammoType, "$ammo_bolts", StringComparison.Ordinal) || HasToken(itemName, shared, "bolt"))) { return GetTotalDamage(shared) > 0f; } return false; } private static bool MatchAmmo(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if ((int)shared.m_itemType != 9) { return (int)shared.m_itemType == 23; } return true; } private static bool MatchBomb(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_000a: 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_0024: Invalid comparison between Unknown and I4 if ((int)shared.m_itemType == 3 && (int)shared.m_animationState == 0) { Attack attack = shared.m_attack; if (attack != null && (int)attack.m_attackType == 2) { return string.Equals(shared.m_attack?.m_attackAnimation, "throw_bomb", StringComparison.Ordinal); } } return false; } private static bool MatchElementalMagic(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 return (int)shared.m_skillType == 9; } private static bool MatchBloodMagic(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 return (int)shared.m_skillType == 10; } private static bool MatchHelmet(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)6); } private static bool MatchChest(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)7); } private static bool MatchLegs(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)11); } private static bool MatchCape(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)17); } private static bool MatchUtility(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)18); } private static bool MatchTrinket(SharedData shared) { return IsItemTypeOrAttach(shared, (ItemType)24); } private static bool MatchNativeFood(SharedData shared, out int subGroupRank) { SharedData foodSharedData = GetFoodSharedData(shared); subGroupRank = 0; if (!HasFoodCarrier(shared) || (foodSharedData.m_food <= 0f && foodSharedData.m_foodStamina <= 0f && foodSharedData.m_foodEitr <= 0f)) { return false; } if (foodSharedData.m_food >= foodSharedData.m_foodStamina && foodSharedData.m_food >= foodSharedData.m_foodEitr) { subGroupRank = 0; return true; } if (foodSharedData.m_foodStamina >= foodSharedData.m_food && foodSharedData.m_foodStamina >= foodSharedData.m_foodEitr) { subGroupRank = 1; return true; } subGroupRank = 2; return true; } private static bool HasFoodCarrier(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 if ((int)shared.m_itemType != 2) { if ((int)shared.m_itemType == 1) { return (Object)(object)shared.m_appendToolTip != (Object)null; } return false; } return true; } private static SharedData GetFoodSharedData(SharedData shared) { return shared.m_appendToolTip?.m_itemData?.m_shared ?? shared; } private static bool MatchFeast(string? itemName) { if (HasToken(itemName, "feast")) { return HasToken(itemName, "material"); } return false; } private static bool MatchMeadbase(string? itemName, SharedData shared) { return HasToken(itemName, shared, "meadbase"); } private static bool MatchMead(string? itemName, SharedData shared) { if (!MatchMeadbase(itemName, shared)) { return HasToken(itemName, shared, "mead"); } return false; } private static bool MatchPotion(string? itemName, SharedData shared) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Invalid comparison between Unknown and I4 //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Invalid comparison between Unknown and I4 if (!MatchMead(itemName, shared) && !MatchMeadbase(itemName, shared) && !MatchNativeFood(shared, out var _)) { if (!HasToken(itemName, shared, "potion")) { if ((int)shared.m_itemType == 1 || (int)shared.m_itemType == 2) { return (Object)(object)shared.m_consumeStatusEffect != (Object)null; } return false; } return true; } return false; } private static int SwordDetail(string? itemName, SharedData? shared) { if (IsTwoHanded(shared) || HasToken(itemName, "greatsword", "twohandedsword", "2hsword")) { return 1; } return 0; } private static int AxeDetail(string? itemName, SharedData? shared) { if (IsBattleaxe(itemName, shared)) { return 2; } if (!IsTwoHanded(shared) && !HasToken(itemName, "twohandedaxe", "2haxe", "dualaxe")) { return 0; } return 1; } private static bool IsBattleaxe(string? itemName, SharedData? shared) { if (HasToken(itemName, "dualaxe")) { return false; } if (HasToken(itemName, "battleaxe")) { return true; } string value = shared?.m_attack?.m_attackAnimation ?? ""; string text = shared?.m_secondaryAttack?.m_attackAnimation ?? ""; if (IsTwoHanded(shared) && string.Equals(text, "battleaxe_secondary", StringComparison.OrdinalIgnoreCase) && !ContainsIgnoreCase(value, "dualaxe")) { if (!ContainsIgnoreCase(value, "battleaxe")) { return ContainsIgnoreCase(text, "battleaxe"); } return true; } return false; } private static int ClubDetail(string? itemName, SharedData? shared) { if (!IsTwoHanded(shared) && !HasToken(itemName, "sledge", "twohandedclub", "2hclub")) { return 0; } return 1; } private static bool IsTwoHanded(SharedData? shared) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Invalid comparison between Unknown and I4 //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if (shared == null || (int)shared.m_itemType != 14) { if (shared == null) { return false; } return (int)shared.m_itemType == 22; } return true; } private static int ShieldDetail(string? itemName) { if (HasToken(itemName, "buckler")) { return 0; } if (!HasToken(itemName, "tower")) { return 1; } return 2; } private static bool IsSkillAttack(SharedData shared, SkillType skillType) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (shared.m_skillType == skillType && HasAttackAnimation(shared)) { return GetTotalDamage(shared) > 0f; } return false; } private static bool HasAttackAnimation(SharedData shared) { return !string.IsNullOrEmpty(shared.m_attack?.m_attackAnimation); } private static float GetTotalDamage(SharedData shared) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0030: 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_0045: Unknown result type (might be due to invalid IL or missing references) DamageTypes damages = shared.m_damages; return damages.m_blunt + damages.m_slash + damages.m_pierce + damages.m_chop + damages.m_pickaxe + damages.m_fire + damages.m_frost + damages.m_lightning + damages.m_poison + damages.m_spirit; } private static bool IsItemTypeOrAttach(SharedData shared, ItemType itemType) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) if (shared.m_itemType != itemType) { return shared.m_attachOverride == itemType; } return true; } private static bool HasToken(string? itemName, SharedData? shared, params string[] tokens) { string[] array = new string[3] { itemName, shared?.m_name, StripLocalizationToken(shared?.m_name) }; for (int i = 0; i < array.Length; i++) { if (HasToken(array[i], tokens)) { return true; } } return false; } private static bool HasToken(string? value, params string[] tokens) { string text = DataForgeResourceMap.NormalizeResourceToken(value); if (text.Length == 0) { return false; } for (int i = 0; i < tokens.Length; i++) { string text2 = DataForgeResourceMap.NormalizeResourceToken(tokens[i]); if (text2.Length > 0 && text.IndexOf(text2, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } private static bool ContainsIgnoreCase(string value, string token) { return value.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0; } private static string StripLocalizationToken(string? value) { if (string.IsNullOrWhiteSpace(value)) { return ""; } string text = value.Trim(); if (!text.StartsWith("$item_", StringComparison.OrdinalIgnoreCase)) { return text.TrimStart(new char[1] { '$' }); } return text.Substring("$item_".Length); } private static DataForgeItemSortGroup Group(int bigGroupRank, int subGroupRank, int detailRank = 0) { return new DataForgeItemSortGroup(bigGroupRank, subGroupRank, detailRank); } } internal static class DataForgeResourceMap { private readonly struct RecipeLookupEntry { internal Recipe Recipe { get; } internal int? Tier { get; } internal RecipeLookupEntry(Recipe recipe, int? tier) { Recipe = recipe; Tier = tier; } } private const string ResourceMapFileName = "z_resourcemap.txt"; private const int UnknownTierSortValue = 999999; private static readonly object Sync = new object(); private static Dictionary<string, int> ResourceTierByToken = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, ItemDrop?> ItemDropCache = new Dictionary<string, ItemDrop>(StringComparer.OrdinalIgnoreCase); private static Dictionary<string, RecipeLookupEntry>? RecipeOutputLookup; private static readonly Dictionary<string, DataForgeItemSortGroup> ItemSortGroupCache = new Dictionary<string, DataForgeItemSortGroup>(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, int> ItemTierSortValueCache = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private static DateTime LoadedWriteTimeUtc = DateTime.MinValue; private static bool Loaded; private static ObjectDB? CachedObjectDb; private static int CachedObjectDbItemCount = -1; private static int CachedObjectDbRecipeCount = -1; private static string ConfigDirectory => Path.Combine(Paths.ConfigPath, "DataForge"); private static string ResourceMapPath => Path.Combine(ConfigDirectory, "z_resourcemap.txt"); internal static string BuildSortKey(int groupRank, int tierSortValue, string name) { return string.Join("|", Math.Max(0, groupRank).ToString("D3", CultureInfo.InvariantCulture), Math.Max(0, tierSortValue).ToString("D6", CultureInfo.InvariantCulture), name ?? ""); } internal static string BuildItemSortKey(string? itemName, int tierSortValue, string name) { DataForgeItemSortGroup itemSortGroup = GetItemSortGroup(itemName); return string.Join("|", Math.Max(0, itemSortGroup.BigGroupRank).ToString("D3", CultureInfo.InvariantCulture), Math.Max(0, itemSortGroup.SubGroupRank).ToString("D3", CultureInfo.InvariantCulture), Math.Max(0, itemSortGroup.DetailRank).ToString("D3", CultureInfo.InvariantCulture), Math.Max(0, tierSortValue).ToString("D6", CultureInfo.InvariantCulture), name ?? ""); } private static DataForgeItemSortGroup GetItemSortGroup(string? itemName) { EnsureObjectDbCacheFresh(); string text = CleanPrefabName(itemName); if (text.Length > 0 && ItemSortGroupCache.TryGetValue(text, out var value)) { return value; } DataForgeItemSortGroup dataForgeItemSortGroup = DataForgeItemSortClassifier.Classify(itemName, ResolveItemDrop(itemName)?.m_itemData?.m_shared); if (text.Length > 0) { ItemSortGroupCache[text] = dataForgeItemSortGroup; } return dataForgeItemSortGroup; } internal static int GetItemTierSortValue(string? itemName) { EnsureObjectDbCacheFresh(); string text = CleanPrefabName(itemName); if (text.Length > 0 && ItemTierSortValueCache.TryGetValue(text, out var value)) { return value; } int? knownTierForItem = GetKnownTierForItem(itemName); int? knownTierForRecipe = GetKnownTierForRecipe(ResolveRecipeForItem(itemName)); int num = SortValue(MaxTier(knownTierForItem, knownTierForRecipe)); if (text.Length > 0) { ItemTierSortValueCache[text] = num; } return num; } internal static int GetResourceTierSortValue(IEnumerable<string?> resourceNames)