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 GroupRecipes with Popups v1.0.2
plugins/GroupRecipesWithPopups.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GroupRecipesWithPopups.Core; using GroupRecipesWithPopups.Game; using GroupRecipesWithPopups.Net; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("555maggot666")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 555maggot666")] [assembly: AssemblyDescription("Shares recipe, material, piece and crafting-station discoveries with every player in a Valheim world and shows the original unlock popups.")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2+06394aaec1ffe4f68c5d1db7696aa7bede0559f9")] [assembly: AssemblyProduct("GroupRecipes with Popups")] [assembly: AssemblyTitle("GroupRecipesWithPopups")] [assembly: AssemblyVersion("1.0.2.0")] namespace GroupRecipesWithPopups { internal sealed class ModSettings { public ConfigEntry<bool> ShowNotifications { get; } public ConfigEntry<bool> SyncItems { get; } public ConfigEntry<bool> SyncRecipesAndPieces { get; } public ConfigEntry<bool> SyncCraftingStations { get; } public ConfigEntry<bool> EnablePopups { get; } public ConfigEntry<int> MaxPopupsOnJoin { get; } public ConfigEntry<float> PopupIntervalSeconds { get; } public ModSettings(ConfigFile config) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Expected O, but got Unknown ShowNotifications = config.Bind<bool>("General", "ShowNotifications", true, "Show the summary line (for example '12 more unlocks were synchronised') when entries were applied without a popup. The original Valheim unlock popups are not affected by this setting."); SyncItems = config.Bind<bool>("Sync", "Items", true, "Share discovered materials and items. Applies to sending and receiving."); SyncRecipesAndPieces = config.Bind<bool>("Sync", "RecipesAndPieces", true, "Share learned recipes and build pieces. Applies to sending and receiving."); SyncCraftingStations = config.Bind<bool>("Sync", "CraftingStations", true, "Share crafting station levels (only ever raised, never lowered). Applies to sending and receiving."); EnablePopups = config.Bind<bool>("Display", "EnablePopups", true, "Show the original Valheim unlock popup (icon plus 'New material' / 'Recipe learned' ...) for every entry received from other players. When disabled, received entries are still applied, just silently."); MaxPopupsOnJoin = config.Bind<int>("Display", "MaxPopupsOnJoin", 25, new ConfigDescription("Maximum number of popups when a large batch arrives (joining a world, or another player uploading their whole knowledge). Further entries are applied silently, nothing is lost.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 500), Array.Empty<object>())); PopupIntervalSeconds = config.Bind<float>("Display", "PopupIntervalSeconds", 0.3f, new ConfigDescription("Delay in seconds between two applied entries, so the popups appear one after another.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>())); } public bool IsKindEnabled(KnowledgeKind kind) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected I4, but got Unknown switch (kind - 1) { case 0: return SyncItems.Value; case 1: case 2: return SyncRecipesAndPieces.Value; case 3: return SyncCraftingStations.Value; default: return false; } } } [BepInPlugin("555maggot666.GroupRecipesWithPopups", "GroupRecipes with Popups", "1.0.2")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "555maggot666.GroupRecipesWithPopups"; public const string PluginName = "GroupRecipes with Popups"; public const string PluginVersion = "1.0.2"; private static readonly HashSet<string> WarnedKeys = new HashSet<string>(StringComparer.Ordinal); internal static ManualLogSource Log { get; private set; } internal static ModSettings Settings { get; private set; } private void Awake() { Log = ((BaseUnityPlugin)this).Logger; Settings = new ModSettings(((BaseUnityPlugin)this).Config); ((Component)this).gameObject.AddComponent<SyncDriver>(); Log.LogInfo((object)"GroupRecipes with Popups 1.0.2 loaded."); } internal static void WarnOnce(string key, string message) { if (WarnedKeys.Add(key)) { ManualLogSource log = Log; if (log != null) { log.LogWarning((object)message); } } } } internal sealed class SyncDriver : MonoBehaviour { private const float TickIntervalSeconds = 1f; private const int StableTicksBeforeFirstSync = 3; private GameCatalog _catalog; private PlayerKnowledgeBridge _bridge; private VanillaUnlocker _unlocker; private ZRoutedRpc _rpc; private ZNet _znet; private Ledger _ledger; private SyncHost _host; private SyncClient _client; private bool _helloDone; private float _tickTimer; private int _playerTicks; private void Awake() { _catalog = new GameCatalog(); _bridge = new PlayerKnowledgeBridge(); _unlocker = new VanillaUnlocker(_catalog, _bridge); } private void Update() { try { ZRoutedRpc instance = ZRoutedRpc.instance; ZNet instance2 = ZNet.instance; if (instance == null || (Object)(object)instance2 == (Object)null) { if (_rpc != null) { EndSession(); } return; } if (instance != _rpc || instance2 != _znet) { BeginSession(instance, instance2); } _host?.EnsureOpen(instance2); float unscaledDeltaTime = Time.unscaledDeltaTime; _client?.Update(unscaledDeltaTime); _tickTimer += unscaledDeltaTime; if (_tickTimer >= 1f) { _tickTimer = 0f; SyncTick(); } } catch (Exception ex) { Failed("update", ex); } } private void BeginSession(ZRoutedRpc rpc, ZNet znet) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown EndSession(); _rpc = rpc; _znet = znet; _ledger = new Ledger(); _host = (znet.IsServer() ? new SyncHost(_ledger) : null); _client = (znet.IsDedicated() ? null : new SyncClient(_ledger, _catalog, _bridge, _unlocker, Plugin.Settings, SendNew)); _helloDone = false; _tickTimer = 0f; _playerTicks = 0; try { rpc.Register<ZPackage>("GRWP_Hello", (Action<long, ZPackage>)OnHello); rpc.Register<ZPackage>("GRWP_New", (Action<long, ZPackage>)OnNew); rpc.Register<ZPackage>("GRWP_CatchUp", (Action<long, ZPackage>)OnCatchUp); } catch (Exception ex) { Failed("rpc registration", ex); } Plugin.Log.LogInfo((object)("Sync session started (" + ((_host != null) ? "server role" : "client role") + ((_client == null) ? ", no local player" : string.Empty) + ").")); } private void EndSession() { _client?.Reset(); _client = null; _host = null; _ledger = null; _rpc = null; _znet = null; _helloDone = false; } private void SyncTick() { Player localPlayer = Player.m_localPlayer; if (_client == null || (Object)(object)localPlayer == (Object)null) { _playerTicks = 0; } else { if (++_playerTicks < 3 || !_catalog.Refresh()) { return; } if (!_helloDone) { _helloDone = true; if (_host != null) { _host.EnsureOpen(_znet); _client.Receive(new List<KnowledgeEntry>(_ledger.Entries), ReceiveMode.CatchUp, final: true, 0L); } else { _rpc.InvokeRoutedRPC("GRWP_Hello", new object[1] { SyncProtocol.Encode(new List<KnowledgeEntry>(), 0, 0, 2) }); } } _client.Tick(localPlayer); } } private void SendNew(List<KnowledgeEntry> changed, bool bulk) { if (_host != null) { _host.EnsureOpen(_znet); _host.Persist(changed); } foreach (ZPackage item in SyncProtocol.EncodeChunks(changed, bulk)) { _rpc.InvokeRoutedRPC(0L, "GRWP_New", new object[1] { item }); } } private void OnHello(long sender, ZPackage package) { try { if (_host != null && _rpc != null && sender != ZNet.GetUID()) { _host.EnsureOpen(_znet); _host.SendCatchUp(_rpc, sender); } } catch (Exception ex) { Failed("HELLO handler", ex); } } private void OnNew(long sender, ZPackage package) { HandleEntries(sender, package, isCatchUp: false); } private void OnCatchUp(long sender, ZPackage package) { HandleEntries(sender, package, isCatchUp: true); } private void HandleEntries(long sender, ZPackage package, bool isCatchUp) { string text = (isCatchUp ? "CATCHUP handler" : "NEW handler"); try { if (_ledger == null || sender == ZNet.GetUID() || (isCatchUp && _host != null)) { return; } if (!SyncProtocol.TryDecode(package, out var entries, out var flags, out var error)) { Plugin.WarnOnce(text + "-decode-" + error, text + ": ignoring bad package from " + sender + " (" + error + ")"); } else { List<KnowledgeEntry> list = _ledger.AddRange((IEnumerable<KnowledgeEntry>)entries); if (_host != null) { _host.EnsureOpen(_znet); _host.Persist(list); } ReceiveMode mode = (isCatchUp ? ReceiveMode.CatchUp : (((flags & 1) != 0) ? ReceiveMode.Bulk : ReceiveMode.Live)); _client?.Receive(list, mode, (flags & 2) != 0, sender); } } catch (Exception ex) { Failed(text, ex); } } private static void Failed(string where, Exception ex) { Plugin.WarnOnce("failure-" + where + "-" + ex.GetType().Name + "-" + ex.Message, "GroupRecipes with Popups: " + where + " failed: " + ex.GetType().Name + ": " + ex.Message); } } } namespace GroupRecipesWithPopups.Net { internal sealed class LedgerStore { private readonly LedgerFile _file; public string Path => _file.Path; public LedgerStore(string worldName, long worldUid) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown string text = System.IO.Path.Combine(Paths.ConfigPath, "GroupRecipesWithPopups", GrwpPaths.LedgerFileName(worldName, worldUid)); _file = new LedgerFile(text); } public void LoadInto(Ledger ledger) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown try { Ledger val = new Ledger(); LedgerParseResult val2 = _file.Load(val); foreach (string warning in val2.Warnings) { Plugin.Log.LogWarning((object)("Ledger file " + _file.Path + ": " + warning)); } if (!val2.IsSupported) { Plugin.Log.LogWarning((object)("Ledger file " + _file.Path + " has an unsupported version; it is left untouched and not used for persistence.")); } List<KnowledgeEntry> list = val.AddRange((IEnumerable<KnowledgeEntry>)ledger.Entries); ledger.AddRange((IEnumerable<KnowledgeEntry>)val.Entries); if (list.Count > 0) { Append(list); } Plugin.Log.LogInfo((object)("Ledger " + _file.Path + " loaded: " + ledger.Count + " entries.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not read ledger file " + _file.Path + ": " + ex.GetType().Name + ": " + ex.Message)); } } public void Append(IReadOnlyCollection<KnowledgeEntry> entries) { if (entries.Count == 0) { return; } try { _file.Append((IEnumerable<KnowledgeEntry>)entries); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { Plugin.WarnOnce("ledger-write-" + ex.GetType().Name, "Could not write ledger file " + _file.Path + ": " + ex.GetType().Name + ": " + ex.Message); } catch (Exception ex2) { Plugin.WarnOnce("ledger-write-other", "Unexpected error while writing " + _file.Path + ": " + ex2.GetType().Name + ": " + ex2.Message); } } } internal enum ReceiveMode { Live, Bulk, CatchUp } internal sealed class SyncClient { private readonly struct Pending { public List<KnowledgeEntry> Entries { get; } public ReceiveMode Mode { get; } public bool Final { get; } public long Sender { get; } public Pending(List<KnowledgeEntry> entries, ReceiveMode mode, bool final, long sender) { Entries = entries; Mode = mode; Final = final; Sender = sender; } } private readonly Ledger _ledger; private readonly GameCatalog _catalog; private readonly PlayerKnowledgeBridge _bridge; private readonly VanillaUnlocker _unlocker; private readonly ModSettings _settings; private readonly Action<List<KnowledgeEntry>, bool> _send; private readonly KnowledgeSnapshot _snapshot = new KnowledgeSnapshot(); private readonly List<Pending> _backlog = new List<Pending>(); private readonly Dictionary<long, int> _popupBudgets = new Dictionary<long, int>(); private PopupPump _pump; private double _pumpInterval; private bool _uploadedOnce; private int _hiddenTotal; private bool _noticeReady; public SyncClient(Ledger ledger, GameCatalog catalog, PlayerKnowledgeBridge bridge, VanillaUnlocker unlocker, ModSettings settings, Action<List<KnowledgeEntry>, bool> send) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown _ledger = ledger; _catalog = catalog; _bridge = bridge; _unlocker = unlocker; _settings = settings; _send = send; _pumpInterval = settings.PopupIntervalSeconds.Value; _pump = new PopupPump(_pumpInterval); } public void Tick(Player player) { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !_catalog.Refresh()) { return; } DrainBacklog(); List<KnowledgeEntry> list = new List<KnowledgeEntry>(); if (!_bridge.TryRead(player, _catalog, list)) { Plugin.WarnOnce("read-knowledge", "Could not read the player's knowledge; nothing is sent."); return; } List<KnowledgeEntry> list2 = new List<KnowledgeEntry>(); foreach (KnowledgeEntry item in _snapshot.Diff((IEnumerable<KnowledgeEntry>)list)) { KnowledgeEntry current = item; if (_settings.IsKindEnabled(((KnowledgeEntry)(ref current)).Kind)) { list2.Add(current); } } bool flag = !_uploadedOnce; _uploadedOnce = true; if (list2.Count != 0) { _snapshot.RecordRange((IEnumerable<KnowledgeEntry>)list2); List<KnowledgeEntry> list3 = _ledger.AddRange((IEnumerable<KnowledgeEntry>)list2); if (list3.Count > 0) { bool arg = flag || list3.Count > _settings.MaxPopupsOnJoin.Value; _send(list3, arg); } } } public void Receive(List<KnowledgeEntry> entries, ReceiveMode mode, bool final, long sender) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { _backlog.Add(new Pending(entries, mode, final, sender)); return; } List<KnowledgeEntry> list = new List<KnowledgeEntry>(); foreach (KnowledgeEntry entry in entries) { KnowledgeEntry current = entry; _snapshot.Record(current); if (_settings.IsKindEnabled(((KnowledgeEntry)(ref current)).Kind) && !_bridge.IsKnown(localPlayer, current)) { list.Add(current); } } int num = int.MaxValue; if (!_settings.EnablePopups.Value) { num = 0; } else if (mode != ReceiveMode.Live) { if (!_popupBudgets.TryGetValue(sender, out var value)) { value = Math.Max(0, _settings.MaxPopupsOnJoin.Value); } num = value; } PopupPlan val = PopupPlanner.BuildPlan((IEnumerable<KnowledgeEntry>)list, num); if (mode != ReceiveMode.Live && _settings.EnablePopups.Value) { if (final) { _popupBudgets.Remove(sender); } else { _popupBudgets[sender] = Math.Max(0, num - val.PopupCount); } } _pump.Enqueue(val); _hiddenTotal += val.HiddenCount; if (final && _hiddenTotal > 0) { _noticeReady = true; } } public void Update(float deltaSeconds) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } if (_pump.IsEmpty && Math.Abs(_pumpInterval - (double)_settings.PopupIntervalSeconds.Value) > 1E-06) { _pumpInterval = _settings.PopupIntervalSeconds.Value; _pump = new PopupPump(_pumpInterval); } _pump.Tick((double)deltaSeconds); _unlocker.Tick(localPlayer); int num = 0; KnowledgeEntry entry = default(KnowledgeEntry); for (int i = 0; i < 500; i++) { if (!_pump.TryDequeueSilent(ref entry)) { break; } if (_unlocker.Apply(localPlayer, entry, showPopup: false)) { num++; } } bool showPopup = default(bool); if (_pump.TryDequeue(ref entry, ref showPopup) && _unlocker.Apply(localPlayer, entry, showPopup)) { num++; } if (num > 0) { _unlocker.Refresh(localPlayer); } if (_noticeReady && _pump.IsEmpty) { ShowNotice(localPlayer); } } public void Reset() { _snapshot.Clear(); _backlog.Clear(); _popupBudgets.Clear(); _pump.Clear(); _unlocker.Reset(); _uploadedOnce = false; _hiddenTotal = 0; _noticeReady = false; } private void DrainBacklog() { if (_backlog.Count == 0 || (Object)(object)Player.m_localPlayer == (Object)null) { return; } List<Pending> list = new List<Pending>(_backlog); _backlog.Clear(); foreach (Pending item in list) { Receive(item.Entries, item.Mode, item.Final, item.Sender); } } private void ShowNotice(Player player) { int hiddenTotal = _hiddenTotal; _hiddenTotal = 0; _noticeReady = false; if (hiddenTotal > 0 && _settings.ShowNotifications.Value) { string text = ((hiddenTotal == 1) ? "unlock was" : "unlocks were"); ((Character)player).Message((MessageType)1, "GroupRecipes: " + hiddenTotal + " more " + text + " synchronised without popup", 0, (Sprite)null, false); } } } internal sealed class SyncHost { private readonly Ledger _ledger; private LedgerStore _store; public bool IsOpen => _store != null; public SyncHost(Ledger ledger) { _ledger = ledger; } public void EnsureOpen(ZNet znet) { if (_store == null && !((Object)(object)znet == (Object)null)) { string worldName = znet.GetWorldName(); if (!string.IsNullOrEmpty(worldName)) { _store = new LedgerStore(worldName, znet.GetWorldUID()); _store.LoadInto(_ledger); } } } public void Persist(IReadOnlyCollection<KnowledgeEntry> changed) { if (_store != null && changed.Count > 0) { _store.Append(changed); } } public void SendCatchUp(ZRoutedRpc rpc, long target) { foreach (ZPackage item in SyncProtocol.EncodeChunks(new List<KnowledgeEntry>(_ledger.Entries), bulk: false)) { rpc.InvokeRoutedRPC(target, "GRWP_CatchUp", new object[1] { item }); } } } internal static class SyncProtocol { public const string RpcHello = "GRWP_Hello"; public const string RpcNew = "GRWP_New"; public const string RpcCatchUp = "GRWP_CatchUp"; public const byte Version = 1; public const byte FlagBulk = 1; public const byte FlagFinal = 2; public const int MaxEntriesPerPackage = 500; private const int MaxAcceptedCount = 100000; public static List<ZPackage> EncodeChunks(IReadOnlyList<KnowledgeEntry> entries, bool bulk) { List<ZPackage> list = new List<ZPackage>(); int num = 0; do { int num2 = Math.Min(500, entries.Count - num); bool flag = num + num2 >= entries.Count; byte flags = (byte)((bulk ? 1u : 0u) | (uint)(flag ? 2 : 0)); list.Add(Encode(entries, num, num2, flags)); num += num2; } while (num < entries.Count); return list; } public static ZPackage Encode(IReadOnlyList<KnowledgeEntry> entries, int start, int count, byte flags) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) ZPackage val = new ZPackage(); val.Write((byte)1); val.Write(flags); val.Write(count); for (int i = start; i < start + count; i++) { KnowledgeEntry val2 = entries[i]; val.Write((byte)((KnowledgeEntry)(ref val2)).Kind); val.Write(((KnowledgeEntry)(ref val2)).Key); val.Write(((KnowledgeEntry)(ref val2)).Level); } return val; } public static bool TryDecode(ZPackage package, out List<KnowledgeEntry> entries, out byte flags, out string error) { //IL_00c9: Unknown result type (might be due to invalid IL or missing references) entries = new List<KnowledgeEntry>(); flags = 0; error = null; if (package == null) { error = "package is null"; return false; } try { byte b = package.ReadByte(); if (b != 1) { error = "unsupported protocol version " + b; return false; } flags = package.ReadByte(); int num = package.ReadInt(); if (num < 0 || num > 100000) { error = "implausible entry count " + num; return false; } for (int i = 0; i < num; i++) { byte b2 = package.ReadByte(); string text = package.ReadString(); int num2 = package.ReadInt(); if (b2 < 1 || b2 > 4 || string.IsNullOrEmpty(text)) { error = "invalid entry #" + i; entries.Clear(); return false; } entries.Add(new KnowledgeEntry((KnowledgeKind)b2, text, num2)); } return true; } catch (Exception ex) { entries.Clear(); error = "malformed package: " + ex.GetType().Name + ": " + ex.Message; return false; } } } } namespace GroupRecipesWithPopups.Game { internal sealed class GameCatalog { private readonly Dictionary<string, Recipe> _recipes = new Dictionary<string, Recipe>(StringComparer.Ordinal); private readonly Dictionary<string, Piece> _pieces = new Dictionary<string, Piece>(StringComparer.Ordinal); private readonly Dictionary<string, ItemData> _items = new Dictionary<string, ItemData>(StringComparer.Ordinal); private readonly Dictionary<string, CraftingStation> _stations = new Dictionary<string, CraftingStation>(StringComparer.Ordinal); private ObjectDB _db; private int _dbItemCount = -1; private int _dbRecipeCount = -1; private ZNetScene _scene; private int _sceneCount = -1; private float _lastForcedRebuild = float.NegativeInfinity; public bool TryRebuildAfterMiss() { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null || instance.m_items == null || instance.m_recipes == null || instance.m_items.Count == 0) { return false; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup - _lastForcedRebuild < 2f) { return false; } _lastForcedRebuild = realtimeSinceStartup; RebuildObjectTables(instance); return true; } public bool Refresh() { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null || instance.m_items == null || instance.m_recipes == null || instance.m_items.Count == 0 || instance.m_recipes.Count == 0) { return false; } if (instance != _db || instance.m_items.Count != _dbItemCount || instance.m_recipes.Count != _dbRecipeCount) { RebuildObjectTables(instance); } ZNetScene instance2 = ZNetScene.instance; if ((Object)(object)instance2 != (Object)null && instance2.m_prefabs != null && (instance2 != _scene || instance2.m_prefabs.Count != _sceneCount)) { RebuildStationTable(instance2); } return true; } public bool TryGetRecipe(string key, out Recipe recipe) { if (_recipes.TryGetValue(key, out recipe)) { return (Object)(object)recipe != (Object)null; } return false; } public bool TryGetPiece(string key, out Piece piece) { if (_pieces.TryGetValue(key, out piece)) { return (Object)(object)piece != (Object)null; } return false; } public bool TryGetItem(string key, out ItemData item) { if (_items.TryGetValue(key, out item)) { return item != null; } return false; } public bool TryGetStation(string key, out CraftingStation station) { if (_stations.TryGetValue(key, out station)) { return (Object)(object)station != (Object)null; } return false; } public Sprite GetStationIcon(string key) { if (!TryGetStation(key, out var station)) { return null; } return station.m_icon; } public KnowledgeKind ClassifyKnownRecipeKey(string key) { if (!_recipes.ContainsKey(key)) { if (_pieces.ContainsKey(key)) { return (KnowledgeKind)3; } return (KnowledgeKind)2; } return (KnowledgeKind)2; } private void RebuildObjectTables(ObjectDB db) { _items.Clear(); _recipes.Clear(); _pieces.Clear(); foreach (GameObject item in db.m_items) { if ((Object)(object)item == (Object)null) { continue; } ItemDrop component = item.GetComponent<ItemDrop>(); if (!((Object)(object)component == (Object)null) && component.m_itemData != null && component.m_itemData.m_shared != null) { string name = component.m_itemData.m_shared.m_name; if (!string.IsNullOrEmpty(name)) { _items.TryAdd(name, component.m_itemData); } } } foreach (Recipe recipe in db.m_recipes) { if (!((Object)(object)recipe == (Object)null) && !((Object)(object)recipe.m_item == (Object)null) && recipe.m_item.m_itemData != null && recipe.m_item.m_itemData.m_shared != null) { string name2 = recipe.m_item.m_itemData.m_shared.m_name; if (!string.IsNullOrEmpty(name2)) { _recipes.TryAdd(name2, recipe); } } } List<Piece> allBuildPieces = db.GetAllBuildPieces(true); if (allBuildPieces != null) { foreach (Piece item2 in allBuildPieces) { if ((Object)(object)item2 != (Object)null && !string.IsNullOrEmpty(item2.m_name)) { _pieces.TryAdd(item2.m_name, item2); } } } _db = db; _dbItemCount = db.m_items.Count; _dbRecipeCount = db.m_recipes.Count; } private void RebuildStationTable(ZNetScene scene) { _stations.Clear(); foreach (GameObject prefab in scene.m_prefabs) { if (!((Object)(object)prefab == (Object)null)) { CraftingStation component = prefab.GetComponent<CraftingStation>(); if ((Object)(object)component != (Object)null && !string.IsNullOrEmpty(component.m_name)) { _stations.TryAdd(component.m_name, component); } } } _scene = scene; _sceneCount = scene.m_prefabs.Count; } } internal sealed class PlayerKnowledgeBridge { private const BindingFlags InstanceNonPublic = BindingFlags.Instance | BindingFlags.NonPublic; private readonly FieldInfo _knownMaterial; private readonly FieldInfo _knownRecipes; private readonly FieldInfo _knownStations; private readonly MethodInfo _addKnownRecipe; private readonly MethodInfo _addKnownPiece; private readonly MethodInfo _updateKnownRecipesList; private readonly MethodInfo _updateAvailablePiecesList; public bool CanAccessKnowledge { get { if (_knownMaterial != null && _knownRecipes != null) { return _knownStations != null; } return false; } } public bool HasAddKnownRecipe => _addKnownRecipe != null; public bool HasAddKnownPiece => _addKnownPiece != null; public PlayerKnowledgeBridge() { Type typeFromHandle = typeof(Player); _knownMaterial = FindField(typeFromHandle, "m_knownMaterial"); _knownRecipes = FindField(typeFromHandle, "m_knownRecipes"); _knownStations = FindField(typeFromHandle, "m_knownStations"); _addKnownRecipe = FindMethod(typeFromHandle, "AddKnownRecipe", typeof(Recipe)); _addKnownPiece = FindMethod(typeFromHandle, "AddKnownPiece", typeof(Piece)); _updateKnownRecipesList = FindMethod(typeFromHandle, "UpdateKnownRecipesList"); _updateAvailablePiecesList = FindMethod(typeFromHandle, "UpdateAvailablePiecesList"); } public bool TryRead(Player player, GameCatalog catalog, List<KnowledgeEntry> sink) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || !CanAccessKnowledge) { return false; } HashSet<string> hashSet = _knownMaterial.GetValue(player) as HashSet<string>; HashSet<string> hashSet2 = _knownRecipes.GetValue(player) as HashSet<string>; Dictionary<string, int> dictionary = _knownStations.GetValue(player) as Dictionary<string, int>; if (hashSet == null || hashSet2 == null || dictionary == null) { return false; } foreach (string item in hashSet) { if (!string.IsNullOrEmpty(item)) { sink.Add(new KnowledgeEntry((KnowledgeKind)1, item, 0)); } } foreach (string item2 in hashSet2) { if (!string.IsNullOrEmpty(item2)) { sink.Add(new KnowledgeEntry(catalog.ClassifyKnownRecipeKey(item2), item2, 0)); } } foreach (KeyValuePair<string, int> item3 in dictionary) { if (!string.IsNullOrEmpty(item3.Key)) { sink.Add(new KnowledgeEntry((KnowledgeKind)4, item3.Key, item3.Value)); } } return true; } public bool IsKnown(Player player, KnowledgeEntry entry) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected I4, but got Unknown if ((Object)(object)player == (Object)null) { return false; } KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind; switch (kind - 1) { case 0: return player.IsMaterialKnown(((KnowledgeEntry)(ref entry)).Key); case 1: case 2: return player.IsRecipeKnown(((KnowledgeEntry)(ref entry)).Key); case 3: { if (TryGetStationLevel(player, ((KnowledgeEntry)(ref entry)).Key, out var level)) { return level >= ((KnowledgeEntry)(ref entry)).Level; } return false; } default: return true; } } public bool TryGetStationLevel(Player player, string station, out int level) { level = 0; if (_knownStations?.GetValue(player) is Dictionary<string, int> dictionary) { return dictionary.TryGetValue(station, out level); } return false; } public bool TryWrite(Player player, KnowledgeEntry entry) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected I4, but got Unknown if ((Object)(object)player == (Object)null || !CanAccessKnowledge) { Plugin.WarnOnce("bridge-fields", "Player knowledge fields were not found in this game version; cannot apply entries."); return false; } KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind; switch (kind - 1) { case 0: return (_knownMaterial.GetValue(player) as HashSet<string>)?.Add(((KnowledgeEntry)(ref entry)).Key) ?? false; case 1: case 2: return (_knownRecipes.GetValue(player) as HashSet<string>)?.Add(((KnowledgeEntry)(ref entry)).Key) ?? false; case 3: { if (_knownStations.GetValue(player) is Dictionary<string, int> dictionary && (!dictionary.TryGetValue(((KnowledgeEntry)(ref entry)).Key, out var value) || value < ((KnowledgeEntry)(ref entry)).Level)) { dictionary[((KnowledgeEntry)(ref entry)).Key] = ((KnowledgeEntry)(ref entry)).Level; return true; } return false; } default: return false; } } public bool TryInvokeAddKnownRecipe(Player player, Recipe recipe) { return TryInvoke(_addKnownRecipe, "AddKnownRecipe", player, recipe); } public bool TryInvokeAddKnownPiece(Player player, Piece piece) { return TryInvoke(_addKnownPiece, "AddKnownPiece", player, piece); } public void Refresh(Player player) { TryInvoke(_updateKnownRecipesList, "UpdateKnownRecipesList", player); TryInvoke(_updateAvailablePiecesList, "UpdateAvailablePiecesList", player); } public void RefreshKnownRecipes(Player player) { TryInvoke(_updateKnownRecipesList, "UpdateKnownRecipesList", player); } private static bool TryInvoke(MethodInfo method, string name, Player player, params object[] args) { if (method == null || (Object)(object)player == (Object)null) { return false; } try { method.Invoke(player, args); return true; } catch (Exception ex) { Exception ex2 = ((ex is TargetInvocationException && ex.InnerException != null) ? ex.InnerException : ex); Plugin.WarnOnce("invoke-" + name, "Player." + name + " failed: " + ex2.GetType().Name + ": " + ex2.Message); return false; } } private static FieldInfo FindField(Type type, string name) { FieldInfo? field = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); if (field == null) { Plugin.WarnOnce("field-" + name, "Player." + name + " was not found in this game version."); } return field; } private static MethodInfo FindMethod(Type type, string name, params Type[] parameters) { MethodInfo? method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null); if (method == null) { Plugin.WarnOnce("method-" + name, "Player." + name + " was not found in this game version; falling back to direct writes."); } return method; } } internal sealed class VanillaUnlocker { private const int MaxDeferred = 200; private readonly GameCatalog _catalog; private readonly PlayerKnowledgeBridge _bridge; private readonly List<KnowledgeEntry> _deferredPieces = new List<KnowledgeEntry>(); private readonly HashSet<string> _deferredKeys = new HashSet<string>(StringComparer.Ordinal); public VanillaUnlocker(GameCatalog catalog, PlayerKnowledgeBridge bridge) { _catalog = catalog; _bridge = bridge; } private static bool IsBuildHudReady() { Hud instance = Hud.instance; if ((Object)(object)instance != (Object)null) { return (Object)(object)instance.m_buildUi != (Object)null; } return false; } private bool DeferPiece(KnowledgeEntry entry) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (_deferredPieces.Count >= 200) { return false; } if (_deferredKeys.Add(((KnowledgeEntry)(ref entry)).Key)) { _deferredPieces.Add(entry); } return true; } public void Reset() { _deferredPieces.Clear(); _deferredKeys.Clear(); } public void Tick(Player player) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || _deferredPieces.Count == 0 || !IsBuildHudReady()) { return; } KnowledgeEntry[] array = _deferredPieces.ToArray(); _deferredPieces.Clear(); _deferredKeys.Clear(); for (int i = 0; i < array.Length; i++) { if (!_bridge.IsKnown(player, array[i])) { try { Apply(player, array[i], showPopup: true); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Retrying the held back piece " + ((object)Unsafe.As<KnowledgeEntry, KnowledgeEntry>(ref array[i])/*cast due to .constrained prefix*/).ToString() + " failed: " + ex.GetType().Name)); } } } } public unsafe bool Apply(Player player, KnowledgeEntry entry, bool showPopup) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected I4, but got Unknown //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return false; } try { if (_bridge.IsKnown(player, entry)) { return false; } KnowledgeKind kind = ((KnowledgeEntry)(ref entry)).Kind; return (kind - 1) switch { 0 => ApplyMaterial(player, entry, showPopup), 1 => ApplyRecipe(player, entry, showPopup), 2 => ApplyPiece(player, entry, showPopup), 3 => ApplyStation(player, entry, showPopup), _ => false, }; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Applying " + ((object)(*(KnowledgeEntry*)(&entry))/*cast due to .constrained prefix*/).ToString() + " failed: " + ex.GetType().Name + ": " + ex.Message)); return false; } } public void Refresh(Player player) { try { _bridge.Refresh(player); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Refreshing the player's known lists failed: " + ex.GetType().Name + ": " + ex.Message)); } } private bool ApplyMaterial(Player player, KnowledgeEntry entry, bool showPopup) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) if (!_catalog.TryGetItem(((KnowledgeEntry)(ref entry)).Key, out var item)) { Plugin.WarnOnce("missing-item-" + ((KnowledgeEntry)(ref entry)).Key, "Item '" + ((KnowledgeEntry)(ref entry)).Key + "' does not exist in this game; adding it to the known materials directly."); bool num = _bridge.TryWrite(player, entry); if (num && showPopup) { QueuePopup(null, "$msg_newmaterial", ((KnowledgeEntry)(ref entry)).Key); } return num; } if (showPopup) { player.AddKnownItem(item); return _bridge.IsKnown(player, entry); } return _bridge.TryWrite(player, entry); } private bool ApplyRecipe(Player player, KnowledgeEntry entry, bool showPopup) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) if (!_catalog.TryGetRecipe(((KnowledgeEntry)(ref entry)).Key, out var recipe)) { Plugin.WarnOnce("unmapped-recipe-" + ((KnowledgeEntry)(ref entry)).Key, "'" + ((KnowledgeEntry)(ref entry)).Key + "' matches no recipe or piece in this game; adding it to the known recipes silently."); return _bridge.TryWrite(player, entry); } if (!showPopup) { return _bridge.TryWrite(player, entry); } if (_bridge.TryInvokeAddKnownRecipe(player, recipe) && _bridge.IsKnown(player, entry)) { return true; } Plugin.WarnOnce("fallback-recipe", "Vanilla AddKnownRecipe is unavailable; using the direct write plus manual popup fallback."); if (!_bridge.TryWrite(player, entry)) { return false; } QueuePopup(((Object)(object)recipe.m_item != (Object)null && recipe.m_item.m_itemData != null) ? recipe.m_item.m_itemData.GetIcon() : null, "$msg_newrecipe", ((KnowledgeEntry)(ref entry)).Key); return true; } private unsafe bool ApplyPiece(Player player, KnowledgeEntry entry, bool showPopup) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Invalid comparison between Unknown and I4 //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Invalid comparison between Unknown and I4 //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Invalid comparison between Unknown and I4 if (!_catalog.TryGetPiece(((KnowledgeEntry)(ref entry)).Key, out var piece) && _catalog.TryRebuildAfterMiss()) { _catalog.TryGetPiece(((KnowledgeEntry)(ref entry)).Key, out piece); } if ((Object)(object)piece == (Object)null) { Plugin.WarnOnce("missing-piece-" + ((KnowledgeEntry)(ref entry)).Key, "Piece '" + ((KnowledgeEntry)(ref entry)).Key + "' does not exist in this game; adding it to the known recipes directly."); bool num = _bridge.TryWrite(player, entry); if (num && showPopup) { QueuePopup(null, "$msg_newpiece", ((KnowledgeEntry)(ref entry)).Key); } return num; } if (!showPopup) { return _bridge.TryWrite(player, entry); } if (!IsBuildHudReady()) { if (DeferPiece(entry)) { return false; } Plugin.Log.LogWarning((object)("Build ui is not ready yet and the hold back list is full; applying " + ((object)(*(KnowledgeEntry*)(&entry))/*cast due to .constrained prefix*/).ToString() + " without a popup.")); return _bridge.TryWrite(player, entry); } if (_bridge.TryInvokeAddKnownPiece(player, piece) && _bridge.IsKnown(player, entry)) { return true; } Plugin.WarnOnce("fallback-piece", "Vanilla AddKnownPiece is unavailable; using the direct write plus manual popup fallback."); if (!_bridge.TryWrite(player, entry)) { return false; } bool flag = (int)piece.m_category == 6 || (int)piece.m_category == 7 || (int)piece.m_category == 8; QueuePopup(piece.m_icon, flag ? "$msg_newdish" : "$msg_newpiece", ((KnowledgeEntry)(ref entry)).Key); return true; } private bool ApplyStation(Player player, KnowledgeEntry entry, bool showPopup) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) int level; bool flag = _bridge.TryGetStationLevel(player, ((KnowledgeEntry)(ref entry)).Key, out level); if (flag && level >= ((KnowledgeEntry)(ref entry)).Level) { return false; } if (!_bridge.TryWrite(player, entry)) { return false; } if (showPopup) { Sprite stationIcon = _catalog.GetStationIcon(((KnowledgeEntry)(ref entry)).Key); if (flag) { QueuePopup(stationIcon, "$msg_newstation_level", ((KnowledgeEntry)(ref entry)).Key + " $msg_level " + ((KnowledgeEntry)(ref entry)).Level); } else { QueuePopup(stationIcon, "$msg_newstation", ((KnowledgeEntry)(ref entry)).Key); } } _bridge.RefreshKnownRecipes(player); return true; } private static void QueuePopup(Sprite icon, string topic, string description) { MessageHud instance = MessageHud.instance; if ((Object)(object)instance != (Object)null) { instance.QueueUnlockMsg(icon, topic, description); } } } }
plugins/GroupRecipesWithPopups.Core.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("555maggot666")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 555maggot666")] [assembly: AssemblyDescription("Engine-independent logic for GroupRecipes with Popups (ledger, snapshot diff, popup planning and pacing).")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2+06394aaec1ffe4f68c5d1db7696aa7bede0559f9")] [assembly: AssemblyProduct("GroupRecipes with Popups")] [assembly: AssemblyTitle("GroupRecipesWithPopups.Core")] [assembly: AssemblyVersion("1.0.2.0")] namespace GroupRecipesWithPopups.Core; public static class GrwpPaths { private const int MaxComponentLength = 64; private const string FallbackName = "world"; public static string SanitizeFileComponent(string name) { if (string.IsNullOrWhiteSpace(name)) { return "world"; } StringBuilder stringBuilder = new StringBuilder(name.Length); string text = name.Trim(); foreach (char c in text) { bool flag = c < ' ' || c == '\u007f' || c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' || char.IsControl(c); stringBuilder.Append(flag ? '_' : c); } string text2 = stringBuilder.ToString().Trim().TrimEnd('.', ' '); while (text2.StartsWith(".", StringComparison.Ordinal)) { text2 = text2.Substring(1); } if (text2.Length > 64) { text2 = text2.Substring(0, 64); if (text2.Length > 0 && char.IsHighSurrogate(text2[text2.Length - 1])) { text2 = text2.Substring(0, text2.Length - 1); } text2 = text2.TrimEnd('.', ' '); } if (text2.Length != 0) { return text2; } return "world"; } public static string LedgerFileName(string worldName, long worldUid) { return SanitizeFileComponent(worldName) + "_" + worldUid.ToString(CultureInfo.InvariantCulture) + ".txt"; } } public readonly struct KnowledgeEntry : IEquatable<KnowledgeEntry> { private sealed class IdentityEqualityComparer : IEqualityComparer<KnowledgeEntry> { public bool Equals(KnowledgeEntry x, KnowledgeEntry y) { return x.HasSameIdentity(y); } public int GetHashCode(KnowledgeEntry obj) { return HashCode.Combine((int)obj.Kind, (obj.Key != null) ? StringComparer.Ordinal.GetHashCode(obj.Key) : 0); } } public KnowledgeKind Kind { get; } public string Key { get; } public int Level { get; } public bool IsEmpty => Key == null; public static IEqualityComparer<KnowledgeEntry> IdentityComparer { get; } = new IdentityEqualityComparer(); public KnowledgeEntry(KnowledgeKind kind, string key, int level = 0) { if (key == null) { throw new ArgumentNullException("key"); } Kind = kind; Key = key; Level = ((kind == KnowledgeKind.Station) ? Math.Max(0, level) : 0); } public bool HasSameIdentity(KnowledgeEntry other) { if (Kind == other.Kind) { return string.Equals(Key, other.Key, StringComparison.Ordinal); } return false; } public bool Equals(KnowledgeEntry other) { if (HasSameIdentity(other)) { return Level == other.Level; } return false; } public override bool Equals(object obj) { if (obj is KnowledgeEntry other) { return Equals(other); } return false; } public override int GetHashCode() { return HashCode.Combine((int)Kind, (Key != null) ? StringComparer.Ordinal.GetHashCode(Key) : 0, Level); } public static bool operator ==(KnowledgeEntry left, KnowledgeEntry right) { return left.Equals(right); } public static bool operator !=(KnowledgeEntry left, KnowledgeEntry right) { return !left.Equals(right); } public override string ToString() { if (Kind != KnowledgeKind.Station) { return Kind.ToString() + ":" + Key; } return Kind.ToString() + ":" + Key + "@" + Level; } } public enum KnowledgeKind { Material = 1, Recipe, Piece, Station } public sealed class KnowledgeSnapshot { private readonly HashSet<string> _materials = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> _recipes = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> _pieces = new HashSet<string>(StringComparer.Ordinal); private readonly Dictionary<string, int> _stations = new Dictionary<string, int>(StringComparer.Ordinal); public bool Knows(KnowledgeEntry entry) { if (entry.IsEmpty) { return true; } switch (entry.Kind) { case KnowledgeKind.Material: return _materials.Contains(entry.Key); case KnowledgeKind.Recipe: return _recipes.Contains(entry.Key); case KnowledgeKind.Piece: return _pieces.Contains(entry.Key); case KnowledgeKind.Station: { if (_stations.TryGetValue(entry.Key, out var value)) { return value >= entry.Level; } return false; } default: return true; } } public void Record(KnowledgeEntry entry) { if (entry.IsEmpty) { return; } switch (entry.Kind) { case KnowledgeKind.Material: _materials.Add(entry.Key); break; case KnowledgeKind.Recipe: _recipes.Add(entry.Key); break; case KnowledgeKind.Piece: _pieces.Add(entry.Key); break; case KnowledgeKind.Station: { if (!_stations.TryGetValue(entry.Key, out var value) || value < entry.Level) { _stations[entry.Key] = entry.Level; } break; } } } public void RecordRange(IEnumerable<KnowledgeEntry> entries) { if (entries == null) { throw new ArgumentNullException("entries"); } foreach (KnowledgeEntry entry in entries) { Record(entry); } } public void Clear() { _materials.Clear(); _recipes.Clear(); _pieces.Clear(); _stations.Clear(); } public List<KnowledgeEntry> Diff(IEnumerable<KnowledgeEntry> current) { if (current == null) { throw new ArgumentNullException("current"); } List<KnowledgeEntry> list = new List<KnowledgeEntry>(); Dictionary<KnowledgeEntry, int> dictionary = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer); foreach (KnowledgeEntry item in current) { if (Knows(item)) { continue; } if (dictionary.TryGetValue(item, out var value)) { if (item.Level > list[value].Level) { list[value] = item; } } else { dictionary.Add(item, list.Count); list.Add(item); } } return list; } } public sealed class Ledger { private readonly List<KnowledgeEntry> _entries = new List<KnowledgeEntry>(); private readonly Dictionary<KnowledgeEntry, int> _indexByIdentity = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer); public int Count => _entries.Count; public IReadOnlyList<KnowledgeEntry> Entries => _entries; public bool Contains(KnowledgeEntry entry) { if (entry.IsEmpty || !_indexByIdentity.TryGetValue(entry, out var value)) { return false; } if (entry.Kind == KnowledgeKind.Station) { return _entries[value].Level >= entry.Level; } return true; } public bool TryGetStationLevel(string stationKey, out int level) { if (stationKey != null && _indexByIdentity.TryGetValue(new KnowledgeEntry(KnowledgeKind.Station, stationKey), out var value)) { level = _entries[value].Level; return true; } level = 0; return false; } public bool Add(KnowledgeEntry entry) { if (entry.IsEmpty || entry.Key.Length == 0) { return false; } if (_indexByIdentity.TryGetValue(entry, out var value)) { if (entry.Kind == KnowledgeKind.Station && entry.Level > _entries[value].Level) { _entries[value] = entry; return true; } return false; } _indexByIdentity.Add(entry, _entries.Count); _entries.Add(entry); return true; } public List<KnowledgeEntry> AddRange(IEnumerable<KnowledgeEntry> entries) { if (entries == null) { throw new ArgumentNullException("entries"); } List<KnowledgeEntry> list = new List<KnowledgeEntry>(); foreach (KnowledgeEntry entry in entries) { if (Add(entry)) { list.Add(entry); } } return list; } } public sealed class LedgerFile { private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private bool _readOnly; public string Path { get; } public bool IsReadOnly => _readOnly; public LedgerFile(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path must not be empty.", "path"); } Path = path; } public LedgerParseResult Load(Ledger ledger) { if (ledger == null) { throw new ArgumentNullException("ledger"); } if (!File.Exists(Path)) { return new LedgerParseResult(); } LedgerParseResult ledgerParseResult = LedgerFileFormat.Parse(File.ReadAllLines(Path, Encoding.UTF8)); if (!ledgerParseResult.IsSupported) { _readOnly = true; return ledgerParseResult; } ledger.AddRange(ledgerParseResult.Entries); return ledgerParseResult; } public bool Append(IEnumerable<KnowledgeEntry> entries) { if (entries == null) { throw new ArgumentNullException("entries"); } if (_readOnly) { return false; } StringBuilder stringBuilder = new StringBuilder(); int num = 0; foreach (KnowledgeEntry entry in entries) { if (!entry.IsEmpty && entry.Key.Length != 0) { stringBuilder.Append(LedgerFileFormat.FormatLine(entry)).Append('\n'); num++; } } if (num == 0) { return false; } string directoryName = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(Path)); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } using (FileStream fileStream = new FileStream(Path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read)) { string text = string.Empty; if (fileStream.Length == 0L) { text = LedgerFileFormat.Header + "\n"; } else { fileStream.Seek(-1L, SeekOrigin.End); if (fileStream.ReadByte() != 10) { text = "\n"; } } fileStream.Seek(0L, SeekOrigin.End); byte[] bytes = Utf8NoBom.GetBytes(text + stringBuilder); fileStream.Write(bytes, 0, bytes.Length); } return true; } } public sealed class LedgerParseResult { public List<KnowledgeEntry> Entries { get; } = new List<KnowledgeEntry>(); public List<string> Warnings { get; } = new List<string>(); public bool IsSupported { get; set; } = true; } public static class LedgerFileFormat { public const int Version = 1; public const string HeaderPrefix = "#grwp-ledger"; public static readonly string Header = "#grwp-ledger v" + 1.ToString(CultureInfo.InvariantCulture); public static string Escape(string value) { if (value == null) { throw new ArgumentNullException("value"); } StringBuilder stringBuilder = null; for (int i = 0; i < value.Length; i++) { char c = value[i]; string text = c switch { '\\' => "\\\\", '\t' => "\\t", '\n' => "\\n", '\r' => "\\r", _ => null, }; if (text == null) { stringBuilder?.Append(c); continue; } if (stringBuilder == null) { stringBuilder = new StringBuilder(value.Length + 8); stringBuilder.Append(value, 0, i); } stringBuilder.Append(text); } if (stringBuilder != null) { return stringBuilder.ToString(); } return value; } public static bool TryUnescape(string value, out string result) { result = null; if (value == null) { return false; } if (value.IndexOf('\\') < 0) { result = value; return true; } StringBuilder stringBuilder = new StringBuilder(value.Length); for (int i = 0; i < value.Length; i++) { char c = value[i]; if (c != '\\') { stringBuilder.Append(c); continue; } if (i + 1 >= value.Length) { return false; } i++; switch (value[i]) { case '\\': stringBuilder.Append('\\'); break; case 't': stringBuilder.Append('\t'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; default: return false; } } result = stringBuilder.ToString(); return true; } public static string FormatLine(KnowledgeEntry entry) { if (entry.IsEmpty) { throw new ArgumentException("Cannot format an empty entry.", "entry"); } return KindToText(entry.Kind) + "\t" + Escape(entry.Key) + "\t" + entry.Level.ToString(CultureInfo.InvariantCulture); } public static bool TryParseLine(string line, out KnowledgeEntry entry, out string error) { entry = default(KnowledgeEntry); error = null; if (line == null) { error = "line is null"; return false; } if (line.EndsWith("\r", StringComparison.Ordinal)) { line = line.Substring(0, line.Length - 1); } string[] array = line.Split('\t'); if (array.Length != 3) { error = "expected 3 tab separated fields but found " + array.Length.ToString(CultureInfo.InvariantCulture); return false; } if (!TryParseKind(array[0], out var kind)) { error = "unknown kind '" + array[0] + "'"; return false; } if (!TryUnescape(array[1], out var result)) { error = "invalid escape sequence in key"; return false; } if (result.Length == 0) { error = "empty key"; return false; } if (!int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out var result2)) { error = "invalid level '" + array[2] + "'"; return false; } entry = new KnowledgeEntry(kind, result, result2); return true; } public static LedgerParseResult Parse(IEnumerable<string> lines) { if (lines == null) { throw new ArgumentNullException("lines"); } LedgerParseResult ledgerParseResult = new LedgerParseResult(); int num = 0; foreach (string line in lines) { string text = line; num++; if (text == null) { text = string.Empty; } string text2 = text; if (text2.Length > 0 && text2[0] == '\ufeff') { text2 = text2.Substring(1); } if (text2.Trim().Length == 0) { continue; } KnowledgeEntry entry; string error; if (text2[0] == '#') { if (text2.StartsWith("#grwp-ledger", StringComparison.Ordinal) && !IsSupportedHeader(text2)) { ledgerParseResult.IsSupported = false; ledgerParseResult.Warnings.Add("line " + num.ToString(CultureInfo.InvariantCulture) + ": unsupported ledger header '" + text2 + "'"); return ledgerParseResult; } } else if (TryParseLine(text2, out entry, out error)) { ledgerParseResult.Entries.Add(entry); } else { ledgerParseResult.Warnings.Add("line " + num.ToString(CultureInfo.InvariantCulture) + ": " + error); } } return ledgerParseResult; } private static bool IsSupportedHeader(string line) { return string.Equals(line.TrimEnd(), Header, StringComparison.Ordinal); } private static string KindToText(KnowledgeKind kind) { return kind switch { KnowledgeKind.Material => "Material", KnowledgeKind.Recipe => "Recipe", KnowledgeKind.Piece => "Piece", KnowledgeKind.Station => "Station", _ => throw new ArgumentOutOfRangeException("kind", kind, "Unknown knowledge kind."), }; } private static bool TryParseKind(string text, out KnowledgeKind kind) { switch (text) { case "Material": kind = KnowledgeKind.Material; return true; case "Recipe": kind = KnowledgeKind.Recipe; return true; case "Piece": kind = KnowledgeKind.Piece; return true; case "Station": kind = KnowledgeKind.Station; return true; default: kind = (KnowledgeKind)0; return false; } } } public readonly struct PopupStep { public KnowledgeEntry Entry { get; } public bool ShowPopup { get; } public PopupStep(KnowledgeEntry entry, bool showPopup) { Entry = entry; ShowPopup = showPopup; } } public sealed class PopupPlan { public IReadOnlyList<PopupStep> Steps { get; } public int PopupCount { get; } public int HiddenCount { get; } public PopupPlan(IReadOnlyList<PopupStep> steps) { Steps = steps ?? throw new ArgumentNullException("steps"); int num = 0; for (int i = 0; i < steps.Count; i++) { if (steps[i].ShowPopup) { num++; } } PopupCount = num; HiddenCount = steps.Count - num; } } public static class PopupPlanner { public const int Unlimited = int.MaxValue; public static PopupPlan BuildPlan(IEnumerable<KnowledgeEntry> pending, int maxPopups) { if (pending == null) { throw new ArgumentNullException("pending"); } List<KnowledgeEntry> list = new List<KnowledgeEntry>(); Dictionary<KnowledgeEntry, int> dictionary = new Dictionary<KnowledgeEntry, int>(KnowledgeEntry.IdentityComparer); foreach (KnowledgeEntry item in pending) { if (item.IsEmpty) { continue; } if (dictionary.TryGetValue(item, out var value)) { if (item.Level > list[value].Level) { list[value] = item; } } else { dictionary.Add(item, list.Count); list.Add(item); } } List<KnowledgeEntry> list2 = new List<KnowledgeEntry>(list.Count); for (int i = 0; i <= 2; i++) { foreach (KnowledgeEntry item2 in list) { if (RankOf(item2.Kind) == i) { list2.Add(item2); } } } int num = Math.Max(0, maxPopups); List<PopupStep> list3 = new List<PopupStep>(list2.Count); for (int j = 0; j < list2.Count; j++) { list3.Add(new PopupStep(list2[j], j < num)); } return new PopupPlan(list3); } private static int RankOf(KnowledgeKind kind) { switch (kind) { case KnowledgeKind.Material: return 0; case KnowledgeKind.Recipe: case KnowledgeKind.Piece: return 1; default: return 2; } } } public sealed class PopupPump { private const double Epsilon = 1E-06; private readonly Queue<PopupStep> _queue = new Queue<PopupStep>(); private readonly double _interval; private double _elapsed; public int Remaining => _queue.Count; public bool IsEmpty => _queue.Count == 0; public PopupPump(PopupPlan plan, double intervalSeconds) : this(intervalSeconds) { if (plan == null) { throw new ArgumentNullException("plan"); } Enqueue(plan); } public PopupPump(double intervalSeconds) { _interval = Math.Max(0.0, intervalSeconds); } public void Enqueue(PopupPlan plan) { if (plan == null) { throw new ArgumentNullException("plan"); } foreach (PopupStep step in plan.Steps) { _queue.Enqueue(step); } } public void Clear() { _queue.Clear(); _elapsed = 0.0; } public void Tick(double deltaSeconds) { if (!(deltaSeconds <= 0.0) && _queue.Count != 0) { _elapsed += deltaSeconds; } } public bool TryDequeue(out KnowledgeEntry entry, out bool showPopup) { entry = default(KnowledgeEntry); showPopup = false; if (_queue.Count == 0 || _elapsed + 1E-06 < _interval) { return false; } PopupStep popupStep = _queue.Dequeue(); entry = popupStep.Entry; showPopup = popupStep.ShowPopup; _elapsed = Math.Min(Math.Max(0.0, _elapsed - _interval), _interval); if (_queue.Count == 0) { _elapsed = 0.0; } return true; } public bool TryDequeueSilent(out KnowledgeEntry entry) { entry = default(KnowledgeEntry); if (_queue.Count == 0 || _queue.Peek().ShowPopup) { return false; } entry = _queue.Dequeue().Entry; if (_queue.Count == 0) { _elapsed = 0.0; } return true; } }