Decompiled source of HTF MoreHead v0.6.3
HTFMoreHead.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using FishNet.Broadcast; using FishNet.Connection; using FishNet.Managing; using FishNet.Object; using FishNet.Serializing; using FishNet.Transporting; using HarmonyLib; using HowToFish.MoreHead.Bootstrap; using HowToFish.MoreHead.Content; using HowToFish.MoreHead.Diagnostics; using HowToFish.MoreHead.Networking; using HowToFish.MoreHead.Persistence; using HowToFish.MoreHead.Players; using HowToFish.MoreHead.Visuals; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [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("HTFMoreHead")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Custom cosmetic framework for How to Fish")] [assembly: AssemblyFileVersion("0.6.3.0")] [assembly: AssemblyInformationalVersion("0.6.3+06517c320af77ef3e7ad1f01838d4b612fc44623")] [assembly: AssemblyProduct("HTFMoreHead")] [assembly: AssemblyTitle("HTFMoreHead")] [assembly: AssemblyVersion("0.6.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace HowToFish.MoreHead { [BepInPlugin("com.htfmodding.morehead", "HTF MoreHead", "0.6.3")] public sealed class MoreHeadPlugin : BaseUnityPlugin { private const int MainThreadQueueCapacity = 128; private const int MainThreadActionsPerFrame = 32; private Harmony _harmony; private MainThreadDispatcher _dispatcher; private BoundedLog _boundedLog; private PlayerProbeTracker _tracker; private CosmeticCatalog _catalog; private LocalLoadoutStore _localLoadoutStore; private LocalSkinPreviewVisuals _localSkinPreviewVisuals; private PlayerAppearanceService _appearanceService; private CosmeticNetworkService _networkService; private ConfigEntry<bool> _enableStage0Probe; private ConfigEntry<float> _probeTimeoutSeconds; private bool _shuttingDown; private int _probeDisableCleanupRequested; private int _probeSettingChangePending; private int _requestedProbeEnabled; internal static MoreHeadPlugin Instance { get; private set; } private void Awake() { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_0219: Expected O, but got Unknown Instance = this; _dispatcher = new MainThreadDispatcher(128); _boundedLog = new BoundedLog(((BaseUnityPlugin)this).Logger, 256); _enableStage0Probe = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "EnableStage0Probe", false, "Legacy development probe. Production 0.6.0 forces this off."); _enableStage0Probe.Value = false; _probeTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "ProbeTimeoutSeconds", 2f, new ConfigDescription("Maximum time to reconcile Player and PlayerSkin lifecycle signals.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 10f), Array.Empty<object>())); _requestedProbeEnabled = (_enableStage0Probe.Value ? 1 : 0); _tracker = new PlayerProbeTracker(((BaseUnityPlugin)this).Logger, _boundedLog, _probeTimeoutSeconds.Value); _catalog = new CosmeticCatalog(((BaseUnityPlugin)this).Logger, _boundedLog); _catalog.LoadAll(Paths.PluginPath); _localLoadoutStore = new LocalLoadoutStore(((BaseUnityPlugin)this).Logger, Path.Combine(Paths.ConfigPath, "HTFMoreHead", "loadout.json")); _localLoadoutStore.Load(); _localSkinPreviewVisuals = new LocalSkinPreviewVisuals(((BaseUnityPlugin)this).Logger, _catalog, _localLoadoutStore); _appearanceService = new PlayerAppearanceService(((BaseUnityPlugin)this).Logger, _boundedLog, _catalog); _networkService = new CosmeticNetworkService(((BaseUnityPlugin)this).Logger, _boundedLog, _catalog, _localLoadoutStore, _appearanceService); _localSkinPreviewVisuals.SelectionCommitted += OnLocalSelectionCommitted; try { RuntimeFingerprint.LogCurrent(((BaseUnityPlugin)this).Logger, ((BaseUnityPlugin)this).Info.Location); } catch (Exception arg) { ((BaseUnityPlugin)this).Logger.LogError((object)$"[HTFMoreHead][Stage0][FINGERPRINT] Runtime fingerprint failed safely: {arg}"); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"[HTFMoreHead][CREDITS] Thanks to Masaicker, creator of MoreHead, for permission to reference the original project: https://github.com/Masaicker/repo-MoreHead"); if (!ValidatePatchTargets()) { ((BaseUnityPlugin)this).Logger.LogError((object)"[HTFMoreHead][Stage0][PATCH] Required lifecycle methods were not found; stage 0 probes are disabled for this game build."); return; } SceneManager.sceneUnloaded += OnSceneUnloaded; _harmony = new Harmony("com.htfmodding.morehead"); try { _harmony.PatchAll(Assembly.GetExecutingAssembly()); } catch (Exception arg2) { SceneManager.sceneUnloaded -= OnSceneUnloaded; try { _harmony.UnpatchSelf(); } catch (Exception arg3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[HTFMoreHead][Stage0][PATCH] Cleanup after patch failure also failed: " + $"{arg3}")); } _harmony = null; ((BaseUnityPlugin)this).Logger.LogError((object)$"[HTFMoreHead][Stage0][PATCH] Lifecycle probes could not be installed: {arg2}"); return; } _enableStage0Probe.SettingChanged += OnStage0ProbeSettingChanged; ((BaseUnityPlugin)this).Logger.LogInfo((object)("[HTFMoreHead][Stage0][READY] Lifecycle probes installed. " + $"Enabled={_enableStage0Probe.Value}, MainThreadId={_dispatcher.MainThreadId}, " + $"Timeout={_probeTimeoutSeconds.Value:F1}s.")); } private void Update() { if (_shuttingDown || _dispatcher == null) { return; } try { ApplyPendingProbeSettingChange(); _dispatcher.Drain(32, delegate(Exception ex) { ReportPatchException("MainThreadQueue", ex); }); int num = _dispatcher.ConsumeDroppedCount(); if (num > 0) { _boundedLog.WarningOnce("main-thread-queue-overflow", "[HTFMoreHead][Stage0][THREAD] Main-thread queue overflowed; " + $"DroppedActions={num}, Capacity={128}."); } if (_enableStage0Probe != null && _enableStage0Probe.Value) { _tracker?.Tick(); } _networkService?.Tick(); } catch (Exception exception) { ReportPatchException("Update", exception); } } private void OnDestroy() { if (_shuttingDown) { return; } _shuttingDown = true; SceneManager.sceneUnloaded -= OnSceneUnloaded; if (_enableStage0Probe != null) { _enableStage0Probe.SettingChanged -= OnStage0ProbeSettingChanged; } try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception arg) { ((BaseUnityPlugin)this).Logger.LogWarning((object)$"[HTFMoreHead][Stage0][SHUTDOWN] Harmony unpatch failed: {arg}"); } try { if (_localSkinPreviewVisuals != null) { _localSkinPreviewVisuals.SelectionCommitted -= OnLocalSelectionCommitted; } _networkService?.ClearAndDetach(); _appearanceService?.ClearAll(); _localSkinPreviewVisuals?.Dispose(); _tracker?.ClearAll("PluginDestroy"); } catch (Exception arg2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)$"[HTFMoreHead][Stage0][SHUTDOWN] Tracker cleanup failed: {arg2}"); } finally { _dispatcher?.Clear(); _harmony = null; _tracker = null; _networkService = null; _appearanceService = null; _localSkinPreviewVisuals = null; _localLoadoutStore = null; _catalog?.UnloadAll(); _catalog = null; _dispatcher = null; if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } internal void DispatchPlayerStarted(Player player) { DispatchInfrastructure("PlayerStart", delegate { _appearanceService?.NotifyPlayerReady(player); _networkService?.NotifyPlayerReady(player); if (_enableStage0Probe != null && _enableStage0Probe.Value) { _tracker?.NotifyPlayerStarted(player); } }); } internal void DispatchSkinStarted(PlayerSkin skin) { Dispatch("SkinStart", delegate { _tracker?.NotifySkinStarted(skin); }); } internal void DispatchPlayerStopping(Player player) { DispatchInfrastructure("PlayerStop", delegate { _appearanceService?.NotifyPlayerStopping(player); if (_enableStage0Probe != null && _enableStage0Probe.Value) { _tracker?.NotifyPlayerStopping(player); } }); } internal void DispatchLocalSkinPreview(bool visible) { DispatchInfrastructure("LocalSkinPreview", delegate { if (visible) { _localSkinPreviewVisuals?.Show(); } else { _localSkinPreviewVisuals?.CloseAndSave(); } }); } internal void DispatchLocalSkinReady() { DispatchInfrastructure("LocalSkinReady", delegate { _localSkinPreviewVisuals?.RestorePersistedSelection(); }); } internal bool HandleNativeCosmeticArrow(CosmeticSlot slot, int amount) { try { return _localSkinPreviewVisuals != null && _localSkinPreviewVisuals.HandleArrow(slot, amount); } catch (Exception exception) { ReportPatchException("NativeCosmeticArrowPrefix", exception); return false; } } internal void NotifyNativeCosmeticArrowCompleted(CosmeticSlot slot, int amount, byte beforeIndex, byte afterIndex) { try { _localSkinPreviewVisuals?.OnNativeArrowCompleted(slot, amount, beforeIndex, afterIndex); } catch (Exception exception) { ReportPatchException("NativeCosmeticArrowPostfix", exception); } } internal void NotifyActiveCosmeticSlot(CosmeticSlot slot) { try { _localSkinPreviewVisuals?.SetActiveSlot(slot); } catch (Exception exception) { ReportPatchException("ActiveCosmeticSlot", exception); } } internal void ReportPatchException(string source, Exception exception) { if (!_shuttingDown && exception != null) { _boundedLog?.ErrorOnce("exception-" + source, $"[HTFMoreHead][Stage0][ERROR] {source} probe failed safely: {exception}"); } } private void Dispatch(string source, Action action) { if (_enableStage0Probe != null && _enableStage0Probe.Value) { DispatchInfrastructure(source, action); } } private void DispatchInfrastructure(string source, Action action) { if (_shuttingDown || _dispatcher == null || action == null) { return; } _dispatcher.RunOrEnqueue(delegate { try { action(); } catch (Exception exception) { ReportPatchException(source, exception); } }); } private void OnSceneUnloaded(Scene scene) { DispatchInfrastructure("SceneUnloaded", delegate { _tracker?.RequestPrune(); _appearanceService?.PruneDestroyedPlayers(); }); } private void OnLocalSelectionCommitted(CosmeticLoadout loadout) { _networkService?.RequestLocalEquip(loadout); } private void OnStage0ProbeSettingChanged(object sender, EventArgs eventArgs) { bool flag = _enableStage0Probe != null && _enableStage0Probe.Value; Volatile.Write(ref _requestedProbeEnabled, flag ? 1 : 0); if (!flag) { Interlocked.Exchange(ref _probeDisableCleanupRequested, 1); } Interlocked.Exchange(ref _probeSettingChangePending, 1); } private void ApplyPendingProbeSettingChange() { if (Interlocked.Exchange(ref _probeDisableCleanupRequested, 0) != 0) { _tracker?.ClearAll("ProbeDisabled"); } if (Interlocked.Exchange(ref _probeSettingChangePending, 0) != 0) { bool flag = Volatile.Read(in _requestedProbeEnabled) != 0; ((BaseUnityPlugin)this).Logger.LogInfo((object)$"[HTFMoreHead][Stage0][CONFIG] EnableStage0Probe={flag}."); } } private bool ValidatePatchTargets() { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(Player), "OnStartClient", Type.EmptyTypes, (Type[])null); MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(PlayerSkin), "OnStartClient", Type.EmptyTypes, (Type[])null); MethodInfo methodInfo3 = AccessTools.DeclaredMethod(typeof(Player), "OnStopClient", Type.EmptyTypes, (Type[])null); bool result = methodInfo != null && methodInfo2 != null && methodInfo3 != null; ((BaseUnityPlugin)this).Logger.LogInfo((object)($"[HTFMoreHead][Stage0][PATCH_CHECK] PlayerStart={methodInfo != null}, " + $"SkinStart={methodInfo2 != null}, PlayerStop={methodInfo3 != null}, " + $"ThreadId={Thread.CurrentThread.ManagedThreadId}.")); return result; } } internal static class RuntimeMetadata { internal const string PluginGuid = "com.htfmodding.morehead"; internal const string PluginName = "HTF MoreHead"; internal const string PluginVersion = "0.6.3"; internal const byte ProtocolVersion = 3; internal const string ExpectedGameVersion = "1.0.11"; internal const string ExpectedAssemblyCSharpSha256 = "349126D96D66B290341CBDA1628C746995210097C1A415652FA8961D9A20CFA6"; internal const string ExpectedAssemblyCSharpMvid = "a79eca11-9a20-409b-ab30-d5f7ec1cd857"; internal const string ExpectedFishNetSha256 = "71AC44A95C7AF0CD9333C851F41792EF342B373AF26E10F69795E02C9550C61A"; internal const string ExpectedFishNetMvid = "5254e175-6a7c-489e-beb8-55179113ab9f"; } } namespace HowToFish.MoreHead.Visuals { internal sealed class LocalSkinPreviewVisuals { private static readonly CosmeticSlot[] AllSlots = new CosmeticSlot[3] { CosmeticSlot.Hat, CosmeticSlot.Accessory, CosmeticSlot.Outfit }; private static readonly FieldInfo LocalSkinInstanceField = AccessTools.Field(typeof(LocalSkin), "_instance"); private static readonly FieldInfo LocalHatRendererField = AccessTools.Field(typeof(LocalSkin), "_hatRenderer"); private static readonly FieldInfo LocalAccessoryRendererField = AccessTools.Field(typeof(LocalSkin), "_accessoryRenderer"); private static readonly FieldInfo LocalOutfitRendererField = AccessTools.Field(typeof(LocalSkin), "_outfitRenderer"); private static readonly FieldInfo LocalHatIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalHatIndex>k__BackingField"); private static readonly FieldInfo LocalAccessoryIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalAccessoryIndex>k__BackingField"); private static readonly FieldInfo LocalOutfitIndexField = AccessTools.Field(typeof(LocalSkin), "<LocalOutfitIndex>k__BackingField"); private static readonly FieldInfo CanvasManagerInstanceField = AccessTools.Field(typeof(CanvasManager), "_instance"); private static readonly FieldInfo VersionTextField = AccessTools.Field(typeof(CanvasManager), "_versionText"); private readonly ManualLogSource _logger; private readonly CosmeticCatalog _catalog; private readonly LocalLoadoutStore _loadoutStore; private readonly GameObject[] _instances = (GameObject[])(object)new GameObject[3]; private readonly int[] _customIndices = new int[3] { -1, -1, -1 }; private readonly TextMeshProUGUI[] _selectionTitles = (TextMeshProUGUI[])(object)new TextMeshProUGUI[3]; private readonly bool[] _selectorFallbackLogged = new bool[3]; private CosmeticLoadout _selected; private bool _previewOpen; internal event Action<CosmeticLoadout> SelectionCommitted; internal LocalSkinPreviewVisuals(ManualLogSource logger, CosmeticCatalog catalog, LocalLoadoutStore loadoutStore) { _logger = logger ?? throw new ArgumentNullException("logger"); _catalog = catalog ?? throw new ArgumentNullException("catalog"); _loadoutStore = loadoutStore ?? throw new ArgumentNullException("loadoutStore"); _selected = _loadoutStore.GetLoadout(); } internal void Show() { _previewOpen = true; DestroyAllCustom(); _selected = _loadoutStore.GetLoadout(); ApplySavedSelections(); RefreshSelectionTitles(); } internal void RestorePersistedSelection() { _selected = _loadoutStore.GetLoadout(); DestroyAllCustom(); ApplySavedSelections(); HideSelectionTitles(); _logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Restored Hat='" + DisplayId(_selected.HatId) + "', Accessory='" + DisplayId(_selected.AccessoryId) + "', Outfit='" + DisplayId(_selected.OutfitId) + "'.")); } internal void SetActiveSlot(CosmeticSlot slot) { if (_previewOpen) { RefreshSelectionTitle(slot); } } internal bool HandleArrow(CosmeticSlot slot, int amount) { int count = _catalog.GetCount(slot); if (!_previewOpen || _customIndices[(uint)slot] < 0 || count == 0 || amount == 0) { return false; } int num = ((amount > 0) ? 1 : (-1)); int num2 = _customIndices[(uint)slot] + num; if (num2 >= 0 && num2 < count) { _customIndices[(uint)slot] = num2; ShowDefinition(slot, _catalog.GetAt(slot, num2), showTitle: true); ColorPicker.UpdateSelectedPicker(); return true; } ExitCustomToNative(slot, num); return true; } internal void OnNativeArrowCompleted(CosmeticSlot slot, int amount, byte beforeIndex, byte afterIndex) { int count = _catalog.GetCount(slot); if (_previewOpen && _customIndices[(uint)slot] < 0 && count != 0 && amount != 0) { _selected.Set(slot, string.Empty); bool flag = amount > 0 && afterIndex <= beforeIndex; bool flag2 = amount < 0 && afterIndex >= beforeIndex; if (!flag && !flag2) { RefreshSelectionTitle(slot); return; } _customIndices[(uint)slot] = ((!flag) ? (count - 1) : 0); ShowDefinition(slot, _catalog.GetAt(slot, _customIndices[(uint)slot]), showTitle: true); _logger.LogInfo((object)($"[HTFMoreHead][NATIVE_LIST] Entered custom {slot}. Direction={amount}, " + $"NativeBefore={beforeIndex}, NativeAfter={afterIndex}, CustomIndex={_customIndices[(uint)slot]}.")); } } internal void CloseAndSave() { _previewOpen = false; HideSelectionTitles(); _loadoutStore.Save(_selected); this.SelectionCommitted?.Invoke(_selected.Clone()); } internal void Dispose() { _loadoutStore.Save(_selected); _previewOpen = false; DestroyAllCustom(); DestroySelectionTitles(); } private void ApplySavedSelections() { for (int i = 0; i < AllSlots.Length; i++) { CosmeticSlot cosmeticSlot = AllSlots[i]; int index = _catalog.GetIndex(cosmeticSlot, _selected.Get(cosmeticSlot)); _customIndices[(uint)cosmeticSlot] = index; if (index >= 0) { ShowDefinition(cosmeticSlot, _catalog.GetAt(cosmeticSlot, index), showTitle: false); } else { _selected.Set(cosmeticSlot, string.Empty); } } } private void ShowDefinition(CosmeticSlot slot, CosmeticDefinition definition, bool showTitle) { //IL_007c: 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_00b2: Unknown result type (might be due to invalid IL or missing references) if (definition == null || definition.Slot != slot) { return; } DestroyCustom(slot); SetNativeBackingToNone(slot); Transform val = ResolvePreviewTarget(slot); if ((Object)(object)val == (Object)null) { _logger.LogWarning((object)$"[HTFMoreHead][PREVIEW] {slot} preview target is unavailable."); return; } GameObject val2 = Object.Instantiate<GameObject>(definition.Prefab, val, false); ((Object)val2).name = "HTFMoreHead_Preview_" + definition.Id; val2.transform.localPosition = definition.Entry.localPosition.ToVector3(); val2.transform.localEulerAngles = definition.Entry.localEulerAngles.ToVector3(); val2.transform.localScale = definition.Entry.localScale.ToVector3(); SetLayerRecursively(val2.transform, ((Component)val).gameObject.layer); _instances[(uint)slot] = val2; _selected.Set(slot, definition.Id); if (showTitle) { ShowSelectionTitle(slot, definition); } } private void RefreshSelectionTitles() { for (int i = 0; i < AllSlots.Length; i++) { RefreshSelectionTitle(AllSlots[i]); } } private void RefreshSelectionTitle(CosmeticSlot slot) { int index = _customIndices[(uint)slot]; ShowSelectionTitle(slot, _catalog.GetAt(slot, index)); } private void ShowSelectionTitle(CosmeticSlot slot, CosmeticDefinition definition) { if (definition == null) { HideSelectionTitle(slot); return; } TextMeshProUGUI val = EnsureSelectionTitle(slot); if (!((Object)(object)val == (Object)null)) { string text = (string.IsNullOrWhiteSpace(definition.Entry.displayName) ? definition.Id : definition.Entry.displayName.Trim()); string text2 = definition.AuthorName?.Trim(); ((TMP_Text)val).text = (string.IsNullOrEmpty(text2) ? text : (text + " By " + text2)); ((Component)val).gameObject.SetActive(true); ((TMP_Text)val).transform.SetAsLastSibling(); } } private TextMeshProUGUI EnsureSelectionTitle(CosmeticSlot slot) { //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01c2: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) int num = (int)slot; if ((Object)(object)_selectionTitles[num] != (Object)null) { AlignSelectionTitle(slot, _selectionTitles[num]); return _selectionTitles[num]; } object obj = CanvasManagerInstanceField?.GetValue(null); TextMeshProUGUI val = (TextMeshProUGUI)((obj != null) ? /*isinst with value type is only supported in some contexts*/: null); Canvas val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponentInParent<Canvas>() : null); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null) { _logger.LogWarning((object)"[HTFMoreHead][TITLE] Full-screen Canvas is unavailable."); return null; } val2 = val2.rootCanvas; GameObject val3 = new GameObject("HTFMoreHead_" + slot.ToString() + "SelectionTitle", new Type[3] { typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI) }) { layer = ((Component)val).gameObject.layer }; val3.transform.SetParent(((Component)val2).transform, false); TextMeshProUGUI component = val3.GetComponent<TextMeshProUGUI>(); _selectionTitles[num] = component; ((TMP_Text)component).font = ((TMP_Text)val).font; ((TMP_Text)component).fontSharedMaterial = ((TMP_Text)val).fontSharedMaterial; ((TMP_Text)component).fontSize = 30f; ((TMP_Text)component).fontStyle = (FontStyles)1; ((TMP_Text)component).alignment = (TextAlignmentOptions)513; ((TMP_Text)component).textWrappingMode = (TextWrappingModes)0; ((TMP_Text)component).overflowMode = (TextOverflowModes)1; ((Graphic)component).raycastTarget = false; ((TMP_Text)component).enableVertexGradient = true; ((TMP_Text)component).colorGradient = new VertexGradient(new Color(1f, 0.25f, 0.35f), new Color(1f, 0.85f, 0.2f), new Color(0.2f, 0.9f, 1f), new Color(0.85f, 0.3f, 1f)); AlignSelectionTitle(slot, component); return component; } private void AlignSelectionTitle(CosmeticSlot slot, TextMeshProUGUI title) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: 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) RectTransform rectTransform = ((TMP_Text)title).rectTransform; Canvas componentInParent = ((Component)title).GetComponentInParent<Canvas>(); Canvas val = ((componentInParent != null) ? componentInParent.rootCanvas : null); if ((Object)(object)val != (Object)null && TryGetSelectorRightEdge(slot, val, out var localRightEdge)) { Transform transform = ((Component)val).transform; RectTransform val2 = (RectTransform)(object)((transform is RectTransform) ? transform : null); float num; if (!((Object)(object)val2 != (Object)null)) { num = 340f; } else { Rect rect = val2.rect; num = ((Rect)(ref rect)).xMax - localRightEdge.x - 18f; } float num2 = num; rectTransform.anchorMin = new Vector2(0.5f, 0.5f); rectTransform.anchorMax = new Vector2(0.5f, 0.5f); rectTransform.pivot = new Vector2(0f, 0.5f); rectTransform.anchoredPosition = localRightEdge + new Vector2(18f, 0f); rectTransform.sizeDelta = new Vector2(Mathf.Clamp(num2, 180f, 420f), 52f); } else { ApplyFallbackTitlePosition(slot, rectTransform); if (!_selectorFallbackLogged[(uint)slot]) { _selectorFallbackLogged[(uint)slot] = true; _logger.LogWarning((object)$"[HTFMoreHead][TITLE] Could not resolve native {slot} arrow row; using fallback position."); } } } private static bool TryGetSelectorRightEdge(CosmeticSlot slot, Canvas canvas, out Vector2 localRightEdge) { //IL_0001: 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_0167: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) localRightEdge = default(Vector2); Transform transform = ((Component)canvas).transform; RectTransform val = (RectTransform)(object)((transform is RectTransform) ? transform : null); if ((Object)(object)val == (Object)null) { return false; } string selectorMethodName = GetSelectorMethodName(slot); Camera val2 = (((int)canvas.renderMode == 0) ? null : canvas.worldCamera); Button[] array = Object.FindObjectsByType<Button>((FindObjectsInactive)1); float num = float.NegativeInfinity; float num2 = 0f; bool flag = false; Vector3[] array2 = (Vector3[])(object)new Vector3[4]; foreach (Button val3 in array) { Canvas val4 = (((Object)(object)val3 != (Object)null) ? ((Component)val3).GetComponentInParent<Canvas>() : null); if ((Object)(object)val3 == (Object)null || !((Component)val3).gameObject.activeInHierarchy || (Object)(object)val4 == (Object)null || (Object)(object)val4.rootCanvas != (Object)(object)canvas || !InvokesMethod(val3, selectorMethodName)) { continue; } Transform transform2 = ((Component)val3).transform; RectTransform val5 = (RectTransform)(object)((transform2 is RectTransform) ? transform2 : null); if ((Object)(object)val5 == (Object)null) { continue; } val5.GetWorldCorners(array2); Rect rect = val5.rect; Vector2 val6 = RectTransformUtility.WorldToScreenPoint(val2, ((Transform)val5).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center))); for (int j = 0; j < array2.Length; j++) { Vector2 val7 = RectTransformUtility.WorldToScreenPoint(val2, array2[j]); if (!(val7.x <= num)) { num = val7.x; num2 = val6.y; flag = true; } } } if (flag) { return RectTransformUtility.ScreenPointToLocalPointInRectangle(val, new Vector2(num, num2), val2, ref localRightEdge); } return false; } private static bool InvokesMethod(Button button, string methodName) { ButtonClickedEvent onClick = button.onClick; for (int i = 0; i < ((UnityEventBase)onClick).GetPersistentEventCount(); i++) { if (string.Equals(((UnityEventBase)onClick).GetPersistentMethodName(i), methodName, StringComparison.Ordinal)) { return true; } } return false; } private static string GetSelectorMethodName(CosmeticSlot slot) { return slot switch { CosmeticSlot.Accessory => "ChangeAccessoryIndex", CosmeticSlot.Outfit => "ChangeOutfitIndex", _ => "ChangeHatIndex", }; } private static void ApplyFallbackTitlePosition(CosmeticSlot slot, RectTransform rect) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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) float num = slot switch { CosmeticSlot.Accessory => 0.56f, CosmeticSlot.Outfit => 0.39f, _ => 0.67f, }; rect.anchorMin = new Vector2(0.81f, num); rect.anchorMax = new Vector2(0.81f, num); rect.pivot = new Vector2(0f, 0.5f); rect.anchoredPosition = Vector2.zero; rect.sizeDelta = new Vector2(340f, 52f); } private void HideSelectionTitle(CosmeticSlot slot) { TextMeshProUGUI val = _selectionTitles[(uint)slot]; if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(false); } } private void HideSelectionTitles() { for (int i = 0; i < AllSlots.Length; i++) { HideSelectionTitle(AllSlots[i]); } } private void DestroySelectionTitles() { for (int i = 0; i < _selectionTitles.Length; i++) { if ((Object)(object)_selectionTitles[i] != (Object)null) { Object.Destroy((Object)(object)((Component)_selectionTitles[i]).gameObject); } _selectionTitles[i] = null; _selectorFallbackLogged[i] = false; } } private Transform ResolvePreviewTarget(CosmeticSlot slot) { switch (slot) { case CosmeticSlot.Accessory: { Renderer obj2 = ResolveNativeRenderer(CosmeticSlot.Accessory); if (obj2 == null) { return null; } return ((Component)obj2).transform; } case CosmeticSlot.Outfit: { Renderer obj = ResolveNativeRenderer(CosmeticSlot.Outfit); if (obj == null) { return null; } return ((Component)obj).transform; } default: { Transform hatTarget = LocalSkin.HatTarget; if ((Object)(object)hatTarget == (Object)null) { return null; } Transform parent = hatTarget.parent; Transform val = (((Object)(object)parent != (Object)null) ? parent.Find("Armature/Body/Head") : null); if (!((Object)(object)val != (Object)null)) { return hatTarget; } return val; } } } private void ExitCustomToNative(CosmeticSlot slot, int direction) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) DestroyCustom(slot); _customIndices[(uint)slot] = -1; _selected.Set(slot, string.Empty); HideSelectionTitle(slot); if (direction < 0) { LocalSkin.ChangeMesh(-1, ToPlayerSkinType(slot)); } ColorPicker.UpdateSelectedPicker(); _logger.LogInfo((object)($"[HTFMoreHead][NATIVE_LIST] Returned to native {slot}. " + $"Direction={direction}, NativeIndex={GetNativeIndex(slot)}.")); } private static Renderer ResolveNativeRenderer(CosmeticSlot slot) { object obj = LocalSkinInstanceField?.GetValue(null); if (obj == null) { return null; } return (Renderer)(slot switch { CosmeticSlot.Accessory => (object)/*isinst with value type is only supported in some contexts*/, CosmeticSlot.Outfit => (object)/*isinst with value type is only supported in some contexts*/, _ => (object)/*isinst with value type is only supported in some contexts*/, }); } private static void SetNativeBackingToNone(CosmeticSlot slot) { Renderer obj = ResolveNativeRenderer(slot); SkinnedMeshRenderer val = (SkinnedMeshRenderer)(object)((obj is SkinnedMeshRenderer) ? obj : null); switch (slot) { case CosmeticSlot.Accessory: LocalAccessoryIndexField?.SetValue(null, (byte)0); if ((Object)(object)val != (Object)null) { val.sharedMesh = SkinManager.GetAccessory((byte)0); } break; case CosmeticSlot.Outfit: LocalOutfitIndexField?.SetValue(null, (byte)0); if ((Object)(object)val != (Object)null) { val.sharedMesh = SkinManager.GetOutfit((byte)0); } break; default: LocalHatIndexField?.SetValue(null, (byte)0); if ((Object)(object)val != (Object)null) { val.sharedMesh = SkinManager.GetHat((byte)0); } break; } } private static PlayerSkinType ToPlayerSkinType(CosmeticSlot slot) { return (PlayerSkinType)(slot switch { CosmeticSlot.Accessory => 7, CosmeticSlot.Outfit => 1, _ => 4, }); } private static byte GetNativeIndex(CosmeticSlot slot) { return slot switch { CosmeticSlot.Accessory => LocalSkin.LocalAccessoryIndex, CosmeticSlot.Outfit => LocalSkin.LocalOutfitIndex, _ => LocalSkin.LocalHatIndex, }; } private void DestroyCustom(CosmeticSlot slot) { if ((Object)(object)_instances[(uint)slot] != (Object)null) { Object.Destroy((Object)(object)_instances[(uint)slot]); } _instances[(uint)slot] = null; } private void DestroyAllCustom() { for (int i = 0; i < AllSlots.Length; i++) { DestroyCustom(AllSlots[i]); } } private static void SetLayerRecursively(Transform root, int layer) { ((Component)root).gameObject.layer = layer; for (int i = 0; i < root.childCount; i++) { SetLayerRecursively(root.GetChild(i), layer); } } private static string DisplayId(string value) { if (!string.IsNullOrEmpty(value)) { return value; } return "<native>"; } } internal sealed class PlayerAppearanceService { private sealed class PlayerVisual { internal int ClientId; internal ulong PlayerEntityId; internal Player Player; internal SlotVisual[] Slots; } private sealed class SlotVisual { internal readonly Transform Anchor; internal string AppliedId = string.Empty; internal GameObject Instance; internal SlotVisual(Transform anchor) { Anchor = anchor; } } private static readonly CosmeticSlot[] AllSlots = new CosmeticSlot[3] { CosmeticSlot.Hat, CosmeticSlot.Accessory, CosmeticSlot.Outfit }; private static readonly FieldInfo RemoteHatRendererField = AccessTools.Field(typeof(PlayerSkin), "_hatRenderer"); private static readonly FieldInfo RemoteAccessoryRendererField = AccessTools.Field(typeof(PlayerSkin), "_accessoryRenderer"); private static readonly FieldInfo RemoteOutfitRendererField = AccessTools.Field(typeof(PlayerSkin), "_outfitRenderer"); private readonly ManualLogSource _logger; private readonly BoundedLog _boundedLog; private readonly CosmeticCatalog _catalog; private readonly Dictionary<int, PlayerVisual> _visuals = new Dictionary<int, PlayerVisual>(); private readonly Dictionary<ulong, int> _clientIdByPlayer = new Dictionary<ulong, int>(); private readonly Dictionary<int, CosmeticLoadout> _loadoutByClientId = new Dictionary<int, CosmeticLoadout>(); private readonly List<int> _scratchClientIds = new List<int>(); private int _localClientId = -1; internal PlayerAppearanceService(ManualLogSource logger, BoundedLog boundedLog, CosmeticCatalog catalog) { _logger = logger ?? throw new ArgumentNullException("logger"); _boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog"); _catalog = catalog ?? throw new ArgumentNullException("catalog"); } internal void NotifyPlayerReady(Player player) { //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null) { return; } NetworkConnection owner = ((NetworkBehaviour)player).Owner; if (owner == (NetworkConnection)null || !owner.IsValid) { return; } if (owner.IsLocalClient) { SetLocalClientId(owner.ClientId); return; } PlayerBody body = player.Body; Transform val = (((Object)(object)body != (Object)null) ? body.Head : null); if ((Object)(object)val == (Object)null) { _boundedLog.WarningOnce("appearance-head-missing-" + EntityId.ToULong(((Object)player).GetEntityId()), "[HTFMoreHead][VISUAL] Remote Player.Body.Head is unavailable."); return; } PlayerSkin skin = player.Skin; GetRenderer(RemoteHatRendererField, skin); Renderer renderer = GetRenderer(RemoteAccessoryRendererField, skin); Renderer renderer2 = GetRenderer(RemoteOutfitRendererField, skin); int clientId = owner.ClientId; ulong num = EntityId.ToULong(((Object)player).GetEntityId()); RemoveVisual(clientId); _visuals[clientId] = new PlayerVisual { ClientId = clientId, PlayerEntityId = num, Player = player, Slots = new SlotVisual[3] { new SlotVisual(val), new SlotVisual(((Object)(object)renderer != (Object)null) ? ((Component)renderer).transform : null), new SlotVisual(((Object)(object)renderer2 != (Object)null) ? ((Component)renderer2).transform : null) } }; _clientIdByPlayer[num] = clientId; ApplyAll(clientId); _logger.LogInfo((object)($"[HTFMoreHead][PLAYER_READY] ClientId={clientId}, PlayerEntity={num}, " + "HatAnchor='" + ((Object)val).name + "', AccessoryAnchor='" + NameOf(renderer) + "', OutfitAnchor='" + NameOf(renderer2) + "'.")); } internal void NotifyPlayerStopping(Player player) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)player == (Object)null)) { ulong key = EntityId.ToULong(((Object)player).GetEntityId()); if (_clientIdByPlayer.TryGetValue(key, out var value)) { RemovePlayer(value, clearLoadout: true); } } } internal void SetLoadout(int clientId, CosmeticLoadout loadout) { if (clientId >= 0) { if (clientId == _localClientId) { _loadoutByClientId.Remove(clientId); return; } _loadoutByClientId[clientId] = (loadout ?? new CosmeticLoadout()).Clone(); ApplyAll(clientId); } } internal void ReplaceSnapshot(IReadOnlyDictionary<int, CosmeticLoadout> snapshot) { _loadoutByClientId.Clear(); if (snapshot != null) { foreach (KeyValuePair<int, CosmeticLoadout> item in snapshot) { if (item.Key >= 0 && item.Key != _localClientId) { _loadoutByClientId[item.Key] = (item.Value ?? new CosmeticLoadout()).Clone(); } } } _scratchClientIds.Clear(); foreach (int key in _visuals.Keys) { _scratchClientIds.Add(key); } for (int i = 0; i < _scratchClientIds.Count; i++) { ApplyAll(_scratchClientIds[i]); } _logger.LogInfo((object)($"[HTFMoreHead][SNAPSHOT_VISUALS] LocalClientId={_localClientId}, " + $"RemoteStates={_loadoutByClientId.Count}, RemotePlayers={_visuals.Count}.")); } internal void RemovePlayer(int clientId, bool clearLoadout) { RemoveVisual(clientId); if (clearLoadout) { _loadoutByClientId.Remove(clientId); } } internal void PruneDestroyedPlayers() { _scratchClientIds.Clear(); foreach (KeyValuePair<int, PlayerVisual> visual in _visuals) { if ((Object)(object)visual.Value.Player == (Object)null || (Object)(object)visual.Value.Slots[0].Anchor == (Object)null) { _scratchClientIds.Add(visual.Key); } } for (int i = 0; i < _scratchClientIds.Count; i++) { RemoveVisual(_scratchClientIds[i]); } } internal void ClearAll() { _scratchClientIds.Clear(); foreach (int key in _visuals.Keys) { _scratchClientIds.Add(key); } for (int i = 0; i < _scratchClientIds.Count; i++) { RemoveVisual(_scratchClientIds[i]); } _loadoutByClientId.Clear(); _clientIdByPlayer.Clear(); _localClientId = -1; } private void SetLocalClientId(int clientId) { if (clientId >= 0) { if (_localClientId >= 0 && _localClientId != clientId) { _loadoutByClientId.Remove(_localClientId); } _localClientId = clientId; _loadoutByClientId.Remove(clientId); _logger.LogInfo((object)$"[HTFMoreHead][LOCAL_ID] LocalClientId={_localClientId}."); } } private void ApplyAll(int clientId) { for (int i = 0; i < AllSlots.Length; i++) { Apply(clientId, AllSlots[i]); } } private void Apply(int clientId, CosmeticSlot slot) { //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) if (!_visuals.TryGetValue(clientId, out var value) || (Object)(object)value.Player == (Object)null) { return; } SlotVisual slotVisual = value.Slots[(uint)slot]; if ((Object)(object)slotVisual.Anchor == (Object)null) { return; } CosmeticLoadout value2; string text = (_loadoutByClientId.TryGetValue(clientId, out value2) ? value2.Get(slot) : string.Empty); if (string.Equals(slotVisual.AppliedId, text, StringComparison.Ordinal) && (string.IsNullOrEmpty(text) || (Object)(object)slotVisual.Instance != (Object)null)) { return; } DestroyInstance(slotVisual); slotVisual.AppliedId = text; if (!string.IsNullOrEmpty(text)) { int index = _catalog.GetIndex(slot, text); CosmeticDefinition at = _catalog.GetAt(slot, index); if (at == null) { _boundedLog.WarningOnce("missing-cosmetic-" + clientId + "-" + text, $"[HTFMoreHead][MISSING_PACK] ClientId={clientId}, Cosmetic='{text}' is not installed locally."); return; } GameObject val = Object.Instantiate<GameObject>(at.Prefab, slotVisual.Anchor, false); ((Object)val).name = "HTFMoreHead_Player_" + at.Id; val.transform.localPosition = at.Entry.localPosition.ToVector3(); val.transform.localEulerAngles = at.Entry.localEulerAngles.ToVector3(); val.transform.localScale = at.Entry.localScale.ToVector3(); SetLayerRecursively(val.transform, ((Component)slotVisual.Anchor).gameObject.layer); slotVisual.Instance = val; _logger.LogInfo((object)($"[HTFMoreHead][EQUIPPED] ClientId={clientId}, Slot={slot}, " + "Cosmetic='" + at.Id + "', Anchor='" + ((Object)slotVisual.Anchor).name + "'.")); } } private void RemoveVisual(int clientId) { if (_visuals.TryGetValue(clientId, out var value)) { for (int i = 0; i < value.Slots.Length; i++) { DestroyInstance(value.Slots[i]); } _visuals.Remove(clientId); if (_clientIdByPlayer.TryGetValue(value.PlayerEntityId, out var value2) && value2 == clientId) { _clientIdByPlayer.Remove(value.PlayerEntityId); } } } private static Renderer GetRenderer(FieldInfo field, PlayerSkin skin) { if (!((Object)(object)skin != (Object)null)) { return null; } object? obj = field?.GetValue(skin); return (Renderer)((obj is Renderer) ? obj : null); } private static string NameOf(Renderer renderer) { if (!((Object)(object)renderer != (Object)null)) { return "<missing>"; } return ((Object)((Component)renderer).transform).name; } private static void DestroyInstance(SlotVisual visual) { if ((Object)(object)visual.Instance != (Object)null) { Object.Destroy((Object)(object)visual.Instance); } visual.Instance = null; visual.AppliedId = string.Empty; } private static void SetLayerRecursively(Transform root, int layer) { ((Component)root).gameObject.layer = layer; for (int i = 0; i < root.childCount; i++) { SetLayerRecursively(root.GetChild(i), layer); } } } } namespace HowToFish.MoreHead.Players { internal sealed class PlayerProbeTracker { private sealed class ProbeRecord { internal Player Player; internal PlayerSkin Skin; internal Transform Head; internal ulong PlayerInstanceId; internal int ClientId = -1; internal bool IsLocal; internal bool PlayerStarted; internal bool SkinStarted; internal bool Finalized; internal bool TimedOut; internal long PlayerStartSequence; internal long SkinStartSequence; internal string SkinResolveRoute = "unknown"; internal float Deadline; } private sealed class PendingSkin { internal PlayerSkin Skin; internal ulong SkinInstanceId; internal long SkinStartSequence; internal float Deadline; } private readonly struct HierarchyNode { internal Transform Transform { get; } internal int Depth { get; } internal HierarchyNode(Transform transform, int depth) { Transform = transform; Depth = depth; } } private const float RetryIntervalSeconds = 0.1f; private const int HierarchyPreviewMaxDepth = 5; private const int HierarchyPreviewMaxNodes = 64; private readonly ManualLogSource _logger; private readonly BoundedLog _boundedLog; private readonly float _timeoutSeconds; private readonly Dictionary<ulong, ProbeRecord> _records = new Dictionary<ulong, ProbeRecord>(); private readonly Dictionary<int, ulong> _playerInstanceByClientId = new Dictionary<int, ulong>(); private readonly Dictionary<ulong, PendingSkin> _pendingSkins = new Dictionary<ulong, PendingSkin>(); private readonly List<ulong> _scratchKeys = new List<ulong>(); private long _signalSequence; private int _sessionGeneration = 1; private float _nextTickAt; private bool _pruneRequested; private bool _sessionHasActivity; internal int LiveCount => _records.Count; internal event Action<Player, Transform> RemotePlayerReady; internal event Action<Player> PlayerStopping; internal PlayerProbeTracker(ManualLogSource logger, BoundedLog boundedLog, float timeoutSeconds) { _logger = logger ?? throw new ArgumentNullException("logger"); _boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog"); _timeoutSeconds = Math.Max(0.5f, timeoutSeconds); } internal void NotifyPlayerStarted(Player player) { if ((Object)(object)player == (Object)null) { _boundedLog.WarningOnce("null-player-start", "[HTFMoreHead][Stage0][PROBE] Player.OnStartClient supplied a null Unity object."); return; } _sessionHasActivity = true; ProbeRecord orCreateRecord = GetOrCreateRecord(player); if (!orCreateRecord.PlayerStarted) { orCreateRecord.PlayerStarted = true; orCreateRecord.PlayerStartSequence = ++_signalSequence; orCreateRecord.Deadline = Time.realtimeSinceStartup + _timeoutSeconds; _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=PlayerStart, Session={_sessionGeneration}, " + $"Sequence={orCreateRecord.PlayerStartSequence}, Frame={Time.frameCount}, " + $"ClientId={orCreateRecord.ClientId}, PlayerInstance={orCreateRecord.PlayerInstanceId}, " + $"ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, ComponentIndex={((NetworkBehaviour)player).ComponentIndex}.")); } TryFinalize(orCreateRecord); } internal void NotifySkinStarted(PlayerSkin skin) { if ((Object)(object)skin == (Object)null) { _boundedLog.WarningOnce("null-skin-start", "[HTFMoreHead][Stage0][PROBE] PlayerSkin.OnStartClient supplied a null Unity object."); return; } _sessionHasActivity = true; string route; Player val = ResolvePlayer(skin, out route); if ((Object)(object)val == (Object)null) { ulong entityId = GetEntityId((Object)(object)skin); if (!_pendingSkins.ContainsKey(entityId)) { _pendingSkins.Add(entityId, new PendingSkin { Skin = skin, SkinInstanceId = entityId, SkinStartSequence = ++_signalSequence, Deadline = Time.realtimeSinceStartup + _timeoutSeconds }); _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=SkinStart, Session={_sessionGeneration}, " + $"Sequence={_signalSequence}, Frame={Time.frameCount}, SkinInstance={entityId}, " + "PlayerRoute=Pending.")); } } else { RegisterSkinSignal(val, skin, route, ++_signalSequence); } } internal void NotifyPlayerStopping(Player player) { if (!((Object)(object)player == (Object)null)) { this.PlayerStopping?.Invoke(player); ulong entityId = GetEntityId((Object)(object)player); int clientId = GetClientId((NetworkBehaviour)(object)player); bool flag = RemoveRecord(entityId); _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_STOP] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={clientId}, PlayerInstance={entityId}, Removed={flag}, " + $"Remaining={_records.Count}.")); TryAdvanceSessionWhenEmpty("PlayerStop"); } } internal void RequestPrune() { _pruneRequested = true; } internal void Tick() { if (_pruneRequested) { _pruneRequested = false; PruneDestroyedReferences(); } float realtimeSinceStartup = Time.realtimeSinceStartup; if (!(realtimeSinceStartup < _nextTickAt)) { _nextTickAt = realtimeSinceStartup + 0.1f; ResolvePendingSkins(realtimeSinceStartup); RetryPendingRecords(realtimeSinceStartup); } } internal void ClearAll(string reason) { int count = _records.Count; int count2 = _pendingSkins.Count; _records.Clear(); _playerInstanceByClientId.Clear(); _pendingSkins.Clear(); _scratchKeys.Clear(); _sessionGeneration++; _sessionHasActivity = false; _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_CLEAR] Reason={reason}, Records={count}, " + $"PendingSkins={count2}, NextSession={_sessionGeneration}.")); } private ProbeRecord GetOrCreateRecord(Player player) { ulong entityId = GetEntityId((Object)(object)player); if (!_records.TryGetValue(entityId, out var value)) { value = new ProbeRecord { Player = player, PlayerInstanceId = entityId, Deadline = Time.realtimeSinceStartup + _timeoutSeconds }; _records.Add(entityId, value); } RefreshIdentity(value); return value; } private void RefreshIdentity(ProbeRecord record) { int num = (record.ClientId = GetClientId((NetworkBehaviour)(object)record.Player)); record.IsLocal = IsLocal((NetworkBehaviour)(object)record.Player); if (num >= 0) { if (_playerInstanceByClientId.TryGetValue(num, out var value) && value != record.PlayerInstanceId) { _boundedLog.WarningOnce($"client-reuse-{num}-{value}-{record.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE] ClientId={num} moved from " + $"PlayerInstance={value} to {record.PlayerInstanceId}; stale record removed."); RemoveRecord(value); } _playerInstanceByClientId[num] = record.PlayerInstanceId; } } private void RegisterSkinSignal(Player player, PlayerSkin skin, string route, long sequence) { ProbeRecord orCreateRecord = GetOrCreateRecord(player); if (!orCreateRecord.SkinStarted) { orCreateRecord.Skin = skin; orCreateRecord.SkinStarted = true; orCreateRecord.SkinStartSequence = sequence; orCreateRecord.SkinResolveRoute = route; orCreateRecord.Deadline = Time.realtimeSinceStartup + _timeoutSeconds; _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_SIGNAL] Signal=SkinStart, Session={_sessionGeneration}, " + $"Sequence={sequence}, Frame={Time.frameCount}, ClientId={orCreateRecord.ClientId}, " + $"PlayerInstance={orCreateRecord.PlayerInstanceId}, SkinInstance={GetEntityId((Object)(object)skin)}, " + $"ObjectId={GetObjectId((NetworkBehaviour)(object)skin)}, ComponentIndex={((NetworkBehaviour)skin).ComponentIndex}, PlayerRoute={route}.")); } TryFinalize(orCreateRecord); } private bool TryFinalize(ProbeRecord record) { //IL_0268: Unknown result type (might be due to invalid IL or missing references) //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_029b: Unknown result type (might be due to invalid IL or missing references) if (record == null || record.Finalized || record.TimedOut || !record.PlayerStarted || !record.SkinStarted) { return false; } Player player = record.Player; PlayerSkin skin = record.Skin; if ((Object)(object)player == (Object)null || (Object)(object)skin == (Object)null || (Object)(object)player.Skin != (Object)(object)skin) { return false; } RefreshIdentity(record); if (record.ClientId < 0) { return false; } if (record.IsLocal) { record.Finalized = true; _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_LOCAL] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={record.ClientId}, ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, " + $"PlayerInstance={record.PlayerInstanceId}, PlayerStartSeq={record.PlayerStartSequence}, " + $"SkinStartSeq={record.SkinStartSequence}, PlayerRoute={record.SkinResolveRoute}, " + "ThirdPersonHead=Skipped.")); return true; } PlayerBody body = player.Body; Transform val = (((Object)(object)body != (Object)null) ? body.Head : null); if ((Object)(object)body == (Object)null || (Object)(object)val == (Object)null) { return false; } bool flag = (Object)(object)val == (Object)(object)((Component)player).transform || val.IsChildOf(((Component)player).transform); record.Head = val; record.Finalized = true; _logger.LogInfo((object)($"[HTFMoreHead][Stage0][PROBE_HEAD] Session={_sessionGeneration}, Frame={Time.frameCount}, " + $"ClientId={record.ClientId}, ObjectId={GetObjectId((NetworkBehaviour)(object)player)}, IsLocal=False, " + $"IsBean={player.IsBean}, PlayerInstance={record.PlayerInstanceId}, " + $"HeadInstance={GetEntityId((Object)(object)val)}, PlayerStartSeq={record.PlayerStartSequence}, " + $"SkinStartSeq={record.SkinStartSequence}, PlayerRoute={record.SkinResolveRoute}, " + $"Path=\"{BuildHierarchyPath(val, ((Component)player).transform)}\", UnderPlayer={flag}, " + $"Active={((Component)val).gameObject.activeInHierarchy}, LocalPosition={FormatVector3(val.localPosition)}, " + "LocalEulerAngles=" + FormatVector3(val.localEulerAngles) + ", LocalScale=" + FormatVector3(val.localScale) + ", PlayerSkinMatched=True.")); if (!flag) { _boundedLog.WarningOnce($"head-outside-player-{record.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE] Body.Head for ClientId={record.ClientId} is not below " + "the Player transform. Do not enter the attachment phase until this is explained."); } this.RemotePlayerReady?.Invoke(player, val); return true; } private void ResolvePendingSkins(float now) { if (_pendingSkins.Count == 0) { return; } _scratchKeys.Clear(); foreach (KeyValuePair<ulong, PendingSkin> pendingSkin in _pendingSkins) { PendingSkin value = pendingSkin.Value; if ((Object)(object)value.Skin == (Object)null) { _scratchKeys.Add(pendingSkin.Key); continue; } string route; Player val = ResolvePlayer(value.Skin, out route); if ((Object)(object)val != (Object)null) { _scratchKeys.Add(pendingSkin.Key); RegisterSkinSignal(val, value.Skin, route, value.SkinStartSequence); } else if (now >= value.Deadline) { _boundedLog.WarningOnce($"skin-player-timeout-{value.SkinInstanceId}", $"[HTFMoreHead][Stage0][PROBE_TIMEOUT] SkinInstance={value.SkinInstanceId} " + $"could not resolve its Player within {_timeoutSeconds:F1}s."); _scratchKeys.Add(pendingSkin.Key); } } for (int i = 0; i < _scratchKeys.Count; i++) { _pendingSkins.Remove(_scratchKeys[i]); } TryAdvanceSessionWhenEmpty("PendingSkinResolvedOrExpired"); } private void RetryPendingRecords(float now) { if (_records.Count == 0) { return; } _scratchKeys.Clear(); foreach (ulong key in _records.Keys) { _scratchKeys.Add(key); } for (int i = 0; i < _scratchKeys.Count; i++) { if (_records.TryGetValue(_scratchKeys[i], out var value) && !value.Finalized && !value.TimedOut && !TryFinalize(value) && !(now < value.Deadline)) { value.TimedOut = true; string text = (((Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null && (Object)(object)value.Player.Body.Head == (Object)null) ? BuildHierarchyPreview(((Component)value.Player).transform) : "<not-required>"); _boundedLog.WarningOnce($"player-finalize-timeout-{value.PlayerInstanceId}", $"[HTFMoreHead][Stage0][PROBE_TIMEOUT] PlayerInstance={value.PlayerInstanceId}, " + $"ClientId={value.ClientId}, PlayerStarted={value.PlayerStarted}, " + $"SkinStarted={value.SkinStarted}, PlayerValid={(Object)(object)value.Player != (Object)null}, " + $"SkinValid={(Object)(object)value.Skin != (Object)null}, BodyValid={(Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null}, " + $"HeadValid={(Object)(object)value.Player != (Object)null && (Object)(object)value.Player.Body != (Object)null && (Object)(object)value.Player.Body.Head != (Object)null}, " + "HierarchyPreview=\"" + text + "\"."); } } } private void PruneDestroyedReferences() { _scratchKeys.Clear(); foreach (KeyValuePair<ulong, ProbeRecord> record in _records) { if ((Object)(object)record.Value.Player == (Object)null) { _scratchKeys.Add(record.Key); } } for (int i = 0; i < _scratchKeys.Count; i++) { RemoveRecord(_scratchKeys[i]); } _scratchKeys.Clear(); foreach (KeyValuePair<ulong, PendingSkin> pendingSkin in _pendingSkins) { if ((Object)(object)pendingSkin.Value.Skin == (Object)null) { _scratchKeys.Add(pendingSkin.Key); } } for (int j = 0; j < _scratchKeys.Count; j++) { _pendingSkins.Remove(_scratchKeys[j]); } TryAdvanceSessionWhenEmpty("ScenePrune"); } private void TryAdvanceSessionWhenEmpty(string reason) { if (_sessionHasActivity && _records.Count == 0 && _pendingSkins.Count == 0) { _sessionHasActivity = false; _sessionGeneration++; _logger.LogInfo((object)("[HTFMoreHead][Stage0][PROBE_SESSION] Reason=" + reason + ", " + $"NextSession={_sessionGeneration}.")); } } private bool RemoveRecord(ulong playerInstanceId) { if (!_records.TryGetValue(playerInstanceId, out var value)) { return false; } _records.Remove(playerInstanceId); if (value.ClientId >= 0 && _playerInstanceByClientId.TryGetValue(value.ClientId, out var value2) && value2 == playerInstanceId) { _playerInstanceByClientId.Remove(value.ClientId); } return true; } private static Player ResolvePlayer(PlayerSkin skin, out string route) { route = "none"; if ((Object)(object)skin == (Object)null) { return null; } Player component = ((Component)skin).GetComponent<Player>(); if (Matches(component, skin)) { route = "same-game-object"; return component; } if ((Object)(object)((NetworkBehaviour)skin).NetworkObject != (Object)null) { component = ((Component)((NetworkBehaviour)skin).NetworkObject).GetComponent<Player>(); if (Matches(component, skin)) { route = "network-object-root"; return component; } IList<NetworkBehaviour> networkBehaviours = ((NetworkBehaviour)skin).NetworkObject.NetworkBehaviours; if (networkBehaviours != null) { for (int i = 0; i < networkBehaviours.Count; i++) { NetworkBehaviour obj = networkBehaviours[i]; component = (Player)(object)((obj is Player) ? obj : null); if (Matches(component, skin)) { route = "network-behaviours"; return component; } } } } component = ((Component)skin).GetComponentInParent<Player>(true); if (Matches(component, skin)) { route = "parent"; return component; } return null; } private static bool Matches(Player player, PlayerSkin skin) { if ((Object)(object)player != (Object)null) { return (Object)(object)player.Skin == (Object)(object)skin; } return false; } private static int GetClientId(NetworkBehaviour behaviour) { if ((Object)(object)behaviour == (Object)null) { return -1; } NetworkConnection owner = behaviour.Owner; if (!(owner != (NetworkConnection)null) || !owner.IsValid) { return -1; } return owner.ClientId; } private static bool IsLocal(NetworkBehaviour behaviour) { if ((Object)(object)behaviour == (Object)null) { return false; } NetworkConnection owner = behaviour.Owner; if (owner != (NetworkConnection)null && owner.IsValid) { return owner.IsLocalClient; } return false; } private static int GetObjectId(NetworkBehaviour behaviour) { if (!((Object)(object)behaviour != (Object)null) || !((Object)(object)behaviour.NetworkObject != (Object)null)) { return -1; } return behaviour.NetworkObject.ObjectId; } private static ulong GetEntityId(Object value) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (!(value != (Object)null)) { return 0uL; } return EntityId.ToULong(value.GetEntityId()); } private static string BuildHierarchyPath(Transform target, Transform playerRoot) { if ((Object)(object)target == (Object)null) { return "<null>"; } List<string> list = new List<string>(); Transform val = target; bool flag = false; while ((Object)(object)val != (Object)null) { list.Add($"{((Object)val).name}[{val.GetSiblingIndex()}]"); if ((Object)(object)val == (Object)(object)playerRoot) { flag = true; break; } val = val.parent; } list.Reverse(); string text = string.Join("/", list.ToArray()); if (!flag) { return "<outside-player>/" + text; } return text; } private static string BuildHierarchyPreview(Transform root) { if ((Object)(object)root == (Object)null) { return "<null>"; } StringBuilder stringBuilder = new StringBuilder(); Queue<HierarchyNode> queue = new Queue<HierarchyNode>(); queue.Enqueue(new HierarchyNode(root, 0)); int num = 0; while (queue.Count > 0 && num < 64) { HierarchyNode hierarchyNode = queue.Dequeue(); if (num > 0) { stringBuilder.Append(" | "); } stringBuilder.Append(hierarchyNode.Depth); stringBuilder.Append(':'); stringBuilder.Append(((Object)hierarchyNode.Transform).name); stringBuilder.Append('['); stringBuilder.Append(hierarchyNode.Transform.GetSiblingIndex()); stringBuilder.Append(']'); num++; if (hierarchyNode.Depth < 5) { for (int i = 0; i < hierarchyNode.Transform.childCount; i++) { queue.Enqueue(new HierarchyNode(hierarchyNode.Transform.GetChild(i), hierarchyNode.Depth + 1)); } } } if (queue.Count > 0) { stringBuilder.Append(" | <truncated>"); } return stringBuilder.ToString(); } private static string FormatVector3(Vector3 value) { //IL_000a: 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_0020: Unknown result type (might be due to invalid IL or missing references) return string.Format(CultureInfo.InvariantCulture, "({0:F4},{1:F4},{2:F4})", value.x, value.y, value.z); } } } namespace HowToFish.MoreHead.Persistence { internal sealed class LocalLoadoutStore { private sealed class LocalLoadoutData { public int schemaVersion = 2; public string headCosmeticId = string.Empty; public string hatCosmeticId = string.Empty; public string accessoryCosmeticId = string.Empty; public string outfitCosmeticId = string.Empty; } private readonly ManualLogSource _logger; private readonly string _path; private CosmeticLoadout _loadout = new CosmeticLoadout(); internal LocalLoadoutStore(ManualLogSource logger, string path) { _logger = logger ?? throw new ArgumentNullException("logger"); _path = path ?? throw new ArgumentNullException("path"); } internal CosmeticLoadout GetLoadout() { return _loadout.Clone(); } internal string Get(CosmeticSlot slot) { return _loadout.Get(slot); } internal void Load() { _loadout = new CosmeticLoadout(); if (!File.Exists(_path)) { return; } try { LocalLoadoutData localLoadoutData = JsonConvert.DeserializeObject<LocalLoadoutData>(File.ReadAllText(_path)); if (localLoadoutData == null || (localLoadoutData.schemaVersion != 1 && localLoadoutData.schemaVersion != 2)) { _logger.LogWarning((object)("[HTFMoreHead][LOADOUT] Ignored unsupported loadout: '" + _path + "'.")); return; } _loadout.HatId = Normalize((localLoadoutData.schemaVersion == 1) ? localLoadoutData.headCosmeticId : localLoadoutData.hatCosmeticId); _loadout.AccessoryId = Normalize(localLoadoutData.accessoryCosmeticId); _loadout.OutfitId = Normalize(localLoadoutData.outfitCosmeticId); _logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Loaded Hat='" + DisplayId(_loadout.HatId) + "', Accessory='" + DisplayId(_loadout.AccessoryId) + "', Outfit='" + DisplayId(_loadout.OutfitId) + "'.")); } catch (Exception ex) { _logger.LogWarning((object)("[HTFMoreHead][LOADOUT] Could not load '" + _path + "': " + ex.Message)); } } internal void Save(CosmeticLoadout loadout) { _loadout = new CosmeticLoadout { HatId = Normalize(loadout?.HatId), AccessoryId = Normalize(loadout?.AccessoryId), OutfitId = Normalize(loadout?.OutfitId) }; try { string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } LocalLoadoutData localLoadoutData = new LocalLoadoutData { schemaVersion = 2, hatCosmeticId = _loadout.HatId, accessoryCosmeticId = _loadout.AccessoryId, outfitCosmeticId = _loadout.OutfitId }; File.WriteAllText(_path, JsonConvert.SerializeObject((object)localLoadoutData, (Formatting)1)); _logger.LogInfo((object)("[HTFMoreHead][LOADOUT] Saved Hat='" + DisplayId(_loadout.HatId) + "', Accessory='" + DisplayId(_loadout.AccessoryId) + "', Outfit='" + DisplayId(_loadout.OutfitId) + "'.")); } catch (Exception ex) { _logger.LogError((object)("[HTFMoreHead][LOADOUT] Could not save '" + _path + "': " + ex.Message)); } } private static string Normalize(string value) { value = value?.Trim(); if (string.IsNullOrEmpty(value) || value.Length > 193) { return string.Empty; } return value; } private static string DisplayId(string value) { if (!string.IsNullOrEmpty(value)) { return value; } return "<native>"; } } } namespace HowToFish.MoreHead.Patches { [HarmonyPatch(typeof(LocalSkin), "Awake")] internal static class LocalSkinAwakePatch { [HarmonyPostfix] private static void Postfix() { try { MoreHeadPlugin.Instance?.DispatchLocalSkinReady(); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinAwakePatch", exception); } } } [HarmonyPatch(typeof(Player), "OnStartClient")] internal static class PlayerStartClientPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { try { MoreHeadPlugin.Instance?.DispatchPlayerStarted(__instance); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("PlayerStartClientPatch", exception); } } } [HarmonyPatch(typeof(PlayerSkin), "OnStartClient")] internal static class PlayerSkinStartClientPatch { [HarmonyPostfix] private static void Postfix(PlayerSkin __instance) { try { MoreHeadPlugin.Instance?.DispatchSkinStarted(__instance); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("PlayerSkinStartClientPatch", exception); } } } [HarmonyPatch(typeof(Player), "OnStopClient")] internal static class PlayerStopClientPatch { [HarmonyPrefix] private static void Prefix(Player __instance) { try { MoreHeadPlugin.Instance?.DispatchPlayerStopping(__instance); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("PlayerStopClientPatch", exception); } } } [HarmonyPatch(typeof(LocalSkin), "EnablePreview")] internal static class LocalSkinEnablePreviewPatch { [HarmonyPostfix] private static void Postfix() { try { MoreHeadPlugin.Instance?.DispatchLocalSkinPreview(visible: true); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinEnablePreviewPatch", exception); } } } [HarmonyPatch(typeof(LocalSkin), "DisablePreview")] internal static class LocalSkinDisablePreviewPatch { [HarmonyPostfix] private static void Postfix() { try { MoreHeadPlugin.Instance?.DispatchLocalSkinPreview(visible: false); } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("LocalSkinDisablePreviewPatch", exception); } } } internal struct CosmeticArrowState { internal byte BeforeIndex; internal bool HandledByMoreHead; } internal static class CosmeticArrowPatchLogic { internal static bool Prefix(CosmeticSlot slot, int amount, ref CosmeticArrowState state) { try { state.BeforeIndex = GetNativeIndex(slot); state.HandledByMoreHead = MoreHeadPlugin.Instance?.HandleNativeCosmeticArrow(slot, amount) ?? false; return !state.HandledByMoreHead; } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("CosmeticArrowPrefix." + slot, exception); return true; } } internal static void Postfix(CosmeticSlot slot, int amount, CosmeticArrowState state) { try { if (!state.HandledByMoreHead) { MoreHeadPlugin.Instance?.NotifyNativeCosmeticArrowCompleted(slot, amount, state.BeforeIndex, GetNativeIndex(slot)); } } catch (Exception exception) { MoreHeadPlugin.Instance?.ReportPatchException("CosmeticArrowPostfix." + slot, exception); } } private static byte GetNativeIndex(CosmeticSlot slot) { return slot switch { CosmeticSlot.Accessory => LocalSkin.LocalAccessoryIndex, CosmeticSlot.Outfit => LocalSkin.LocalOutfitIndex, _ => LocalSkin.LocalHatIndex, }; } } [HarmonyPatch(typeof(ButtonManager), "ChangeHatIndex")] internal static class NativeHatArrowPatch { [HarmonyPrefix] private static bool Prefix(int amount, ref CosmeticArrowState __state) { return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Hat, amount, ref __state); } [HarmonyPostfix] private static void Postfix(int amount, CosmeticArrowState __state) { CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Hat, amount, __state); } } [HarmonyPatch(typeof(ButtonManager), "ChangeAccessoryIndex")] internal static class NativeAccessoryArrowPatch { [HarmonyPrefix] private static bool Prefix(int amount, ref CosmeticArrowState __state) { return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Accessory, amount, ref __state); } [HarmonyPostfix] private static void Postfix(int amount, CosmeticArrowState __state) { CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Accessory, amount, __state); } } [HarmonyPatch(typeof(ButtonManager), "ChangeOutfitIndex")] internal static class NativeOutfitArrowPatch { [HarmonyPrefix] private static bool Prefix(int amount, ref CosmeticArrowState __state) { return CosmeticArrowPatchLogic.Prefix(CosmeticSlot.Outfit, amount, ref __state); } [HarmonyPostfix] private static void Postfix(int amount, CosmeticArrowState __state) { CosmeticArrowPatchLogic.Postfix(CosmeticSlot.Outfit, amount, __state); } } [HarmonyPatch(typeof(ButtonManager), "SetHatColoringPart")] internal static class HatTabPatch { [HarmonyPostfix] private static void Postfix() { MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Hat); } } [HarmonyPatch(typeof(ButtonManager), "SetAccessoryColoringPart")] internal static class AccessoryTabPatch { [HarmonyPostfix] private static void Postfix() { MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Accessory); } } [HarmonyPatch(typeof(ButtonManager), "SetOutfitColoringPart")] internal static class OutfitTabPatch { [HarmonyPostfix] private static void Postfix() { MoreHeadPlugin.Instance?.NotifyActiveCosmeticSlot(CosmeticSlot.Outfit); } } } namespace HowToFish.MoreHead.Networking { internal enum CosmeticCommand : byte { ClientHello = 1, ServerHello, EquipRequest, EquipDelta, SnapshotRequest, SnapshotBegin, SnapshotEntry, SnapshotEnd, ClearPlayer } internal struct CosmeticMessage : IBroadcast { public byte ProtocolVersion; public CosmeticCommand Command; public byte Accepted; public int SubjectClientId; public int Revision; public int EntryCount; public string HatCosmeticId; public string AccessoryCosmeticId; public string OutfitCosmeticId; public string CatalogDigest; } internal static class CosmeticMessageSerializers { internal static void Register() { GenericWriter<CosmeticMessage>.SetWrite((Action<Writer, CosmeticMessage>)Write); GenericReader<CosmeticMessage>.SetRead((Func<Reader, CosmeticMessage>)Read); } private static void Write(Writer writer, CosmeticMessage value) { writer.WriteUInt8Unpacked(value.ProtocolVersion); writer.WriteUInt8Unpacked((byte)value.Command); writer.WriteUInt8Unpacked(value.Accepted); writer.WriteInt32(value.SubjectClientId); writer.WriteInt32(value.Revision); writer.WriteInt32(value.EntryCount); writer.WriteString(value.HatCosmeticId ?? string.Empty); writer.WriteString(value.AccessoryCosmeticId ?? string.Empty); writer.WriteString(value.OutfitCosmeticId ?? string.Empty); writer.WriteString(value.CatalogDigest ?? string.Empty); } private static CosmeticMessage Read(Reader reader) { return new CosmeticMessage { ProtocolVersion = reader.ReadUInt8Unpacked(), Command = (CosmeticCommand)reader.ReadUInt8Unpacked(), Accepted = reader.ReadUInt8Unpacked(), SubjectClientId = reader.ReadInt32(), Revision = reader.ReadInt32(), EntryCount = reader.ReadInt32(), HatCosmeticId = reader.ReadStringAllocated(), AccessoryCosmeticId = reader.ReadStringAllocated(), OutfitCosmeticId = reader.ReadStringAllocated(), CatalogDigest = reader.ReadStringAllocated() }; } } internal sealed class CosmeticNetworkService { private const int MaxSnapshotEntries = 32; private const float HookCheckInterval = 0.5f; private const float HelloRetryInterval = 2f; private const float EquipRequestInterval = 0.15f; private static readonly Regex NetworkCosmeticId = new Regex("^[a-z0-9][a-z0-9._-]{0,95}:[a-z0-9][a-z0-9._-]{0,95}$", RegexOptions.CultureInvariant); private readonly ManualLogSource _logger; private readonly BoundedLog _boundedLog; private readonly CosmeticCatalog _catalog; private readonly LocalLoadoutStore _loadoutStore; private readonly PlayerAppearanceService _appearance; private readonly Dictionary<int, CosmeticLoadout> _hostState = new Dictionary<int, CosmeticLoadout>(); private readonly Dictionary<int, float> _nextEquipAt = new Dictionary<int, float>(); private readonly HashSet<int> _acceptedClients = new HashSet<int>(); private readonly Dictionary<int, CosmeticLoadout> _clientState = new Dictionary<int, CosmeticLoadout>(); private readonly Dictionary<int, CosmeticLoadout> _pendingSnapshot = new Dictionary<int, CosmeticLoadout>(); private NetworkManager _networkManager; private float _nextHookCheckAt; private float _nextHelloRetryAt; private int _revision; private int _pendingSnapshotRevision = -1; private int _pendingSnapshotExpected; private bool _protocolAccepted; internal CosmeticNetworkService(ManualLogSource logger, BoundedLog boundedLog, CosmeticCatalog catalog, LocalLoadoutStore loadoutStore, PlayerAppearanceService appearance) { _logger = logger ?? throw new ArgumentNullException("logger"); _boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog"); _catalog = catalog ?? throw new ArgumentNullException("catalog"); _loadoutStore = loadoutStore ?? throw new ArgumentNullException("loadoutStore"); _appearance = appearance ?? throw new ArgumentNullException("appearance"); CosmeticMessageSerializers.Register(); } internal void Tick() { if (!(Time.unscaledTime < _nextHookCheckAt)) { _nextHookCheckAt = Time.unscaledTime + 0.5f; EnsureHooks(); if ((Object)(object)_networkManager != (Object)null && _networkManager.IsClientStarted && !_protocolAccepted && Time.unscaledTime >= _nextHelloRetryAt) { SendHello("HandshakeRetry"); } } } internal void NotifyPlayerReady(Player player) { if (!((Object)(object)player == (Object)null) && !(((NetworkBehaviour)player).Owner == (NetworkConnection)null) && ((NetworkBehaviour)player).Owner.IsLocalClient) { EnsureHooks(); if ((Object)(object)_networkManager != (Object)null && _networkManager.IsClientStarted) { SendHello("LocalPlayerReady"); } } } internal void RequestLocalEquip(CosmeticLoadout loadout) { if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsClientStarted) { if (!_protocolAccepted) { SendHello("EquipBeforeAccepted"); } loadout = loadout ?? new CosmeticLoadout(); if (!IsValidLoadout(loadout)) { _logger.LogWarning((object)"[HTFMoreHead][NETWORK] Refused invalid local cosmetic loadout."); return; } _networkManager.ClientManager.Broadcast<CosmeticMessage>(new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.EquipRequest, HatCosmeticId = loadout.HatId, AccessoryCosmeticId = loadout.AccessoryId, OutfitCosmeticId = loadout.OutfitId }, (Channel)0); } } internal void ClearAndDetach() { DetachHooks(); ClearSessionState(); } private void EnsureHooks() { NetworkManager val = (((Object)(object)Server.Instance != (Object)null) ? ((NetworkBehaviour)Server.Instance).NetworkManager : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)_networkManager)) { bool num = (Object)(object)_networkManager != (Object)null; DetachHooks(); if (num) { ClearSessionState(); } _networkManager = val; _networkManager.ClientManager.RegisterBroadcast<CosmeticMessage>((Action<CosmeticMessage, Channel>)OnClientMessage); _networkManager.ServerManager.RegisterBroadcast<CosmeticMessage>((Action<NetworkConnection, CosmeticMessage, Channel>)OnServerMessage, true); _networkManager.ClientManager.OnClientConnectionState += OnClientConnectionState; _networkManager.ServerManager.OnRemoteConnectionState += OnRemoteConnectionState; _logger.LogInfo((object)"[HTFMoreHead][Stage3][NETWORK] FishNet Broadcast hooks registered."); if (_networkManager.IsClientStarted) { SendHello("HooksRegistered"); } } } private void DetachHooks() { if (!((Object)(object)_networkManager == (Object)null)) { try { _networkManager.ClientManager.UnregisterBroadcast<CosmeticMessage>((Action<CosmeticMessage, Channel>)OnClientMessage); _networkManager.ServerManager.UnregisterBroadcast<CosmeticMessage>((Action<NetworkConnection, CosmeticMessage, Channel>)OnServerMessage); _networkManager.ClientManager.OnClientConnectionState -= OnClientConnectionState; _networkManager.ServerManager.OnRemoteConnectionState -= OnRemoteConnectionState; } catch (Exception ex) { _logger.LogWarning((object)("[HTFMoreHead][Stage3][NETWORK] Hook cleanup failed safely: " + ex.Message)); } _networkManager = null; } } private void SendHello(string reason) { if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsClientStarted) { _protocolAccepted = false; _nextHelloRetryAt = Time.unscaledTime + 2f; _networkManager.ClientManager.Broadcast<CosmeticMessage>(new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.ClientHello, HatCosmeticId = _loadoutStore.Get(CosmeticSlot.Hat), AccessoryCosmeticId = _loadoutStore.Get(CosmeticSlot.Accessory), OutfitCosmeticId = _loadoutStore.Get(CosmeticSlot.Outfit), CatalogDigest = _catalog.CatalogDigest }, (Channel)0); _logger.LogInfo((object)($"[HTFMoreHead][HELLO] Sent Protocol={(byte)3}, " + "Catalog=" + _catalog.CatalogDigest + ", Loadout=" + FormatLoadout(_loadoutStore.GetLoadout()) + ", Reason=" + reason + ".")); } } private void OnServerMessage(NetworkConnection connection, CosmeticMessage message, Channel channel) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) try { OnServerMessageCore(connection, message, channel); } catch (Exception arg) { _boundedLog.ErrorOnce("server-message-exception-" + message.Command, $"[HTFMoreHead][Stage3][ERROR] Server message {message.Command} failed safely: {arg}"); } } private void OnServerMessageCore(NetworkConnection connection, CosmeticMessage message, Channel channel) { if ((Object)(object)_networkManager == (Object)null || !_networkManager.IsServerStarted || connection == (NetworkConnection)null || !connection.IsActive || !connection.IsAuthenticated) { return; } if (message.ProtocolVersion != 3) { SendTo(connection, new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.ServerHello, Accepted = 0 }); return; } switch (message.Command) { case CosmeticCommand.ClientHello: HandleClientHello(connection, message); break; case CosmeticCommand.EquipRequest: if (_acceptedClients.Contains(connection.ClientId)) { HandleEquipRequest(connection, ReadLoadout(message)); } break; case CosmeticCommand.SnapshotRequest: if (_acceptedClients.Contains(connection.ClientId)) { SendSnapshot(connection); } break; case CosmeticCommand.ServerHello: case CosmeticCommand.EquipDelta: break; } } private void HandleClientHello(NetworkConnection connection, CosmeticMessage message) { int clientId = connection.ClientId; CosmeticLoadout cosmeticLoadout = ReadLoadout(message); if (cosmeticLoadout == null) { cosmeticLoadout = new CosmeticLoadout(); } CosmeticLoadout value; bool num = !_hostState.TryGetValue(clientId, out value) || !LoadoutEquals(value, cosmeticLoadout); _hostState[clientId] = cosmeticLoadout.Clone(); _acceptedClients.Add(clientId); if (num) { _revision++; } SendTo(connection, new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.ServerHello, Accepted = 1, Revision = _revision, CatalogDigest = _catalog.CatalogDigest }); if (num) { BroadcastDelta(clientId, cosmeticLoadout); } SendSnapshot(connection); if (!string.Equals(message.CatalogDigest, _catalog.CatalogDigest, StringComparison.Ordinal)) { _boundedLog.WarningOnce("catalog-mismatch-server-" + clientId, $"[HTFMoreHead][Stage3][CATALOG] ClientId={clientId} catalog differs from Host; " + "missing content will be skipped locally."); } _logger.LogInfo((object)($"[HTFMoreHead][HELLO] Accepted ClientId={clientId}, " + $"Loadout={FormatLoadout(cosmeticLoadout)}, Revision={_revision}.")); } private void HandleEquipRequest(NetworkConnection connection, CosmeticLoadout requestedLoadout) { int clientId = connection.ClientId; float realtimeSinceStartup = Time.realtimeSinceStartup; if (!_nextEquipAt.TryGetValue(clientId, out var value) || !(realtimeSinceStartup < value)) { _nextEquipAt[clientId] = realtimeSinceStartup + 0.15f; if (!IsValidLoadout(requestedLoadout)) { _boundedLog.WarningOnce("invalid-equip-" + clientId, $"[HTFMoreHead][NETWORK] ClientId={clientId} sent an invalid cosmetic loadout."); return; } _hostState[clientId] = requestedLoadout.Clone(); _revision++; BroadcastDelta(clientId, requestedLoadout); } } private void BroadcastDelta(int subjectClientId, CosmeticLoadout loadout) { if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsServerStarted) { _networkManager.ServerManager.Broadcast<CosmeticMessage>(new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.EquipDelta, SubjectClientId = subjectClientId, Revision = _revision, HatCosmeticId = (loadout?.HatId ?? string.Empty), AccessoryCosmeticId = (loadout?.AccessoryId ?? string.Empty), OutfitCosmeticId = (loadout?.OutfitId ?? string.Empty) }, true, (Channel)0); } } private void SendSnapshot(NetworkConnection connection) { if (connection == (NetworkConnection)null || !connection.IsActive || _hostState.Count > 32) { return; } SendTo(connection, new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.SnapshotBegin, Revision = _revision, EntryCount = _hostState.Count }); _logger.LogInfo((object)($"[HTFMoreHead][Stage3][SNAPSHOT_SEND] ClientId={connection.ClientId}, " + $"Revision={_revision}, Entries={_hostState.Count}.")); foreach (KeyValuePair<int, CosmeticLoadout> item in _hostState) { SendTo(connection, new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.SnapshotEntry, SubjectClientId = item.Key, Revision = _revision, HatCosmeticId = (item.Value?.HatId ?? string.Empty), AccessoryCosmeticId = (item.Value?.AccessoryId ?? string.Empty), OutfitCosmeticId = (item.Value?.OutfitId ?? string.Empty) }); } SendTo(connection, new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.SnapshotEnd, Revision = _revision, EntryCount = _hostState.Count }); } private void SendTo(NetworkConnection connection, CosmeticMessage message) { if (!((Object)(object)_networkManager == (Object)null) && !(connection == (NetworkConnection)null) && connection.IsActive && connection.IsAuthenticated) { _networkManager.ServerManager.Broadcast<CosmeticMessage>(connection, message, true, (Channel)0); } } private void OnClientMessage(CosmeticMessage message, Channel channel) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) try { OnClientMessageCore(message, channel); } catch (Exception arg) { _boundedLog.ErrorOnce("client-message-exception-" + message.Command, $"[HTFMoreHead][Stage3][ERROR] Client message {message.Command} failed safely: {arg}"); } } private void OnClientMessageCore(CosmeticMessage message, Channel channel) { if (message.ProtocolVersion != 3) { _protocolAccepted = false; _boundedLog.ErrorOnce("protocol-mismatch-client", $"[HTFMoreHead][Stage3][PROTOCOL] Host={message.ProtocolVersion}, " + $"Local={(byte)3}; custom synchronization disabled."); return; } switch (message.Command) { case CosmeticCommand.ServerHello: _protocolAccepted = message.Accepted != 0; if (!_protocolAccepted) { ClearClientState(); break; } _logger.LogInfo((object)$"[HTFMoreHead][Stage3][HELLO_ACK] Accepted=True, Revision={message.Revision}."); if (!string.Equals(message.CatalogDigest, _catalog.CatalogDigest, StringComparison.Ordinal)) { _boundedLog.WarningOnce("catalog-mismatch-client", "[HTFMoreHead][Stage3][CATALOG] Host catalog differs; missing content will be skipped locally."); } break; case CosmeticCommand.EquipDelta: { CosmeticLoadout cosmeticLoadout = ReadLoadout(message); if (_protocolAccepted && message.SubjectClientId >= 0 && cosmeticLoadout != null) { _clientState[message.SubjectClientId] = cosmeticLoadout.Clone(); _appearance.SetLoadout(message.SubjectClientId, cosmeticLoadout); _logger.LogInfo((object)($"[HTFMoreHead][DELTA_APPLY] ClientId={message.SubjectClientId}, " + $"Loadout={FormatLoadout(cosmeticLoadout)}, Revision={message.Revision}.")); } break; } case CosmeticCommand.SnapshotBegin: BeginSnapshot(message); break; case CosmeticCommand.SnapshotEntry: AddSnapshotEntry(message); break; case CosmeticCommand.SnapshotEnd: EndSnapshot(message); break; case CosmeticCommand.ClearPlayer: _clientState.Remove(message.SubjectClientId); _appearance.RemovePlayer(message.SubjectClientId, clearLoadout: true); break; case CosmeticCommand.EquipRequest: case CosmeticCommand.SnapshotRequest: break; } } private void BeginSnapshot(CosmeticMessage message) { if (!_protocolAccepted || message.EntryCount < 0 || message.EntryCount > 32) { _pendingSnapshotRevision = -1; _pendingSnapshot.Clear(); } else { _pendingSnapshotRevision = message.Revision; _pendingSnapshotExpected = message.EntryCount; _pendingSnapshot.Clear(); } } private void AddSnapshotEntry(CosmeticMessage message) { CosmeticLoadout cosmeticLoadout = ReadLoadout(message); if (_pendingSnapshotRevision == message.Revision && message.SubjectClientId >= 0 && cosmeticLoadout != null && _pendingSnapshot.Count < 32) { _pendingSnapshot[message.SubjectClientId] = cosmeticLoadout; } } private void EndSnapshot(CosmeticMessage message) { if (_pendingSnapshotRevision != message.Revision || message.EntryCount != _pendingSnapshotExpected || _pendingSnapshot.Count != _pendingSnapshotExpected) { _boundedLog.WarningOnce("snapshot-incomplete-" + message.Revision, $"[HTFMoreHead][Stage3][SNAPSHOT] Incomplete Revision={message.Revision}, " + $"Expected={_pendingSnapshotExpected}, Received={_pendingSnapshot.Count}."); _pendingSnapshotRevision = -1; _pendingSnapshot.Clear(); return; } _clientState.Clear(); foreach (KeyValuePair<int, CosmeticLoadout> item in _pendingSnapshot) { _clientState[item.Key] = item.Value.Clone(); } _appearance.ReplaceSnapshot(_clientState); _logger.LogInfo((object)($"[HTFMoreHead][Stage3][SNAPSHOT] Applied Revision={message.Revision}, " + $"Entries={_clientState.Count}.")); _pendingSnapshotRevision = -1; _pendingSnapshot.Clear(); } private void OnClientConnectionState(ClientConnectionStateArgs args) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) try { OnClientConnectionStateCore(args); } catch (Exception arg) { _boundedLog.ErrorOnce("client-connection-exception", $"[HTFMoreHead][Stage3][ERROR] Client connection event failed safely: {arg}"); } } private void OnClientConnectionStateCore(ClientConnectionStateArgs args) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 if ((int)args.ConnectionState == 8) { SendHello("ConnectionStarted"); } else if ((int)args.ConnectionState == 1) { _logger.LogInfo((object)"[HTFMoreHead][Stage3][NETWORK] Local connection stopped; session state cleared."); ClearSessionState(); } } private void OnRemoteConnectionState(NetworkConnection connection, RemoteConnectionStateArgs args) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) try { OnRemoteConnectionStateCore(connection, args); } catch (Exception arg) { _boundedLog.ErrorOnce("remote-connection-exception", $"[HTFMoreHead][Stage3][ERROR] Remote connection event failed safely: {arg}"); } } private void OnRemoteConnectionStateCore(NetworkConnection connection, RemoteConnectionStateArgs args) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001c: 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) if (!((Object)(object)_networkManager == (Object)null) && _networkManager.IsServerStarted && (int)args.ConnectionState == 0) { int connectionId = args.ConnectionId; if (_hostState.Remove(connectionId)) { _nextEquipAt.Remove(connectionId); _acceptedClients.Remove(connectionId); _revision++; _networkManager.ServerManager.Broadcast<CosmeticMessage>(new CosmeticMessage { ProtocolVersion = 3, Command = CosmeticCommand.ClearPlayer, SubjectClientId = connectionId, Revision = _revision }, true, (Channel)0); } } } private void ClearSessionState() { _protocolAccepted = false; _hostState.Clear(); _nextEquipAt.Clear(); _acceptedClients.Clear(); ClearClientState(); _revision = 0; } private void ClearClientState() { _clientState.Clear(); _pendingSnapshot.Clear(); _pendingSnapshotRevision = -1; _pendingSnapshotExpected = 0; _appearance.ClearAll(); } private static bool IsValidCosmeticId(string cosmeticId) { if (!string.IsNullOrEmpty(cosmeticId)) { if (cosmeticId.Length <= 193) { return NetworkCosmeticId.IsMatch(cosmeticId); } return false; } return true; } private static bool IsValidLoadout(CosmeticLoadout loadout) { if (loadout != null && IsValidCosmeticId(loadout.HatId) && IsValidCosmeticId(loadout.AccessoryId)) { return IsValidCosmeticId(loadout.OutfitId); } return false; } private static CosmeticLoadout ReadLoadout(CosmeticMessage message) { CosmeticLoadout cosmeticLoadout = new CosmeticLoadout { HatId = (message.HatCosmeticId ?? string.Empty), AccessoryId = (message.AccessoryCosmeticId ?? string.Empty), OutfitId = (message.OutfitCosmeticId ?? string.Empty) }; if (!IsValidLoadout(cosmeticLoadout)) { return null; } return cosmeticLoadout; } private static bool LoadoutEquals(CosmeticLoadout left, CosmeticLoadout right) { if (left != null && right != null && string.Equals(left.HatId, right.HatId, StringComparison.Ordinal) && string.Equals(left.AccessoryId, right.AccessoryId, StringComparison.Ordinal)) { return string.Equals(left.OutfitId, right.OutfitId, StringComparison.Ordinal); } return false; } private static string FormatLoadout(CosmeticLoadout loadout) { loadout = loadout ?? new CosmeticLoadout(); return "[Hat=" + DisplayId(loadout.HatId) + ", Accessory=" + DisplayId(loadout.AccessoryId) + ", Outfit=" + DisplayId(loadout.OutfitId) + "]"; } private static string DisplayId(string value) { if (!string.IsNullOrEmpty(value)) { return value; } return "<native>"; } } } namespace HowToFish.MoreHead.Diagnostics { internal sealed class BoundedLog { private readonly ManualLogSource _logger; private readonly int _maximumKeys; private readonly object _gate = new object(); private readonly HashSet<string> _onceKeys = new HashSet<string>(StringComparer.Ordinal); private bool _capacityWarningWritten; internal BoundedLog(ManualLogSource logger, int maximumKeys) { _logger = logger ?? throw new ArgumentNullException("logger"); if (maximumKeys <= 0) { throw new ArgumentOutOfRangeException("maximumKeys"); } _maximumKeys = maximumKeys; } internal void WarningOnce(string key, string message) { if (TryAddKey(key)) { _logger.LogWarning((object)message); } } internal void ErrorOnce(string key, string message) { if (TryAddKey(key)) { _logger.LogError((object)message); } } private bool TryAddKey(string key) { lock (_gate) { if (_onceKeys.Contains(key)) { return false; } if (_onceKeys.Count >= _maximumKeys) { if (!_capacityWarningWritten) { _capacityWarningWritten = true; _logger.LogWarning((object)"[HTFMoreHead][Stage0][LOG] One-shot diagnostic key capacity reached; additional repeated diagnostics will be suppressed."); } return false; } return _onceKeys.Add(key); } } } } namespace HowToFish.MoreHead.Content { internal sealed class CosmeticCatalog { private const string ManifestFileName = "manifest.json"; private const string EmbeddedManifestFileName = "htfmorehead.json"; private readonly ManualLogSource _logger; private readonly BoundedLog _boundedLog; private readonly Dictionary<string, CosmeticDefinition> _byId = new Dictionary<string, CosmeticDefinition>(StringComparer.Ordinal); private readonly List<CosmeticDefinition> _heads = new List<CosmeticDefinition>(); private readonly List<CosmeticDefinition> _accessories = new List<CosmeticDefinition>(); private readonly List<CosmeticDefinition> _outfits = new List<CosmeticDefinition>(); private readonly List<AssetBundle> _loadedBundles = new List<AssetBundle>(); private readonly List<string> _catalogTokens = new List<string>(); internal int HeadCount => _heads.Count; internal int AccessoryCount => _accessories.Count; internal int OutfitCount => _outfits.Count; internal string CatalogDigest { get; private set; } = "empty"; internal CosmeticDefinition FirstHead { get { if (_heads.Count <= 0) { return null; } return _heads[0]; } } internal CosmeticCatalog(ManualLogSource logger, BoundedLog boundedLog) { _logger = logger ?? throw new ArgumentNullException("logger"); _boundedLog = boundedLog ?? throw new ArgumentNullException("boundedLog"); } internal CosmeticDefinition GetHeadAt(int index) { if (index < 0 || index >= _heads.Count) { return null; } return _heads[index]; } internal int GetHeadIndex(string id) { return GetIndex(CosmeticSlot.Hat, id); } internal int GetCount(CosmeticSlot slot) { return GetList(slot).Count; } internal CosmeticDefinition GetAt(CosmeticSlot slot, int index) { List<CosmeticDefinition> list = GetList(slot); if (index < 0 || index >= list.Count) { return null; } return list[index]; } internal int GetIndex(CosmeticSlot slot, string id) { if (string.IsNullOrEmpty(id)) { return -1; } List<CosmeticDefinition> list = GetList(slot); for (int i = 0; i < list.Count; i++) { if (string.Equals(list[i].Id, id, StringComparison.Ordinal)) { return i; } } return -1; } private List<CosmeticDefinition> GetList(CosmeticSlot slot) { return slot switch { CosmeticSlot.Accessory => _accessories, CosmeticSlot.Outfit => _outfits, _ => _heads, }; } internal void LoadAll(string pluginRoot) { UnloadAll(); if (string.IsNullOrWhiteSpace(pluginRoot) || !Directory.Exists(pluginRoot)) { _logger.LogInfo((object)("[HTFMoreHead][Stage2][CONTENT] Plugin directory not found: \"" + pluginRoot + "\".")); return; } string[] files = Directory.GetFiles(pluginRoot, "*.htfhhh", SearchOption.AllDirectories); Array.Sort(files, (IComparer<string>?)StringComparer.OrdinalIgnoreCase); for (int i = 0; i < files.Length; i++) { LoadEmbeddedPack(files[i]); } string text = Path.Combine(pluginRoot, "HTFMoreHead", "Content"); string[] array = (Directory.Exists(text) ? Directory.GetFiles(text, "manifest.json", SearchOption.AllDirectories) : Array.Empty<string>())