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.3.5
DataForge.dll
Decompiled 2 days 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 ModAssetOwnership; 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.3.5")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.3.5.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; } } [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } } namespace ModAssetOwnership { internal sealed class AssetOwner { internal readonly string Guid; internal readonly string Name; internal readonly string AssemblyName; internal readonly string[] Resources; internal AssetOwner(string guid, string name, string assemblyName, string[] resources) { Guid = guid; Name = (string.IsNullOrWhiteSpace(name) ? guid : name); AssemblyName = assemblyName; Resources = resources; } } internal static class AssetOwnerMatching { internal static AssetOwner? Resolve(string bundleName, IReadOnlyList<AssetOwner> plugins) { if (string.IsNullOrWhiteSpace(bundleName)) { return null; } AssetOwner assetOwner = null; bool flag = false; foreach (AssetOwner plugin in plugins) { if (string.IsNullOrWhiteSpace(plugin.Guid)) { continue; } bool flag2 = false; string[] resources = plugin.Resources; foreach (string text in resources) { if (text.Equals(bundleName, StringComparison.OrdinalIgnoreCase) || text.EndsWith("." + bundleName, StringComparison.OrdinalIgnoreCase)) { flag2 = true; break; } } if (flag2) { flag = true; if (assetOwner != null && !assetOwner.Guid.Equals(plugin.Guid, StringComparison.OrdinalIgnoreCase)) { return null; } assetOwner = plugin; } } if (flag) { return assetOwner; } string text2 = Normalize(Path.GetFileNameWithoutExtension(bundleName)); if (text2.Length == 0) { return null; } foreach (AssetOwner plugin2 in plugins) { if (!string.IsNullOrWhiteSpace(plugin2.Guid) && (!(text2 != Normalize(plugin2.Name)) || !(text2 != Normalize(plugin2.Guid)) || !(text2 != Normalize(plugin2.AssemblyName)))) { if (assetOwner != null && !assetOwner.Guid.Equals(plugin2.Guid, StringComparison.OrdinalIgnoreCase)) { return null; } assetOwner = plugin2; } } return assetOwner; } internal static void Add(Dictionary<string, AssetOwner> owners, HashSet<string> ambiguous, string name, AssetOwner owner) { if (!ambiguous.Contains(name)) { if (owners.TryGetValue(name, out AssetOwner value) && !value.Guid.Equals(owner.Guid, StringComparison.OrdinalIgnoreCase)) { owners.Remove(name); ambiguous.Add(name); } else { owners[name] = owner; } } } private static string Normalize(string value) { StringBuilder stringBuilder = new StringBuilder(); string text = value ?? ""; foreach (char c in text) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } return stringBuilder.ToString(); } } } 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 static readonly FieldInfo LocalizationInstanceField = AccessTools.Field(typeof(Localization), "m_instance"); private static readonly FieldRef<Localization, Dictionary<string, string>> Translations = AccessTools.FieldRefAccess<Localization, Dictionary<string, string>>("m_translations"); private static readonly FieldRef<Localization, LRUCache<string>> TranslationCache = AccessTools.FieldRefAccess<Localization, LRUCache<string>>("m_cache"); 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 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; private static Localization LocalizationInstance() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown return (Localization)LocalizationInstanceField.GetValue(null); } 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; lock (StateLock) { ActivePayload = CreateEmptyPayload(); LastParsedPayload = null; } } internal static bool EnsureSourceOfTruthFileMode() { if (!HasLocalAuthority) { return false; } if (LocalFileModeReady) { return true; } LocalFileModeReady = true; try { SetupFileWatcher(); DataForgeFileWatcher.CancelPendingRecreate("localization"); if (!ReloadFromDiskAndSync()) { NotifyLocalizationChanged(); ReloadDebouncer?.Schedule(); } return true; } catch (Exception arg) { LocalFileModeReady = false; Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; DataForgePlugin.Log.LogError((object)$"Failed to initialize server localization files: {arg}"); return false; } } private static void SetupFileWatcher() { Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; if (HasLocalAuthority) { EnsureConfigDirectoryAndDefaultOverride(); ReloadDebouncer = DataForgeFileWatcher.CreateDebouncedAction(10000000L, ReloadYamlValues); Watcher = DataForgeFileWatcher.Create(LocalizationDirectory, "*.*", includeSubdirectories: false, ReadYamlValues, OnWatcherError); } } 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 val = LocalizationInstance(); if (val != null) { ApplyCurrentLocalization(val, val.GetSelectedLanguage()); } } internal static void ApplyCurrentLocalization(Localization localization, string? language) { if (!IsLiveLocalization(localization)) { return; } string text = NormalizeLanguage(language); bool flag = AppliedLocalization != localization || !AppliedLanguage.Equals(text, StringComparison.OrdinalIgnoreCase); Dictionary<string, string> dictionary; lock (StateLock) { dictionary = BuildTranslationsForLanguage(ActivePayload, text); } if (!flag && DataForgeApi.GetState(DataForgeDomain.Localization).IsReady && !NeedsTranslationApply(localization, dictionary)) { return; } using DataForgeApplyScope dataForgeApplyScope = DataForgeApi.BeginApply(DataForgeDomain.Localization, HasLocalAuthority, from token in AppliedTranslations.Keys.Concat(dictionary.Keys) select "$" + token, flag, text); if (flag) { RestoreAppliedTranslations(); AppliedLocalization = localization; AppliedLanguage = text; } bool flag2 = RestoreRemovedTranslations(localization, dictionary.Keys); foreach (KeyValuePair<string, string> item in dictionary) { flag2 |= ApplyTranslation(localization, item.Key, item.Value); } if (flag2 || dictionary.Count > 0) { TranslationCache.Invoke(localization).EvictAll(); } dataForgeApplyScope.Complete(dictionary.Keys.Select((string token) => "$" + token)); } private static bool NeedsTranslationApply(Localization localization, Dictionary<string, string> translations) { if (AppliedTranslations.Count != translations.Count) { return true; } foreach (KeyValuePair<string, string> translation in translations) { if (!AppliedTranslations.TryGetValue(translation.Key, out TranslationLease value) || !string.Equals(value.LastAppliedValue, translation.Value, StringComparison.Ordinal) || !Translations.Invoke(localization).TryGetValue(translation.Key, out var value2) || !string.Equals(value2, translation.Value, StringComparison.Ordinal)) { return true; } } return false; } 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 && LocalizationInstance() != null) { return localization == LocalizationInstance(); } 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}"); } } private static void OnWatcherError(object sender, ErrorEventArgs e) { if (HasLocalAuthority) { DataForgePlugin.Log.LogWarning((object)("Localization file watcher lost events; scheduling a full reload: " + e.GetException().Message)); if (!DataForgeFileWatcher.TryRecreate("localization", delegate { SetupFileWatcher(); LocalFileModeReady = true; ReloadDebouncer?.Schedule(); })) { LocalFileModeReady = false; ReloadYamlValues(); } } } 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) { NotifyLocalizationChanged(); } } else { Watcher?.Dispose(); Watcher = null; ReloadDebouncer?.Dispose(); ReloadDebouncer = null; 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 = Translations.Invoke(localization).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); Translations.Invoke(localization)[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; } using DataForgeApplyScope dataForgeApplyScope = ((!DataForgeWorldLifecycle.IsShuttingDown && IsLiveLocalization(localization) && AppliedTranslations.Count > 0) ? DataForgeApi.BeginApply(DataForgeDomain.Localization, HasLocalAuthority, AppliedTranslations.Keys.Select((string text) => "$" + text), fullRefresh: false, AppliedLanguage) : null); bool flag = false; string[] array = AppliedTranslations.Keys.ToArray(); foreach (string token in array) { flag |= RestoreTranslationIfOwned(localization, token); } if (flag) { TranslationCache.Invoke(localization).EvictAll(); } ClearAppliedTranslationState(); dataForgeApplyScope?.Complete(Array.Empty<string>()); } private static bool RestoreTranslationIfOwned(Localization localization, string token) { if (!AppliedTranslations.TryGetValue(token, out TranslationLease value) || !Translations.Invoke(localization).TryGetValue(token, out var value2) || !string.Equals(value2, value.LastAppliedValue, StringComparison.Ordinal)) { return false; } if (value.OriginalExisted) { Translations.Invoke(localization)[token] = value.OriginalValue ?? ""; } else { Translations.Invoke(localization).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 (LocalizationInstance() == 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.3.5")] [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.3.5"; 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.3.5", MinimumRequiredVersion = "1.3.5", 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_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Expected O, but got Unknown //IL_01d9: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Expected O, but got Unknown DataForgeApi.SetWarningSink(delegate(string message) { Log.LogWarning((object)message); }); 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); DataForgeIconSync.ScheduleManifestRefresh(); }; _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); DataForgeIconSync.ScheduleManifestRefresh(); }; _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); DataForgeIconSync.ScheduleManifestRefresh(); }; _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); DataForgeIconSync.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() { DataForgeLifecycleStep.Run("file watcher recovery cleanup", DataForgeFileWatcher.CancelPendingRecreates); DataForgeLifecycleStep.Run("configuration save", delegate { SaveWithRespectToConfigSet(); }); DataForgeLifecycleStep.Run("configuration watcher cleanup", delegate { try { _watcher?.Dispose(); } finally { _watcher = null; } }); DataForgeLifecycleStep.Run("configuration reload cleanup", delegate { try { _configReloadDebouncer?.Dispose(); } finally { _configReloadDebouncer = null; } }); DataForgeLifecycleStep.Run("runtime cleanup", delegate { DataForgeRuntimeCleanup.RunOnce(); }); DataForgeLifecycleStep.Run("icon sync dispose", DataForgeIconSync.Dispose); DataForgeLifecycleStep.Run("localization dispose", LocalizationOverrideManager.Dispose); DataForgeLifecycleStep.Run("status-effect dispose", StatusEffectOverrideManager.Dispose); DataForgeLifecycleStep.Run("item dispose", ItemOverrideManager.Dispose); DataForgeLifecycleStep.Run("recipe dispose", RecipeOverrideManager.Dispose); DataForgeLifecycleStep.Run("piece dispose", PieceOverrideManager.Dispose); DataForgeLifecycleStep.Run("VNEI refresh dispose", VneiRefreshManager.Dispose); DataForgeLifecycleStep.Run("source-of-truth event cleanup", delegate { ConfigSync.SourceOfTruthChanged -= OnSourceOfTruthChanged; }); DataForgeLifecycleStep.Run("Harmony cleanup", (Action)_harmony.UnpatchSelf); DataForgeApi.Shutdown(); } private static void OnSourceOfTruthChanged(bool isSourceOfTruth) { DataForgeIconSync.OnSourceOfTruthChanged(); _sourceOfTruthFileModeReady = false; if (isSourceOfTruth) { EnsureSourceOfTruthFileMode(); return; } CancelDomainWatcherRecovery(); DataForgeLifecycleStep.Run("status-effect local-file cleanup", StatusEffectOverrideManager.SetupFileWatcher); DataForgeLifecycleStep.Run("item local-file cleanup", ItemOverrideManager.SetupFileWatcher); DataForgeLifecycleStep.Run("recipe local-file cleanup", RecipeOverrideManager.SetupFileWatcher); DataForgeLifecycleStep.Run("piece local-file cleanup", PieceOverrideManager.SetupFileWatcher); } internal static void EnsureSourceOfTruthFileMode() { if (!UsesLocalAuthorityFiles) { return; } DataForgeLifecycleStep.Run("localization source-of-truth setup", delegate { LocalizationOverrideManager.EnsureSourceOfTruthFileMode(); }); if (!_sourceOfTruthFileModeReady) { bool flag = DataForgeLifecycleStep.Run("status-effect source-of-truth watcher setup", StatusEffectOverrideManager.SetupFileWatcher); int num = 1 & (flag ? 1 : 0); if (flag) { DataForgeFileWatcher.CancelPendingRecreate("status-effect"); } int num2 = num & (DataForgeLifecycleStep.Run("status-effect source-of-truth reload", StatusEffectOverrideManager.ReloadFromDiskAndSync) ? 1 : 0); bool flag2 = DataForgeLifecycleStep.Run("item source-of-truth watcher setup", ItemOverrideManager.SetupFileWatcher); int num3 = num2 & (flag2 ? 1 : 0); if (flag2) { DataForgeFileWatcher.CancelPendingRecreate("item"); } int num4 = num3 & (DataForgeLifecycleStep.Run("item source-of-truth reload", ItemOverrideManager.ReloadFromDiskAndSync) ? 1 : 0); bool flag3 = DataForgeLifecycleStep.Run("recipe source-of-truth watcher setup", RecipeOverrideManager.SetupFileWatcher); int num5 = num4 & (flag3 ? 1 : 0); if (flag3) { DataForgeFileWatcher.CancelPendingRecreate("recipe"); } int num6 = num5 & (DataForgeLifecycleStep.Run("recipe source-of-truth reload", RecipeOverrideManager.ReloadFromDiskAndSync) ? 1 : 0); bool flag4 = DataForgeLifecycleStep.Run("piece source-of-truth watcher setup", PieceOverrideManager.SetupFileWatcher); int num7 = num6 & (flag4 ? 1 : 0); if (flag4) { DataForgeFileWatcher.CancelPendingRecreate("piece"); } _sourceOfTruthFileModeReady = (byte)((uint)num7 & (DataForgeLifecycleStep.Run("piece source-of-truth reload", PieceOverrideManager.ReloadFromDiskAndSync) ? 1u : 0u)) != 0; } } private static void CancelDomainWatcherRecovery() { DataForgeFileWatcher.CancelPendingRecreate("localization"); DataForgeFileWatcher.CancelPendingRecreate("status-effect"); DataForgeFileWatcher.CancelPendingRecreate("item"); DataForgeFileWatcher.CancelPendingRecreate("recipe"); DataForgeFileWatcher.CancelPendingRecreate("piece"); } private void Update() { DataForgeIconSync.Update(); VneiPrefabCleanupGuard.TryPatchVneiIndexAll(_harmony); DataForgeApi.DispatchPending(); } 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, OnConfigWatcherError); } private void ReadConfigValues(object sender, FileSystemEventArgs e) { _configReloadDebouncer?.Schedule(); } private void OnConfigWatcherError(object sender, ErrorEventArgs e) { Log.LogWarning((object)("Configuration file watcher lost events; scheduling a full reload: " + e.GetException().Message)); if (!DataForgeFileWatcher.TryRecreate("configuration", delegate { SetupWatcher(); _configReloadDebouncer?.Schedule(); })) { ReloadConfigValues(); } } 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; try { if (reload) { ((BaseUnityPlugin)this).Config.Reload(); } else { ((BaseUnityPlugin)this).Config.Save(); } } finally { ((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 { private delegate bool TryRegenerateReference(out string path, out bool changed, out string error); [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__WriteFullScaffoldFiles; public static ConsoleOptionsFetcher <1>__GetFullTabOptions; public static ConsoleEvent <2>__WriteReferenceFiles; public static ConsoleOptionsFetcher <3>__GetReferenceTabOptions; public static TryRegenerateReference <4>__TryRegenerateReferenceFile; public static TryRegenerateReference <5>__TryRegenerateReferenceFile; public static TryRegenerateReference <6>__TryRegenerateReferenceFile; public static TryRegenerateReference <7>__TryRegenerateReferenceFile; public static TryRegenerateReference <8>__TryRegeneratePieceCategoryReferenceFile; public static TryRegenerateReference <9>__TryRegenerateReferenceFile; } private const string WriteFullCommandName = "dataforge:full"; private const string WriteReferenceCommandName = "dataforge:refer"; private static readonly List<string> FullTabOptions = new List<string> { "item", "recipe", "effect", "piece", "all" }; private static readonly List<string> ReferenceTabOptions = new List<string> { "item", "recipe", "effect", "piece", "pieceCategory", "material", "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_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: 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, true, false, false, false, (ConsoleOptionsFetcher)obj2, false, true, true); object obj3 = <>O.<2>__WriteReferenceFiles; if (obj3 == null) { ConsoleEvent val3 = WriteReferenceFiles; <>O.<2>__WriteReferenceFiles = val3; obj3 = (object)val3; } object obj4 = <>O.<3>__GetReferenceTabOptions; if (obj4 == null) { ConsoleOptionsFetcher val4 = GetReferenceTabOptions; <>O.<3>__GetReferenceTabOptions = val4; obj4 = (object)val4; } new ConsoleCommand("dataforge:refer", "Regenerate DataForge compact reference files. Usage: dataforge:refer [item|recipe|effect|piece|pieceCategory|material|all]", (ConsoleEvent)obj3, false, false, true, false, false, false, (ConsoleOptionsFetcher)obj4, false, true, true); } } private static List<string> GetFullTabOptions() { return FullTabOptions; } private static List<string> GetReferenceTabOptions() { return ReferenceTabOptions; } 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 void WriteReferenceFiles(ConsoleEventArgs args) { string text = ((args.Length >= 2) ? (args[1] ?? "").Trim().ToLowerInvariant() : "all"); if (text.Length == 0) { text = "all"; } switch (text) { case "all": WriteReferenceResult(args, "item", ItemOverrideManager.TryRegenerateReferenceFile); WriteReferenceResult(args, "recipe", RecipeOverrideManager.TryRegenerateReferenceFile); WriteReferenceResult(args, "effect", StatusEffectOverrideManager.TryRegenerateReferenceFile); WriteReferenceResult(args, "piece", PieceOverrideManager.TryRegenerateReferenceFile); WriteReferenceResult(args, "piece category", PieceOverrideManager.TryRegeneratePieceCategoryReferenceFile); WriteReferenceResult(args, "material", MaterialReferenceWriter.TryRegenerateReferenceFile); break; case "items": case "item": WriteReferenceResult(args, "item", ItemOverrideManager.TryRegenerateReferenceFile); break; case "recipe": case "recipes": WriteReferenceResult(args, "recipe", RecipeOverrideManager.TryRegenerateReferenceFile); break; case "effect": case "effects": WriteReferenceResult(args, "effect", StatusEffectOverrideManager.TryRegenerateReferenceFile); break; case "piece": case "pieces": WriteReferenceResult(args, "piece", PieceOverrideManager.TryRegenerateReferenceFile); break; case "category": case "piececategory": case "piece-category": case "categories": WriteReferenceResult(args, "piece category", PieceOverrideManager.TryRegeneratePieceCategoryReferenceFile); break; case "material": case "materials": WriteReferenceResult(args, "material", MaterialReferenceWriter.TryRegenerateReferenceFile); break; default: { Terminal context = args.Context; if (context != null) { context.AddString("Syntax: dataforge:refer [item|recipe|effect|piece|pieceCategory|material|all]"); } break; } } } private static void WriteReferenceResult(ConsoleEventArgs args, string label, TryRegenerateReference regenerate) { if (!regenerate(out var path, out var changed, out var error)) { Terminal context = args.Context; if (context != null) { context.AddString(error); } } else { Terminal context2 = args.Context; if (context2 != null) { context2.AddString(changed ? ("Wrote " + label + " reference to " + path) : (label + " reference is already up to date at " + path)); } } } 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 sealed class DataForgeIconManifestEntry { internal string LogicalName { get; } internal string Hash { get; } internal int ByteLength { get; } internal int Width { get; } internal int Height { get; } internal long PixelCount => (long)Width * (long)Height; internal DataForgeIconManifestEntry(string logicalName, string hash, int byteLength, int width, int height) { LogicalName = logicalName; Hash = hash; ByteLength = byteLength; Width = width; Height = height; } } internal sealed class DataForgeIconManifest { internal string Revision { get; } internal IReadOnlyList<DataForgeIconManifestEntry> Entries { get; } internal int UniqueContentCount { get; } internal long TotalBytes { get; } internal long TotalPixels { get; } internal DataForgeIconManifest(string revision, List<DataForgeIconManifestEntry> entries, int uniqueContentCount, long totalBytes, long totalPixels) { Revision = revision; Entries = new ReadOnlyCollection<DataForgeIconManifestEntry>(entries.ToArray()); UniqueContentCount = uniqueContentCount; TotalBytes = totalBytes; TotalPixels = totalPixels; } } internal static class DataForgeIconProtocol { private readonly struct ContentDescription : IEquatable<ContentDescription> { private int ByteLength { get; } private int Width { get; } private int Height { get; } internal ContentDescription(int byteLength, int width, int height) { ByteLength = byteLength; Width = width; Height = height; } public bool Equals(ContentDescription other) { if (ByteLength == other.ByteLength && Width == other.Width) { return Height == other.Height; } return false; } } internal const int ProtocolVersion = 1; internal const int MaxIconBytes = 524288; internal const int MaxIconDimension = 1024; internal const long MaxIconPixels = 1048576L; internal const int MaxTotalBytes = 2097152; internal const long MaxTotalPixels = 16777216L; internal const int MaxIconCount = 128; internal const int MaxLogicalNameUtf8Bytes = 240; internal const int MaxManifestCharacters = 65536; private const int MinimumPngBytes = 33; private const string ManifestMagic = "DATAFORGE_ICONS_V1"; private const string RevisionPrefix = "revision="; private static readonly byte[] PngSignature = new byte[8] { 137, 80, 78, 71, 13, 10, 26, 10 }; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static bool TryNormalizeLogicalName(string? value, out string normalized, out string error) { normalized = string.Empty; error = string.Empty; if (string.IsNullOrWhiteSpace(value)) { error = "Icon name is empty."; return false; } string text = value.Trim(); if (text.Length > 240) { error = "Icon name '" + value + "' is too long."; return false; } string text2; try { text2 = text.Normalize(NormalizationForm.FormC).Replace('\\', '/'); if (StrictUtf8.GetByteCount(text2) > 240) { error = $"Icon name '{value}' exceeds the {240}-byte UTF-8 limit."; return false; } } catch (ArgumentException) { error = "Icon name '" + value + "' contains invalid Unicode text."; return false; } if (text2[0] == '/' || (text2.Length >= 2 && text2[1] == ':')) { error = "Icon name '" + value + "' must be relative."; return false; } string text3 = text2; for (int i = 0; i < text3.Length; i++) { if (char.IsControl(text3[i])) { error = "Icon name '" + value + "' contains a control character."; return false; } } string[] array = text2.Split(new char[1] { '/' }); foreach (string text4 in array) { if (text4.Length == 0 || text4 == "." || text4 == "..") { error = "Icon name '" + value + "' contains an empty or dot path segment."; return false; } if (!string.Equals(text4, text4.Trim(), StringComparison.Ordinal) || text4.EndsWith(".", StringComparison.Ordinal)) { error = "Icon name '" + value + "' contains a path segment with unsafe trailing characters."; return false; } if (text4.IndexOfAny(new char[7] { '<', '>', ':', '"', '|', '?', '*' }) >= 0) { error = "Icon name '" + value + "' contains a character that is invalid in a file name."; return false; } } string text5 = array[^1]; int num = text5.LastIndexOf('.'); if (num < 0) { array[^1] = text5 + ".png"; } else { if (num == 0 || !text5.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { error = "Icon name '" + value + "' must use the .png extension."; return false; } array[^1] = text5.Substring(0, text5.Length - 4) + ".png"; } normalized = string.Join("/", array); if (StrictUtf8.GetByteCount(normalized) > 240) { normalized = string.Empty; error = $"Icon name '{value}' exceeds the {240}-byte UTF-8 limit after normalization."; return false; } return true; } internal static string ComputeSha256(byte[] bytes) { if (bytes == null) { throw new ArgumentNullException("bytes"); } using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } internal static bool IsValidSha256(string? value) { if (value == null || value.Length != 64) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { return false; } } return true; } internal static bool TryReadPngInfo(byte[]? bytes, out int width, out int height, out string error) { width = 0; height = 0; error = string.Empty; if (bytes == null || bytes.Length < 33) { error = "PNG data is shorter than its signature and IHDR chunk."; return false; } if (bytes.Length > 524288) { error = $"PNG data exceeds the {524288}-byte per-icon limit."; return false; } for (int i = 0; i < PngSignature.Length; i++) { if (bytes[i] != PngSignature[i]) { error = "PNG signature is invalid."; return false; } } if (ReadUInt32BigEndian(bytes, 8) != 13 || bytes[12] != 73 || bytes[13] != 72 || bytes[14] != 68 || bytes[15] != 82) { error = "PNG does not start with a 13-byte IHDR chunk."; return false; } uint num = ReadUInt32BigEndian(bytes, 16); uint num2 = ReadUInt32BigEndian(bytes, 20); if (num == 0 || num2 == 0 || num > 1024 || num2 > 1024) { error = $"PNG dimensions must be between 1 and {1024} pixels."; return false; } if ((long)num * (long)num2 > 1048576) { error = $"PNG exceeds the {1048576L}-pixel per-icon limit."; return false; } width = (int)num; height = (int)num2; return true; } internal static bool TryCreateManifest(IEnumerable<DataForgeIconManifestEntry>? entries, out DataForgeIconManifest manifest, out string error) { manifest = null; error = string.Empty; if (entries == null) { error = "Icon manifest entries are missing."; return false; } List<DataForgeIconManifestEntry> list = new List<DataForgeIconManifestEntry>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Dictionary<string, ContentDescription> dictionary = new Dictionary<string, ContentDescription>(StringComparer.Ordinal); long num = 0L; long num2 = 0L; foreach (DataForgeIconManifestEntry entry in entries) { if (entry == null) { error = "Icon manifest contains a null entry."; return false; } if (list.Count >= 128) { error = $"Icon manifest exceeds the {128}-icon limit."; return false; } if (!TryNormalizeLogicalName(entry.LogicalName, out string normalized, out error)) { return false; } if (!hashSet.Add(normalized)) { error = "Icon manifest contains the duplicate logical name '" + normalized + "'."; return false; } if (!IsValidSha256(entry.Hash)) { error = "Icon '" + normalized + "' has an invalid lowercase SHA-256 hash."; return false; } if (!TryValidateManifestDimensions(normalized, entry.ByteLength, entry.Width, entry.Height, out error)) { return false; } ContentDescription contentDescription = new ContentDescription(entry.ByteLength, entry.Width, entry.Height); if (dictionary.TryGetValue(entry.Hash, out var value)) { if (!value.Equals(contentDescription)) { error = "Icons sharing hash '" + entry.Hash + "' disagree on size or dimensions."; return false; } } else { dictionary.Add(entry.Hash, contentDescription); num += entry.ByteLength; num2 += entry.PixelCount; if (num > 2097152) { error = $"Icon manifest exceeds the {2097152}-byte aggregate limit."; return false; } if (num2 > 16777216) { error = $"Icon manifest exceeds the {16777216L}-pixel aggregate limit."; return false; } } list.Add(new DataForgeIconManifestEntry(normalized, entry.Hash, entry.ByteLength, entry.Width, entry.Height)); } list.Sort((DataForgeIconManifestEntry left, DataForgeIconManifestEntry right) => StringComparer.Ordinal.Compare(left.LogicalName, right.LogicalName)); string s = BuildCanonicalBody(list); string revision = ComputeSha256(StrictUtf8.GetBytes(s)); manifest = new DataForgeIconManifest(revision, list, dictionary.Count, num, num2); return true; } internal static string SerializeManifest(IEnumerable<DataForgeIconManifestEntry> entries) { if (!TryCreateManifest(entries, out DataForgeIconManifest manifest, out string error)) { throw new ArgumentException(error, "entries"); } return SerializeManifest(manifest); } internal static string SerializeManifest(DataForgeIconManifest manifest) { if (manifest == null) { throw new ArgumentNullException("manifest"); } string text = BuildCanonicalBody(manifest.Entries); return "DATAFORGE_ICONS_V1\nrevision=" + manifest.Revision + "\n" + text; } internal static bool TryParseManifest(string? payload, out DataForgeIconManifest manifest, out string error) { manifest = null; error = string.Empty; if (payload == null) { error = "Icon manifest payload is missing."; return false; } if (payload.Length > 65536) { error = $"Icon manifest exceeds the {65536}-character limit."; return false; } string text = payload.Replace("\r\n", "\n"); if (text.IndexOf('\r') >= 0) { error = "Icon manifest contains an invalid line ending."; return false; } int num = text.IndexOf('\n'); int num2 = ((num < 0) ? (-1) : text.IndexOf('\n', num + 1)); if (num < 0 || num2 < 0 || !string.Equals(text.Substring(0, num), "DATAFORGE_ICONS_V1", StringComparison.Ordinal)) { error = "Icon manifest header is invalid."; return false; } string text2 = text.Substring(num + 1, num2 - num - 1); if (!text2.StartsWith("revision=", StringComparison.Ordinal)) { error = "Icon manifest revision line is invalid."; return false; } string text3 = text2.Substring("revision=".Length); if (!IsValidSha256(text3)) { error = "Icon manifest revision is not a lowercase SHA-256 hash."; return false; } string text4 = text.Substring(num2 + 1); if (text4.EndsWith("\n", StringComparison.Ordinal)) { error = "Icon manifest has a non-canonical trailing line."; return false; } string[] array = ((text4.Length == 0) ? Array.Empty<string>() : text4.Split(new char[1] { '\n' })); if (array.Length > 128) { error = $"Icon manifest exceeds the {128}-icon limit."; return false; } List<DataForgeIconManifestEntry> list = new List<DataForgeIconManifestEntry>(array.Length); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string[] array3 = array2[i].Split(new char[1] { '|' }); if (array3.Length != 5 || array3[0].Length == 0) { error = "Icon manifest contains a malformed entry line."; return false; } string text5; try { byte[] bytes = Convert.FromBase64String(array3[0]); text5 = StrictUtf8.GetString(bytes); if (!string.Equals(Convert.ToBase64String(StrictUtf8.GetBytes(text5)), array3[0], StringComparison.Ordinal)) { error = "Icon manifest contains a non-canonical encoded name."; return false; } } catch (FormatException) { error = "Icon manifest contains an invalid base64 name."; return false; } catch (DecoderFallbackException) { error = "Icon manifest contains a name that is not valid UTF-8."; return false; } if (!TryParsePositiveInt(array3[2], out var value) || !TryParsePositiveInt(array3[3], out var value2) || !TryParsePositiveInt(array3[4], out var value3)) { error = "Icon '" + text5 + "' has malformed numeric metadata."; return false; } list.Add(new DataForgeIconManifestEntry(text5, array3[1], value, value2, value3)); } if (!TryCreateManifest(list, out DataForgeIconManifest manifest2, out error)) { return false; } string b = BuildCanonicalBody(manifest2.Entries); if (!string.Equals(text4, b, StringComparison.Ordinal)) { error = "Icon manifest entries are not in canonical form or order."; return false; } if (!string.Equals(text3, manifest2.Revision, StringComparison.Ordinal)) { error = "Icon manifest revision does not match its canonical body."; return false; } manifest = manifest2; return true; } private static bool TryValidateManifestDimensions(string logicalName, int byteLength, int width, int height, out string error) { error = string.Empty; if (byteLength < 33 || byteLength > 524288) { error = "Icon '" + logicalName + "' has a byte length outside the permitted range."; return false; } if (width <= 0 || height <= 0 || width > 1024 || height > 1024) { error = "Icon '" + logicalName + "' has dimensions outside the permitted range."; return false; } if ((long)width * (long)height > 1048576) { error = "Icon '" + logicalName + "' exceeds the per-icon pixel limit."; return false; } return true; } private static bool TryParsePositiveInt(string text, out int value) { if (int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out value) && value > 0) { return string.Equals(value.ToString(CultureInfo.InvariantCulture), text, StringComparison.Ordinal); } return false; } private static string BuildCanonicalBody(IReadOnlyList<DataForgeIconManifestEntry> entries) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < entries.Count; i++) { if (i > 0) { stringBuilder.Append('\n'); } DataForgeIconManifestEntry dataForgeIconManifestEntry = entries[i]; stringBuilder.Append(Convert.ToBase64String(StrictUtf8.GetBytes(dataForgeIconManifestEntry.LogicalName))); stringBuilder.Append('|'); stringBuilder.Append(dataForgeIconManifestEntry.Hash); stringBuilder.Append('|'); stringBuilder.Append(dataForgeIconManifestEntry.ByteLength.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append('|'); stringBuilder.Append(dataForgeIconManifestEntry.Width.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append('|'); stringBuilder.Append(dataForgeIconManifestEntry.Height.ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } private static uint ReadUInt32BigEndian(byte[] bytes, int offset) { return (uint)((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]); } } 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, Func<TSource, IEnumerable<string>>? getComments = null) { DataForgeAssetOwnerCatalog.PrepareForReferenceGeneration(); 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.OwnerName, StringComparer.Ordinal).ThenBy<GroupedEntry<TSource>, string>((GroupedEntry<TSource> entry) => entry.SortKey, StringComparer.OrdinalIgnoreCase) .ThenBy<GroupedEntry<TSource>, string>((GroupedEntry<TSource> entry) => entry.SortKey, StringComparer.Ordinal) .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) { if (getComments != null) { AppendEntryComments(stringBuilder, getComments(item2.Entry)); } 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 void AppendEntryComments(StringBuilder builder, IEnumerable<string> comments) { foreach (string comment in comments) { string[] array = comment.Replace("\r\n", "\n").Replace('\r', '\n').Replace('\u0085', '\n') .Replace('\u2028', '\n') .Replace('\u2029', '\n') .Split(new char[1] { '\n' }); foreach (string value in array) { builder.Append("# ").AppendLine(value); } } } 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) { yield return normalizedName; int num = normalizedName.IndexOf(':'); if (num > 0) { string text = NormalizeName(normalizedName.Substring(0, num)); if (text.Length > 0 && !text.Equals(normalizedName, StringComparison.OrdinalIgnoreCase)) { yield return 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 static readonly object Sync = new object(); private static readonly Dictionary<string, AssetOwner> AssetOwners = new Dictionary<string, AssetOwner>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<string> AmbiguousAssetNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static string _loadedSignature = ""; private static bool _mappingsInitialized; internal static void PrepareForReferenceGeneration() { EnsureMappingsLoaded(); } internal static string GetOwnerName(string assetName) { if (!_mappingsInitialized) { EnsureMappingsLoaded(); } foreach (string item in EnumerateLookupCandidates(assetName)) { if (AssetOwners.TryGetValue(item, out AssetOwner value) && !string.IsNullOrWhiteSpace(value.Guid)) { return value.Name; } } return "Unknown / Untracked"; } private static void EnsureMappingsLoaded() { List<AssetOwner> pluginResources = GetPluginResources(); AssetBundle[] array = AssetBundle.GetAllLoadedAssetBundles().ToArray(); string text = BuildSignature(array, pluginResources); if (_mappingsInitialized && string.Equals(text, _loadedSignature, StringComparison.Ordinal)) { return; } lock (Sync) { if (_mappingsInitialized && string.Equals(text, _loadedSignature, StringComparison.Ordinal)) { return; } AssetOwners.Clear(); AmbiguousAssetNames.Clear(); foreach (AssetBundle item in array.OrderBy<AssetBundle, string>((AssetBundle bundle) => ((Object)bundle).name ?? "", StringComparer.OrdinalIgnoreCase).ThenBy<AssetBundle, string>((AssetBundle bundle) => ((Object)bundle).name ?? "", StringComparer.Ordinal)) { string text2 = ((Object)item).name ?? ""; if (text2.Length == 0) { continue; } AssetOwner assetOwner = AssetOwnerMatching.Resolve