Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of PortalOresAndTeleportMarkers v2.3.1
plugins\PortalOresAndTeleportMarkers\UnrestrictedMineralPortals.dll
Decompiled 20 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using AndersonDavid.UnrestrictedMineralPortals.Commands; using AndersonDavid.UnrestrictedMineralPortals.Configuration; using AndersonDavid.UnrestrictedMineralPortals.Localization; using AndersonDavid.UnrestrictedMineralPortals.Models; using AndersonDavid.UnrestrictedMineralPortals.Patches; using AndersonDavid.UnrestrictedMineralPortals.Persistence; using AndersonDavid.UnrestrictedMineralPortals.Services; using AndersonDavid.UnrestrictedMineralPortals.Utilities; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("masukito")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright © masukito 2026")] [assembly: AssemblyDescription("Transport ores and restricted items through Valheim portals, create personal teleport markers and travel to them using /tp commands.")] [assembly: AssemblyFileVersion("2.3.1.0")] [assembly: AssemblyInformationalVersion("2.3.1+178cf3f3ad62dcff9811deaa21d1cad47e627423")] [assembly: AssemblyProduct("Portal Ores & Teleport Markers")] [assembly: AssemblyTitle("Portal Ores & Teleport Markers")] [assembly: AssemblyVersion("2.3.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace AndersonDavid.UnrestrictedMineralPortals { internal static class ModInfo { public const string ProjectName = "UnrestrictedMineralPortals"; public const string DisplayName = "Portal Ores & Teleport Markers"; public const string AssemblyName = "UnrestrictedMineralPortals"; public const string Guid = "com.andersondavid.valheim.unrestrictedmineralportals"; public const string Version = "2.3.1"; public const string Author = "masukito"; public const string Authors = "Anderson David & masukito"; } [BepInPlugin("com.andersondavid.valheim.unrestrictedmineralportals", "Portal Ores & Teleport Markers", "2.3.1")] public sealed class Plugin : BaseUnityPlugin { private Harmony? _harmony; private ConsoleCommand? _teleportMarkerCommand; private TeleportMarkerService? _teleportMarkerService; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; try { ModConfig.Bind(((BaseUnityPlugin)this).Config); TeleportMarkerConfig.Bind(((BaseUnityPlugin)this).Config); LanguageManager.Initialize(); _harmony = new Harmony("com.andersondavid.valheim.unrestrictedmineralportals"); InitializeTeleportMarkers(); MethodInfo methodInfo = GameCompatibility.FindTargetMethod(); if ((object)methodInfo == null) { Log.LogError((object)"Compatibility check failed: Inventory.IsTeleportable() was not found. No patches were applied; original game behavior is preserved."); return; } MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(PortalRestrictionPatch), "Postfix", (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Patch strategy: postfix on Inventory.IsTeleportable()."); ApplyItemIconPatches(); Log.LogInfo((object)"Portal Ores & Teleport Markers 2.3.1 loaded successfully."); } catch (Exception arg) { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; Log.LogError((object)string.Format("Failed to initialize {0}; original game behavior is preserved.\n{1}", "Portal Ores & Teleport Markers", arg)); } } private void InitializeTeleportMarkers() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Expected O, but got Unknown //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown try { string path = Path.Combine(Paths.ConfigPath, "UnrestrictedMineralPortals", "teleport-markers.json"); if ((int)SystemInfo.graphicsDeviceType == 4) { if (File.Exists(path)) { Log.LogWarning((object)"A server-synchronized marker file was found but server synchronization is disabled in this version. The file was preserved."); } Log.LogInfo((object)"Personal teleport markers are not initialized on a dedicated server."); return; } TeleportMarkerRepository teleportMarkerRepository = new TeleportMarkerRepository(path); int num = teleportMarkerRepository.Load(); if (teleportMarkerRepository.LoadError != null) { Log.LogError((object)("Failed to read teleport marker data. Existing data will not be overwritten: " + teleportMarkerRepository.LoadError)); } _teleportMarkerService = new TeleportMarkerService(teleportMarkerRepository); TeleportService teleport = new TeleportService(); ConnectedPlayerService connectedPlayerService = new ConnectedPlayerService(); TeleportCommandHandler teleportCommandHandler = new TeleportCommandHandler(_teleportMarkerService, teleport, connectedPlayerService); _teleportMarkerCommand = new ConsoleCommand("tp", "Personal teleport markers", new ConsoleEvent(teleportCommandHandler.Handle), false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); ApplyAutocompletePatches(new TeleportAutoCompleteController(_teleportMarkerService, connectedPlayerService)); Log.LogInfo((object)"Player teleport command initialized successfully."); Log.LogInfo((object)$"Local teleport markers initialized successfully. {num} marker(s) loaded."); if (ModConfig.DebugLogging.Value) { Log.LogDebug((object)("Teleport marker storage: " + teleportMarkerRepository.FilePath)); } } catch (Exception arg) { Log.LogError((object)$"Failed to initialize teleport markers. Portal features remain available.\n{arg}"); } } private void Update() { LanguageManager.Tick(); ItemRestrictionUiService.Tick(); } private void ApplyAutocompletePatches(TeleportAutoCompleteController controller) { //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Expected O, but got Unknown //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Expected O, but got Unknown //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown MethodInfo methodInfo = null; MethodInfo methodInfo2 = null; MethodInfo methodInfo3 = null; MethodInfo methodInfo4 = null; try { methodInfo = AccessTools.DeclaredMethod(typeof(Terminal), "UpdateInput", Type.EmptyTypes, (Type[])null); methodInfo2 = AccessTools.DeclaredMethod(typeof(Chat), "Hide", Type.EmptyTypes, (Type[])null); methodInfo3 = AccessTools.DeclaredMethod(typeof(Chat), "SendInput", Type.EmptyTypes, (Type[])null); methodInfo4 = AccessTools.DeclaredMethod(typeof(Chat), "Update", Type.EmptyTypes, (Type[])null); if ((object)methodInfo == null || (object)methodInfo2 == null || (object)methodInfo3 == null || (object)methodInfo4 == null || !ChatAutoCompletePatch.IsCompatible) { throw new MissingMethodException("Expected Terminal.UpdateInput(), Chat.Update(), Chat.Hide(), Chat.SendInput(), or Terminal.m_input was not found."); } ChatAutoCompletePatch.Initialize(controller); MethodInfo methodInfo5 = AccessTools.DeclaredMethod(typeof(ChatAutoCompletePatch), "UpdateInputPrefix", (Type[])null, (Type[])null); MethodInfo methodInfo6 = AccessTools.DeclaredMethod(typeof(ChatAutoCompletePatch), "CancelPostfix", (Type[])null, (Type[])null); MethodInfo methodInfo7 = AccessTools.DeclaredMethod(typeof(ChatAutoCompletePatch), "ChatUpdatePostfix", (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo5), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(methodInfo6), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(methodInfo6), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _harmony.Patch((MethodBase)methodInfo4, (HarmonyMethod)null, new HarmonyMethod(methodInfo7), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"Teleport marker autocomplete initialized successfully."); if (ModConfig.DebugLogging.Value) { Log.LogDebug((object)"Autocomplete target: Terminal.UpdateInput() on focused Chat instances; input: Terminal.m_input (TMP_InputField)."); } } catch (Exception arg) { if ((object)methodInfo != null) { Harmony? harmony = _harmony; if (harmony != null) { harmony.Unpatch((MethodBase)methodInfo, (HarmonyPatchType)1, "com.andersondavid.valheim.unrestrictedmineralportals"); } } if ((object)methodInfo2 != null) { Harmony? harmony2 = _harmony; if (harmony2 != null) { harmony2.Unpatch((MethodBase)methodInfo2, (HarmonyPatchType)2, "com.andersondavid.valheim.unrestrictedmineralportals"); } } if ((object)methodInfo3 != null) { Harmony? harmony3 = _harmony; if (harmony3 != null) { harmony3.Unpatch((MethodBase)methodInfo3, (HarmonyPatchType)2, "com.andersondavid.valheim.unrestrictedmineralportals"); } } if ((object)methodInfo4 != null) { Harmony? harmony4 = _harmony; if (harmony4 != null) { harmony4.Unpatch((MethodBase)methodInfo4, (HarmonyPatchType)2, "com.andersondavid.valheim.unrestrictedmineralportals"); } } Log.LogError((object)$"Failed to initialize teleport marker autocomplete. Other mod features remain available.\n{arg}"); } } private void ApplyItemIconPatches() { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown MethodInfo methodInfo = null; try { Log.LogInfo((object)(ItemRestrictionUiService.IsCompatible ? "RestrictionIconPatch: Active (timed InventoryGrid element refresh)." : "RestrictionIconPatch: Failed (compatible element fields not found). Portal transport remains active.")); } catch (Exception ex) { Log.LogError((object)("RestrictionIconPatch: Failed. Portal transport remains active.\n" + ex)); } try { methodInfo = GameCompatibility.FindTooltipMethod(); MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(ItemTeleportRestrictionIconPatch), "TooltipPostfix", (Type[])null, (Type[])null); if ((object)methodInfo == null || (object)methodInfo2 == null) { Log.LogWarning((object)"TooltipPatch: Failed (compatible method not found). Icon hiding remains active."); } else { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log.LogInfo((object)"TooltipPatch: Active."); } } catch (Exception arg) { if ((object)methodInfo != null) { _harmony.Unpatch((MethodBase)methodInfo, (HarmonyPatchType)2, "com.andersondavid.valheim.unrestrictedmineralportals"); } Log.LogError((object)$"TooltipPatch: Failed. Icon hiding and portal transport remain active.\n{arg}"); } if (ModConfig.DebugLogging.Value) { Log.LogDebug((object)GameCompatibility.CompatibilityReport()); } } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; } } } namespace AndersonDavid.UnrestrictedMineralPortals.Utilities { internal static class GameCompatibility { internal const string TargetType = "Inventory"; internal const string TargetMethod = "IsTeleportable"; internal static MethodInfo? FindTargetMethod() { Type type = AccessTools.TypeByName("Inventory"); if ((object)type != null) { return AccessTools.DeclaredMethod(type, "IsTeleportable", Type.EmptyTypes, (Type[])null); } return null; } internal static FieldInfo? FindInventoryElementsField() { return AccessTools.DeclaredField(typeof(InventoryGrid), "m_elements"); } internal static FieldInfo? FindRestrictionIconField() { Type type = AccessTools.Inner(typeof(InventoryGrid), "Element"); if ((object)type != null) { return AccessTools.DeclaredField(type, "m_noteleport"); } return null; } internal static MethodInfo? FindTooltipMethod() { Type type = AccessTools.Inner(typeof(ItemDrop), "ItemData"); if ((object)type != null) { return AccessTools.DeclaredMethod(type, "GetTooltip", new Type[5] { type, typeof(int), typeof(bool), typeof(float), typeof(int) }, (Type[])null); } return null; } internal static string CompatibilityReport() { return "Valheim compatibility report:\n- Inventory grid update method: none exists; timed active-grid refresh used\n- Restriction icon member: " + (FindRestrictionIconField()?.ToString() ?? "not found") + "\n- Tooltip method: " + (FindTooltipMethod()?.ToString() ?? "not found"); } } } namespace AndersonDavid.UnrestrictedMineralPortals.Services { internal sealed class ConnectedPlayerService { internal enum CurrentState { Ready, Disconnected, PositionUnavailable, PositionNotShared } internal enum PositionSource { PlayerTransform, Zdo, PublicMapData } internal bool IsAvailable => ZNet.instance != null; internal List<ConnectedPlayerInfo> ListOthers() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: 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_0155: Unknown result type (might be due to invalid IL or missing references) if (!IsAvailable) { return new List<ConnectedPlayerInfo>(); } ZDOID local = ZNet.instance.LocalPlayerCharacterID; List<ConnectedPlayerInfo> list = (from x in ZNet.instance.GetPlayerList() where x.m_characterID != local && !string.IsNullOrWhiteSpace(x.m_name) select new ConnectedPlayerInfo(x.m_characterID, x.m_name.Trim(), PlatformDisplayName(x)) into x group x by x.CharacterId into x select x.First()).OrderBy<ConnectedPlayerInfo, string>((ConnectedPlayerInfo x) => x.Name, StringComparer.CurrentCultureIgnoreCase).ToList(); if (ModConfig.DebugLogging.Value) { foreach (ConnectedPlayerInfo item in list) { Player val = FindActivePlayer(item.CharacterId); ManualLogSource log = Plugin.Log; object[] obj = new object[5] { item.CharacterName, item.PlatformDisplayName, val != null, val != null, null }; ZDOMan instance = ZDOMan.instance; obj[4] = ((instance != null) ? instance.GetZDO(item.CharacterId) : null) != null; log.LogDebug((object)string.Format("Connected player discovered: characterName='{0}', platformDisplayName='{1}', hasPlayerInstance={2}, hasTransform={3}, hasZNetView={4}.", obj)); } } return list; } internal CurrentState ResolveCurrent(ConnectedPlayerInfo player, out Vector3 position, out Quaternion rotation, out PositionSource source) { //IL_000e: 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_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: 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_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: 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) position = default(Vector3); rotation = Quaternion.identity; source = PositionSource.PlayerTransform; if (!IsAvailable) { return CurrentState.Disconnected; } PlayerInfo val = ((IEnumerable<PlayerInfo>)ZNet.instance.GetPlayerList()).FirstOrDefault((Func<PlayerInfo, bool>)((PlayerInfo x) => x.m_characterID == player.CharacterId)); if (val.m_characterID != player.CharacterId) { return CurrentState.Disconnected; } Player val2 = FindActivePlayer(player.CharacterId); if (val2 != null) { position = ((Component)val2).transform.position; rotation = ((Component)val2).transform.rotation; source = PositionSource.PlayerTransform; if (Valid(position) && Valid(rotation)) { return CurrentState.Ready; } } ZDOMan instance = ZDOMan.instance; ZDO val3 = ((instance != null) ? instance.GetZDO(player.CharacterId) : null); if (val3 != null) { position = val3.GetPosition(); rotation = val3.GetRotation(); source = PositionSource.Zdo; if (Valid(position) && Valid(rotation)) { return CurrentState.Ready; } } if (!val.m_publicPosition) { return CurrentState.PositionNotShared; } position = val.m_position; rotation = Quaternion.identity; source = PositionSource.PublicMapData; if (!Valid(position)) { return CurrentState.PositionUnavailable; } return CurrentState.Ready; } private static Player? FindActivePlayer(ZDOID characterId) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) return ((IEnumerable<Player>)Player.GetAllPlayers()).FirstOrDefault((Func<Player, bool>)((Player player) => player != null && (Object)(object)player != (Object)(object)Player.m_localPlayer && ((Component)player).gameObject.activeInHierarchy && ((Character)player).GetZDOID() == characterId)); } private static string PlatformDisplayName(PlayerInfo player) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return player.m_serverAssignedDisplayName ?? string.Empty; } private static bool Valid(Vector3 value) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) if (Finite(value.x) && Finite(value.y)) { return Finite(value.z); } return false; } private static bool Valid(Quaternion value) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) if (Finite(value.x) && Finite(value.y) && Finite(value.z)) { return Finite(value.w); } return false; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal static class ItemRestrictionUiService { internal const string TooltipLocalizationKey = "$item_noteleport"; private const string TooltipMarkup = "<color=orange>$item_noteleport</color>"; private static readonly FieldInfo? ElementsField = GameCompatibility.FindInventoryElementsField(); private static readonly FieldInfo? NoTeleportField = GameCompatibility.FindRestrictionIconField(); private static float _nextRefresh; private static bool _loggedComponentFound; private static bool _loggedComponentMissing; internal static bool IsCompatible { get { if ((object)ElementsField != null) { return (object)NoTeleportField != null; } return false; } } internal static void Tick() { if (ModConfig.Enabled.Value && IsCompatible && !(Time.unscaledTime < _nextRefresh)) { _nextRefresh = Time.unscaledTime + 0.25f; InventoryGrid[] array = Object.FindObjectsByType<InventoryGrid>((FindObjectsSortMode)0); for (int i = 0; i < array.Length; i++) { HideRestrictionIcons(array[i]); } } } internal static int HideRestrictionIcons(InventoryGrid grid) { if (!ModConfig.Enabled.Value || grid == null || !(ElementsField?.GetValue(grid) is IEnumerable enumerable)) { return 0; } int num = 0; foreach (object item in enumerable) { if (item != null) { object? obj = NoTeleportField?.GetValue(item); Component val = (Component)((obj is Component) ? obj : null); if (val != null && val.gameObject.activeSelf) { val.gameObject.SetActive(false); num++; } } } if (ModConfig.DebugLogging.Value && num > 0 && !_loggedComponentFound) { Plugin.Log.LogDebug((object)$"InventoryGrid.Element.m_noteleport found; hidden in {num} visible slot(s). Further UI updates will not be logged."); _loggedComponentFound = true; } else if (ModConfig.DebugLogging.Value && num == 0 && !_loggedComponentMissing) { Plugin.Log.LogDebug((object)"Active InventoryGrid scan completed without an active m_noteleport component to hide."); _loggedComponentMissing = true; } return num; } internal static string RemoveRestrictionTooltip(string tooltip) { if (!ModConfig.Enabled.Value || string.IsNullOrEmpty(tooltip)) { return tooltip; } return tooltip.Replace("\n<color=orange>$item_noteleport</color>", string.Empty).Replace("<color=orange>$item_noteleport</color>", string.Empty); } } internal enum MarkerOperationStatus { Success, Duplicate, LimitReached, NotFound, SaveFailed } internal sealed class MarkerOperationResult { internal MarkerOperationStatus Status { get; set; } internal string? Error { get; set; } } internal static class PlayerIdentityService { internal static bool TryGet(out string playerId) { playerId = string.Empty; Game instance = Game.instance; PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null); if (val == null) { return false; } playerId = val.GetPlayerID().ToString(CultureInfo.InvariantCulture); return true; } } internal enum PlayerTeleportStatus { Success, Disconnected, PositionUnavailable, PositionNotShared, DestinationBlocked, PlayerUnavailable, InProgress, Cooldown, Failed } internal sealed class PlayerTeleportResult { internal PlayerTeleportStatus Status { get; set; } internal int RemainingSeconds { get; set; } } internal sealed class PlayerTeleportService { private readonly ConnectedPlayerService _players; private readonly TeleportService _teleport; internal PlayerTeleportService(ConnectedPlayerService players, TeleportService teleport) { _players = players; _teleport = teleport; } internal PlayerTeleportResult Teleport(Player local, string playerId, ConnectedPlayerInfo target, int cooldown) { //IL_005e: 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_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) Vector3 position; Quaternion rotation; ConnectedPlayerService.PositionSource source; ConnectedPlayerService.CurrentState currentState = _players.ResolveCurrent(target, out position, out rotation, out source); if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Resolving player teleport target: requestedName='{target.CharacterName}', matchedCharacterName='{target.CharacterName}', platformDisplayName='{target.PlatformDisplayName}', resolutionState={currentState}, positionSource={source}, currentPosition={position}."); } switch (currentState) { case ConnectedPlayerService.CurrentState.Disconnected: return Result(PlayerTeleportStatus.Disconnected); case ConnectedPlayerService.CurrentState.PositionUnavailable: return Result(PlayerTeleportStatus.PositionUnavailable); case ConnectedPlayerService.CurrentState.PositionNotShared: return Result(PlayerTeleportStatus.PositionNotShared); default: { if (!TrySafeDestination(local, position, rotation, out var destination)) { return Result(PlayerTeleportStatus.DestinationBlocked); } TeleportAttemptResult teleportAttemptResult = _teleport.TeleportTo(local, playerId, destination, rotation, cooldown); if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Player teleport destination resolved: characterName='{target.CharacterName}', distance={Vector3.Distance(((Component)local).transform.position, position):F1}, destination={destination}."); } return new PlayerTeleportResult { Status = Map(teleportAttemptResult.Status), RemainingSeconds = teleportAttemptResult.RemainingSeconds }; } } } private static bool TrySafeDestination(Player local, Vector3 target, Quaternion rotation, out Vector3 destination) { //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_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012f: 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) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0160: 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_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Unknown result type (might be due to invalid IL or missing references) Vector3 val = rotation * Vector3.right; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude < 0.01f) { val = Vector3.right; } else { ((Vector3)(ref val)).Normalize(); } Vector3 val2 = rotation * Vector3.forward; val2.y = 0f; if (((Vector3)(ref val2)).sqrMagnitude < 0.01f) { val2 = Vector3.forward; } else { ((Vector3)(ref val2)).Normalize(); } CapsuleCollider component = ((Component)local).GetComponent<CapsuleCollider>(); float num = ((component == null) ? 0.45f : Mathf.Max(0.3f, component.radius * Mathf.Max(((Component)local).transform.lossyScale.x, ((Component)local).transform.lossyScale.z))); float height = ((component == null) ? 1.8f : Mathf.Max(num * 2f, component.height * ((Component)local).transform.lossyScale.y)); float num2 = Mathf.Max(1.75f, num * 3.5f); Vector3[] array = (Vector3[])(object)new Vector3[4] { target + val * num2, target - val * num2, target - val2 * num2, target + val2 * num2 }; foreach (Vector3 val3 in array) { if (Free(val3, num, height, local)) { destination = val3; return true; } } destination = default(Vector3); return false; } private static bool Free(Vector3 point, float radius, float height, Player local) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) Vector3 val = point + Vector3.up * (radius + 0.05f); Vector3 val2 = point + Vector3.up * (height - radius); return !Physics.OverlapCapsule(val, val2, radius, -1, (QueryTriggerInteraction)1).Any((Collider x) => x != null && !((Component)x).transform.IsChildOf(((Component)local).transform)); } private static PlayerTeleportStatus Map(TeleportAttemptStatus status) { return status switch { TeleportAttemptStatus.Success => PlayerTeleportStatus.Success, TeleportAttemptStatus.PlayerUnavailable => PlayerTeleportStatus.PlayerUnavailable, TeleportAttemptStatus.InProgress => PlayerTeleportStatus.InProgress, TeleportAttemptStatus.Cooldown => PlayerTeleportStatus.Cooldown, _ => PlayerTeleportStatus.Failed, }; } private static PlayerTeleportResult Result(PlayerTeleportStatus status) { return new PlayerTeleportResult { Status = status }; } } internal static class PortalRestrictionService { internal static bool ShouldAllowTeleport() { return ModConfig.Enabled?.Value ?? false; } } internal sealed class TeleportMarkerService { private readonly TeleportMarkerRepository _repository; internal int Revision { get; private set; } internal TeleportMarkerService(TeleportMarkerRepository repository) { _repository = repository; } internal MarkerOperationResult Create(TeleportMarker marker, int maximum) { MarkerOperationStatus status = MarkerOperationStatus.Success; string error; bool flag = _repository.MutateAndSave(delegate(TeleportMarkerDatabase database) { WorldTeleportMarkers world = GetWorld(database, marker.PlayerId, marker.WorldId, create: true); if (world.Markers.Any((TeleportMarker x) => NamesEqual(x.Name, marker.Name))) { status = MarkerOperationStatus.Duplicate; return false; } if (maximum > 0 && world.Markers.Count >= maximum) { status = MarkerOperationStatus.LimitReached; return false; } world.Markers.Add(marker); return true; }, out error); if (flag && status == MarkerOperationStatus.Success) { Revision++; } return new MarkerOperationResult { Status = (flag ? status : MarkerOperationStatus.SaveFailed), Error = error }; } internal MarkerOperationResult Delete(string playerId, string worldId, string name) { MarkerOperationStatus status = MarkerOperationStatus.NotFound; string error; bool flag = _repository.MutateAndSave(delegate(TeleportMarkerDatabase database) { WorldTeleportMarkers world = GetWorld(database, playerId, worldId, create: false); TeleportMarker teleportMarker = world?.Markers.FirstOrDefault((TeleportMarker x) => NamesEqual(x.Name, name)); if (teleportMarker == null) { return false; } world.Markers.Remove(teleportMarker); status = MarkerOperationStatus.Success; return true; }, out error); if (flag && status == MarkerOperationStatus.Success) { Revision++; } return new MarkerOperationResult { Status = (flag ? status : MarkerOperationStatus.SaveFailed), Error = error }; } internal TeleportMarker? Find(string playerId, string worldId, string name) { return _repository.Read((TeleportMarkerDatabase database) => GetWorld(database, playerId, worldId, create: false)?.Markers.FirstOrDefault((TeleportMarker x) => NamesEqual(x.Name, name))); } internal TeleportMarker? FindInOtherWorld(string playerId, string worldId, string name) { PlayerTeleportMarkers value; return _repository.Read((TeleportMarkerDatabase database) => (!database.Players.TryGetValue(playerId, out value)) ? null : value.Worlds.Where<KeyValuePair<string, WorldTeleportMarkers>>((KeyValuePair<string, WorldTeleportMarkers> x) => x.Key != worldId).SelectMany((KeyValuePair<string, WorldTeleportMarkers> x) => x.Value.Markers).FirstOrDefault((TeleportMarker x) => NamesEqual(x.Name, name))); } internal List<TeleportMarker> List(string playerId, string worldId) { return _repository.Read((TeleportMarkerDatabase database) => (GetWorld(database, playerId, worldId, create: false)?.Markers ?? new List<TeleportMarker>()).OrderBy<TeleportMarker, string>((TeleportMarker x) => x.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); } internal int CountOtherWorlds(string playerId, string worldId) { PlayerTeleportMarkers value; return _repository.Read((TeleportMarkerDatabase database) => database.Players.TryGetValue(playerId, out value) ? value.Worlds.Where<KeyValuePair<string, WorldTeleportMarkers>>((KeyValuePair<string, WorldTeleportMarkers> x) => x.Key != worldId).Sum((KeyValuePair<string, WorldTeleportMarkers> x) => x.Value.Markers.Count) : 0); } internal static string NormalizeName(string name) { return name.Trim(); } private static bool NamesEqual(string left, string right) { return string.Equals(TeleportMarkerMatcher.Normalize(left), TeleportMarkerMatcher.Normalize(right), StringComparison.Ordinal); } private static WorldTeleportMarkers? GetWorld(TeleportMarkerDatabase database, string playerId, string worldId, bool create) { if (!database.Players.TryGetValue(playerId, out PlayerTeleportMarkers value)) { if (!create) { return null; } value = new PlayerTeleportMarkers(); database.Players[playerId] = value; } if (!value.Worlds.TryGetValue(worldId, out WorldTeleportMarkers value2) && create) { value2 = new WorldTeleportMarkers(); value.Worlds[worldId] = value2; } return value2; } } internal enum TeleportAttemptStatus { Success, PlayerUnavailable, InProgress, Cooldown, Failed } internal sealed class TeleportAttemptResult { internal TeleportAttemptStatus Status { get; set; } internal int RemainingSeconds { get; set; } } internal sealed class TeleportService { private readonly Dictionary<string, DateTime> _lastTeleports = new Dictionary<string, DateTime>(StringComparer.Ordinal); internal TeleportAttemptResult Teleport(Player player, string playerId, TeleportMarker marker, int cooldownSeconds) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) return TeleportTo(player, playerId, new Vector3(marker.PositionX, marker.PositionY, marker.PositionZ), new Quaternion(marker.RotationX, marker.RotationY, marker.RotationZ, marker.RotationW), cooldownSeconds); } internal TeleportAttemptResult TeleportTo(Player player, string playerId, Vector3 position, Quaternion rotation, int cooldownSeconds) { //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) if (player == null || Game.instance == null || ZNet.instance == null || ((Character)player).IsDead() || Game.instance.WaitingForRespawn()) { return Result(TeleportAttemptStatus.PlayerUnavailable); } if (((Character)player).IsTeleporting()) { return Result(TeleportAttemptStatus.InProgress); } DateTime utcNow = DateTime.UtcNow; if (cooldownSeconds > 0 && _lastTeleports.TryGetValue(playerId, out var value)) { double num = (double)cooldownSeconds - (utcNow - value).TotalSeconds; if (num > 0.0) { return new TeleportAttemptResult { Status = TeleportAttemptStatus.Cooldown, RemainingSeconds = Math.Max(1, (int)Math.Ceiling(num)) }; } } if (!((Character)player).TeleportTo(position, rotation, true)) { return Result(TeleportAttemptStatus.Failed); } _lastTeleports[playerId] = utcNow; return Result(TeleportAttemptStatus.Success); } private static TeleportAttemptResult Result(TeleportAttemptStatus status) { return new TeleportAttemptResult { Status = status }; } } internal static class WorldIdentityService { internal static bool TryGet(out string worldId) { worldId = string.Empty; if (ZNet.instance == null) { return false; } worldId = ZNet.instance.GetWorldUID().ToString(CultureInfo.InvariantCulture); return true; } } } namespace AndersonDavid.UnrestrictedMineralPortals.Persistence { internal sealed class TeleportMarkerRepository { private readonly object _sync = new object(); private readonly string _path; private TeleportMarkerDatabase _database = new TeleportMarkerDatabase(); private bool _writesAllowed = true; internal string FilePath => _path; internal string? LoadError { get; private set; } internal TeleportMarkerRepository(string path) { _path = path; } internal int Load() { lock (_sync) { if (!File.Exists(_path) || new FileInfo(_path).Length == 0L) { return 0; } try { using FileStream fileStream = File.OpenRead(_path); _database = ((TeleportMarkerDatabase)((XmlObjectSerializer)CreateSerializer()).ReadObject((Stream)fileStream)) ?? new TeleportMarkerDatabase(); Normalize(_database); return CountMarkers(_database); } catch (Exception ex) { LoadError = ex.Message; _writesAllowed = false; return 0; } } } internal T Read<T>(Func<TeleportMarkerDatabase, T> operation) { lock (_sync) { return operation(_database); } } internal bool MutateAndSave(Func<TeleportMarkerDatabase, bool> operation, out string? error) { lock (_sync) { if (!_writesAllowed) { error = "O arquivo existente não pôde ser carregado; a gravação foi bloqueada para preservar os dados."; return false; } if (!operation(_database)) { error = null; return true; } return SaveLocked(out error); } } private bool SaveLocked(out string? error) { string directoryName = Path.GetDirectoryName(_path); string text = _path + ".tmp"; string destFileName = _path + ".bak"; try { Directory.CreateDirectory(directoryName); using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None)) { ((XmlObjectSerializer)CreateSerializer()).WriteObject((Stream)fileStream, (object)_database); fileStream.Flush(flushToDisk: true); } if (File.Exists(_path)) { File.Copy(_path, destFileName, overwrite: true); try { File.Replace(text, _path, null); } catch (Exception ex) when (ex is PlatformNotSupportedException || ex is IOException || ex is UnauthorizedAccessException) { File.Copy(text, _path, overwrite: true); File.Delete(text); } } else { File.Move(text, _path); } error = null; return true; } catch (Exception ex2) { error = ex2.ToString(); try { if (File.Exists(text)) { File.Delete(text); } } catch { } return false; } } private static DataContractJsonSerializer CreateSerializer() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown return new DataContractJsonSerializer(typeof(TeleportMarkerDatabase), new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); } private static void Normalize(TeleportMarkerDatabase database) { if (database.Players == null) { Dictionary<string, PlayerTeleportMarkers> dictionary = (database.Players = new Dictionary<string, PlayerTeleportMarkers>()); } foreach (PlayerTeleportMarkers value in database.Players.Values) { PlayerTeleportMarkers current; PlayerTeleportMarkers playerTeleportMarkers = (current = value); if (current.Worlds == null) { Dictionary<string, WorldTeleportMarkers> dictionary3 = (current.Worlds = new Dictionary<string, WorldTeleportMarkers>()); } foreach (WorldTeleportMarkers value2 in playerTeleportMarkers.Worlds.Values) { if (value2.Markers == null) { List<TeleportMarker> list = (value2.Markers = new List<TeleportMarker>()); } } } } private static int CountMarkers(TeleportMarkerDatabase database) { int num = 0; foreach (PlayerTeleportMarkers value in database.Players.Values) { foreach (WorldTeleportMarkers value2 in value.Worlds.Values) { num += value2.Markers.Count; } } return num; } } } namespace AndersonDavid.UnrestrictedMineralPortals.Patches { internal static class ChatAutoCompletePatch { private static readonly FieldInfo? InputField = AccessTools.Field(typeof(Terminal), "m_input"); private static TeleportAutoCompleteController? _controller; internal static bool IsCompatible => (object)InputField != null; internal static void Initialize(TeleportAutoCompleteController controller) { _controller = controller; } internal static bool UpdateInputPrefix(Terminal __instance) { if (TeleportMarkerConfig.Enabled.Value && TeleportMarkerConfig.AutoCompleteEnabled.Value) { Chat val = (Chat)(object)((__instance is Chat) ? __instance : null); if (val != null && !((Object)(object)Chat.instance != (Object)(object)val) && val.HasFocus() && _controller != null) { object? obj = InputField?.GetValue(val); TMP_InputField val2 = (TMP_InputField)((obj is TMP_InputField) ? obj : null); if (val2 == null || !val2.isFocused || !ZInput.GetKeyDown((KeyCode)9, true)) { return true; } bool reverse = ZInput.GetKey((KeyCode)304, true) || ZInput.GetKey((KeyCode)303, true); if (!_controller.TryComplete(val2.text, val2.caretPosition, reverse, out string completedText)) { return true; } val2.text = completedText; val2.caretPosition = completedText.Length; val2.selectionAnchorPosition = completedText.Length; val2.selectionFocusPosition = completedText.Length; val2.ActivateInputField(); return false; } } return true; } internal static void CancelPostfix() { _controller?.Cancel("chat closed or input submitted"); } internal static void ChatUpdatePostfix(Chat __instance) { if ((Object)(object)Chat.instance == (Object)(object)__instance && !__instance.HasFocus()) { _controller?.Cancel("chat lost focus"); } } } internal static class ItemTeleportRestrictionIconPatch { internal static void TooltipPostfix(ref string __result) { __result = ItemRestrictionUiService.RemoveRestrictionTooltip(__result); } } internal static class PortalRestrictionPatch { internal static void Postfix(ref bool __result) { if (PortalRestrictionService.ShouldAllowTeleport()) { if (!__result && ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)"Overriding Inventory.IsTeleportable result for portal travel."); } __result = true; } } } } namespace AndersonDavid.UnrestrictedMineralPortals.Models { internal sealed class ConnectedPlayerInfo { internal ZDOID CharacterId { get; } internal string CharacterName { get; } internal string PlatformDisplayName { get; } internal string Name => CharacterName; internal ConnectedPlayerInfo(ZDOID characterId, string characterName, string platformDisplayName) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) CharacterId = characterId; CharacterName = characterName; PlatformDisplayName = platformDisplayName; } } [DataContract] internal sealed class PlayerTeleportMarkers { [DataMember(Order = 1)] public Dictionary<string, WorldTeleportMarkers> Worlds { get; set; } = new Dictionary<string, WorldTeleportMarkers>(); } [DataContract] internal sealed class TeleportMarker { [DataMember(Order = 1)] public string Name { get; set; } = string.Empty; [DataMember(Order = 2)] public float PositionX { get; set; } [DataMember(Order = 3)] public float PositionY { get; set; } [DataMember(Order = 4)] public float PositionZ { get; set; } [DataMember(Order = 5)] public float RotationX { get; set; } [DataMember(Order = 6)] public float RotationY { get; set; } [DataMember(Order = 7)] public float RotationZ { get; set; } [DataMember(Order = 8)] public float RotationW { get; set; } [DataMember(Order = 9)] public string WorldId { get; set; } = string.Empty; [DataMember(Order = 10)] public string PlayerId { get; set; } = string.Empty; [DataMember(Order = 11)] public DateTime CreatedAt { get; set; } [DataMember(Order = 13)] public long CreatedAtUnix { get; set; } } [DataContract] internal sealed class TeleportMarkerDatabase { [DataMember(Order = 1)] public Dictionary<string, PlayerTeleportMarkers> Players { get; set; } = new Dictionary<string, PlayerTeleportMarkers>(); } [DataContract] internal sealed class WorldTeleportMarkers { [DataMember(Order = 1)] public List<TeleportMarker> Markers { get; set; } = new List<TeleportMarker>(); } } namespace AndersonDavid.UnrestrictedMineralPortals.Localization { internal static class EnglishMessages { internal static readonly IReadOnlyDictionary<string, string> Values = new Dictionary<string, string> { ["command.invalid"] = "Invalid command. Use /tp help to view the commands.", ["marker.empty"] = "You do not have any markers yet.\nUse /tp create markerName to create one.", ["marker.disabled"] = "Teleport markers are disabled.", ["marker.multiplayer_disabled"] = "Teleport markers are disabled on this server.", ["marker.player_unavailable"] = "The local player or world is not available yet.", ["marker.in_progress"] = "A teleport is already in progress.", ["marker.save_error"] = "Markers could not be saved. Check LogOutput.log.", ["command.internal_error"] = "An internal error occurred while processing the command.", ["marker.created"] = "Marker \"{0}\" created successfully.\nPosition: {1}", ["marker.duplicate"] = "A marker named \"{0}\" already exists.\nRemove it first with /tp delete {0}.", ["marker.removed"] = "Marker \"{0}\" removed successfully.", ["marker.not_found"] = "Marker \"{0}\" was not found.\nUse /tp list to view your markers.", ["marker.other_world"] = "Marker \"{0}\" belongs to another world and cannot be used here.", ["marker.limit"] = "You reached the limit of {0} markers in this world.", ["marker.cooldown"] = "Wait {0} seconds before using another teleport.", ["marker.teleported"] = "Teleported to \"{0}\".", ["marker.list.header"] = "Your markers: {0}", ["marker.other_world_count"] = "{0} marker(s) exist in other worlds.", ["command.reserved_name"] = "The name \"{0}\" is reserved and cannot be used as a marker.", ["command.help.title"] = "Teleport commands", ["command.help.create"] = "/tp create markerName\nCreates a marker at the player's current position and altitude.", ["command.help.teleport"] = "/tp markerName\nTeleports to an existing marker.", ["command.help.delete"] = "/tp delete markerName\nDeletes a marker created by the current player.", ["command.help.list"] = "/tp list\nLists all markers belonging to the player in the current world.", ["command.help.help"] = "/tp help\nDisplays this command list.", ["command.help.player"] = "/tp player playerName\nTeleports to another player connected to the same world.", ["command.help.autocomplete_tip"] = "Tip: press Tab while typing a marker or player name to use autocomplete.", ["teleport.player.success"] = "Teleported to player \"{0}\".", ["teleport.player.not_found"] = "Player \"{0}\" was not found in this world.\nUse /tp player and press Tab to view available players.", ["teleport.player.disconnected"] = "Player \"{0}\" is no longer connected.", ["teleport.player.self"] = "You cannot teleport to yourself.", ["teleport.player.none_connected"] = "There are no other players connected to this world.", ["teleport.player.multiple_found"] = "Multiple players were found:\n\n{0}\n\nEnter a more specific name or press Tab to autocomplete.", ["teleport.player.multiple_item"] = "- {0}", ["teleport.player.name_required"] = "Enter the player's name.\n\n/tp player playerName", ["teleport.player.invalid_position"] = "Could not obtain a valid position for player \"{0}\".", ["teleport.player.destination_blocked"] = "Could not find a safe position near player \"{0}\".", ["teleport.player.unavailable"] = "Teleporting to players is not available in this game version.", ["teleport.player.feature_disabled"] = "Teleporting to players is disabled.", ["teleport.player.position_unavailable"] = "The position of player \"{0}\" is not available yet. Try again in a few seconds.", ["teleport.player.position_not_shared"] = "Player \"{0}\" is not sharing their position.", ["teleport.player.duplicate_name"] = "Multiple players named \"{0}\" are connected. The destination could not be determined safely." }; } internal static class LanguageCodeNormalizer { internal static string Configured(string? value) { string text = (value ?? string.Empty).Trim().Replace('_', '-').ToLowerInvariant(); switch (text) { case "auto": return "auto"; case "pt": case "pt-br": case "portuguese": case "portuguese-brazil": case "brazilianportuguese": return "pt-BR"; default: if (!text.StartsWith("en-", StringComparison.Ordinal)) { return string.Empty; } goto case "en"; case "en": case "english": return "en"; } } internal static string Game(string? value) { if (!(Configured(value) == "pt-BR")) { return "en"; } return "pt-BR"; } } internal static class LanguageManager { private static string _language = "en"; private static bool _awaitingGameLanguage; private static float _nextAttempt; private static int _attempts; internal static string CurrentLanguage => _language; internal static void Initialize() { string text = ModConfig.Language.Value?.Trim() ?? "auto"; string text2 = LanguageCodeNormalizer.Configured(text); if (text2 == "pt-BR" || text2 == "en") { Select(text2, fromGame: false); return; } if (!text.Equals("auto", StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)("Unsupported configured language \"" + text + "\". Using game language detection.")); } _language = "en"; _awaitingGameLanguage = true; TryDetectGameLanguage(); } internal static void Tick() { if (_awaitingGameLanguage && !(Time.unscaledTime < _nextAttempt)) { _nextAttempt = Time.unscaledTime + 0.5f; if (TryDetectGameLanguage() || ++_attempts >= 20) { _awaitingGameLanguage = false; } } } internal static string Get(string key, params object[] arguments) { if (!((_language == "pt-BR") ? PortugueseBrazilMessages.Values : EnglishMessages.Values).TryGetValue(key, out string value)) { value = key; } if (arguments.Length != 0) { return string.Format(CultureInfo.CurrentCulture, value, arguments); } return value; } internal static bool HaveLanguageParity() { return new HashSet<string>(PortugueseBrazilMessages.Values.Keys).SetEquals(EnglishMessages.Values.Keys); } private static bool TryDetectGameLanguage() { try { Type type = AccessTools.TypeByName("Localization"); if ((object)type == null) { return false; } object obj = AccessTools.Property(type, "instance")?.GetValue(null, null) ?? AccessTools.Field(type, "instance")?.GetValue(null) ?? AccessTools.Field(type, "m_instance")?.GetValue(null); MethodInfo methodInfo = AccessTools.Method(type, "GetSelectedLanguage", Type.EmptyTypes, (Type[])null); if (obj == null || (object)methodInfo == null) { return false; } string value = methodInfo.Invoke(obj, null)?.ToString(); if (string.IsNullOrWhiteSpace(value)) { return false; } Select(LanguageCodeNormalizer.Game(value), fromGame: true); _awaitingGameLanguage = false; return true; } catch (Exception ex) { if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)("Game language detection is not ready: " + ex.Message)); } return false; } } private static void Select(string language, bool fromGame) { _language = language; Plugin.Log.LogInfo((object)(fromGame ? ("Language selected from game settings: " + language + ".") : ("Language selected: " + language + "."))); } } internal static class MessageKeys { internal const string Invalid = "command.invalid"; internal const string Empty = "marker.empty"; internal const string Disabled = "marker.disabled"; internal const string MultiplayerDisabled = "marker.multiplayer_disabled"; internal const string PlayerUnavailable = "marker.player_unavailable"; internal const string InProgress = "marker.in_progress"; internal const string SaveError = "marker.save_error"; internal const string InternalError = "command.internal_error"; internal const string Created = "marker.created"; internal const string Duplicate = "marker.duplicate"; internal const string Removed = "marker.removed"; internal const string NotFound = "marker.not_found"; internal const string OtherWorld = "marker.other_world"; internal const string Limit = "marker.limit"; internal const string Cooldown = "marker.cooldown"; internal const string Teleported = "marker.teleported"; internal const string ListHeader = "marker.list.header"; internal const string OtherWorldCount = "marker.other_world_count"; internal const string ReservedName = "command.reserved_name"; internal const string HelpTitle = "command.help.title"; internal const string HelpCreate = "command.help.create"; internal const string HelpTeleport = "command.help.teleport"; internal const string HelpDelete = "command.help.delete"; internal const string HelpList = "command.help.list"; internal const string HelpHelp = "command.help.help"; internal const string HelpAutocompleteTip = "command.help.autocomplete_tip"; internal const string HelpPlayer = "command.help.player"; internal const string PlayerSuccess = "teleport.player.success"; internal const string PlayerNotFound = "teleport.player.not_found"; internal const string PlayerDisconnected = "teleport.player.disconnected"; internal const string PlayerSelf = "teleport.player.self"; internal const string PlayerNone = "teleport.player.none_connected"; internal const string PlayerMultiple = "teleport.player.multiple_found"; internal const string PlayerMultipleItem = "teleport.player.multiple_item"; internal const string PlayerNameRequired = "teleport.player.name_required"; internal const string PlayerInvalidPosition = "teleport.player.invalid_position"; internal const string PlayerDestinationBlocked = "teleport.player.destination_blocked"; internal const string PlayerTeleportUnavailable = "teleport.player.unavailable"; internal const string PlayerFeatureDisabled = "teleport.player.feature_disabled"; internal const string PlayerPositionUnavailable = "teleport.player.position_unavailable"; internal const string PlayerPositionNotShared = "teleport.player.position_not_shared"; internal const string PlayerDuplicateName = "teleport.player.duplicate_name"; } internal static class PortugueseBrazilMessages { internal static readonly IReadOnlyDictionary<string, string> Values = new Dictionary<string, string> { ["command.invalid"] = "Comando inválido. Use /tp help para visualizar os comandos.", ["marker.empty"] = "Você ainda não possui marcadores.\nUse /tp create nomeMarcador para criar um.", ["marker.disabled"] = "Os marcadores de teletransporte estão desabilitados.", ["marker.multiplayer_disabled"] = "Os marcadores de teletransporte estão desabilitados neste servidor.", ["marker.player_unavailable"] = "O jogador local ou o mundo ainda não está disponível.", ["marker.in_progress"] = "Já existe um teletransporte em andamento.", ["marker.save_error"] = "Não foi possível salvar os marcadores. Consulte o LogOutput.log.", ["command.internal_error"] = "Ocorreu um erro interno ao processar o comando.", ["marker.created"] = "Marcador \"{0}\" criado com sucesso.\nPosição: {1}", ["marker.duplicate"] = "Já existe um marcador chamado \"{0}\".\nRemova-o primeiro com /tp delete {0}.", ["marker.removed"] = "Marcador \"{0}\" removido com sucesso.", ["marker.not_found"] = "Marcador \"{0}\" não encontrado.\nUse /tp list para visualizar seus marcadores.", ["marker.other_world"] = "O marcador \"{0}\" pertence a outro mundo e não pode ser usado aqui.", ["marker.limit"] = "Você atingiu o limite de {0} marcadores neste mundo.", ["marker.cooldown"] = "Aguarde {0} segundos para usar outro teletransporte.", ["marker.teleported"] = "Teletransportado para \"{0}\".", ["marker.list.header"] = "Seus marcadores: {0}", ["marker.other_world_count"] = "{0} marcador(es) existem em outros mundos.", ["command.reserved_name"] = "O nome \"{0}\" é reservado e não pode ser usado como marcador.", ["command.help.title"] = "Comandos de teletransporte", ["command.help.create"] = "/tp create nomeMarcador\nCria um marcador na posição e altitude atuais do jogador.", ["command.help.teleport"] = "/tp nomeMarcador\nTeletransporta para um marcador existente.", ["command.help.delete"] = "/tp delete nomeMarcador\nRemove um marcador criado pelo jogador atual.", ["command.help.list"] = "/tp list\nLista todos os marcadores do jogador no mundo atual.", ["command.help.help"] = "/tp help\nExibe esta lista de comandos.", ["command.help.player"] = "/tp player nomeJogador\nTeletransporta até outro jogador conectado no mesmo mundo.", ["command.help.autocomplete_tip"] = "Dica: pressione Tab enquanto digita o nome de um marcador ou jogador para usar o autocomplete.", ["teleport.player.success"] = "Teletransportado até o jogador \"{0}\".", ["teleport.player.not_found"] = "Jogador \"{0}\" não encontrado neste mundo.\nUse /tp player e pressione Tab para visualizar os jogadores disponíveis.", ["teleport.player.disconnected"] = "O jogador \"{0}\" não está mais conectado.", ["teleport.player.self"] = "Você não pode se teletransportar até si mesmo.", ["teleport.player.none_connected"] = "Não existem outros jogadores conectados neste mundo.", ["teleport.player.multiple_found"] = "Foram encontrados vários jogadores:\n\n{0}\n\nDigite um nome mais específico ou use Tab para completar.", ["teleport.player.multiple_item"] = "- {0}", ["teleport.player.name_required"] = "Informe o nome do jogador.\n\n/tp player nomeJogador", ["teleport.player.invalid_position"] = "Não foi possível obter uma posição válida para o jogador \"{0}\".", ["teleport.player.destination_blocked"] = "Não foi possível encontrar uma posição segura próxima ao jogador \"{0}\".", ["teleport.player.unavailable"] = "O teletransporte até jogadores não está disponível nesta versão do jogo.", ["teleport.player.feature_disabled"] = "O teletransporte até jogadores está desabilitado.", ["teleport.player.position_unavailable"] = "A posição do jogador \"{0}\" ainda não está disponível. Tente novamente em alguns segundos.", ["teleport.player.position_not_shared"] = "A posição do jogador \"{0}\" não está sendo compartilhada.", ["teleport.player.duplicate_name"] = "Existem vários jogadores chamados \"{0}\". Não foi possível determinar o destino com segurança." }; } } namespace AndersonDavid.UnrestrictedMineralPortals.Configuration { internal static class ModConfig { internal static ConfigEntry<bool> Enabled { get; private set; } internal static ConfigEntry<bool> DebugLogging { get; private set; } internal static ConfigEntry<string> Language { get; private set; } internal static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("General", "Enabled", true, "Allow restricted items through portals."); DebugLogging = config.Bind<bool>("General", "DebugLogging", false, "Enable diagnostic logging."); Language = config.Bind<string>("General", "Language", "auto", "Language used by mod messages. Supported values: auto, pt-BR, en."); } } internal static class TeleportMarkerConfig { internal static ConfigEntry<bool> Enabled { get; private set; } internal static ConfigEntry<int> MaxMarkersPerPlayer { get; private set; } internal static ConfigEntry<int> CooldownSeconds { get; private set; } internal static ConfigEntry<bool> AllowInMultiplayer { get; private set; } internal static ConfigEntry<bool> AutoCompleteEnabled { get; private set; } internal static ConfigEntry<int> AutoCompleteMaxSuggestions { get; private set; } internal static ConfigEntry<bool> AutoCompleteFuzzySearch { get; private set; } internal static ConfigEntry<bool> TeleportPlayersEnabled { get; private set; } internal static void Bind(ConfigFile config) { Enabled = config.Bind<bool>("TeleportMarkers", "Enabled", true, "Enable personal /tp marker commands."); MaxMarkersPerPlayer = config.Bind<int>("TeleportMarkers", "MaxMarkersPerPlayer", 50, "Maximum markers per player per world. Zero means unlimited."); CooldownSeconds = config.Bind<int>("TeleportMarkers", "CooldownSeconds", 3, "Seconds between marker teleports. Zero disables cooldown."); AllowInMultiplayer = config.Bind<bool>("TeleportMarkers", "AllowInMultiplayer", true, "Allow marker commands on remote multiplayer servers."); AutoCompleteEnabled = config.Bind<bool>("TeleportMarkers", "AutoCompleteEnabled", true, "Complete marker names with Tab in the normal chat."); AutoCompleteMaxSuggestions = config.Bind<int>("TeleportMarkers", "AutoCompleteMaxSuggestions", 20, "Maximum autocomplete results. Zero means unlimited."); AutoCompleteFuzzySearch = config.Bind<bool>("TeleportMarkers", "AutoCompleteFuzzySearch", true, "Suggest marker names for small typing mistakes."); TeleportPlayersEnabled = config.Bind<bool>("TeleportPlayers", "Enabled", true, "Enable teleporting to other connected players with /tp player playerName."); } } } namespace AndersonDavid.UnrestrictedMineralPortals.Commands { internal static class NameMatcher { internal static List<T> Match<T>(IEnumerable<T> values, Func<T, string> name, string query, bool fuzzy, int maximum) { string normalizedQuery = Normalize(query); double similarity; IEnumerable<T> source = from x in (from x in values select new { Value = x, Name = name(x), Normalized = Normalize(name(x)) } into x select new { Value = x.Value, Name = x.Name, Normalized = x.Normalized, Rank = Rank(x.Normalized, normalizedQuery, fuzzy, out similarity), Similarity = similarity } into x where x.Rank >= 0 orderby x.Rank, x.Similarity descending select x).ThenBy(x => x.Normalized, StringComparer.Ordinal) select x.Value; if (maximum <= 0) { return source.ToList(); } return source.Take(maximum).ToList(); } internal static string Normalize(string value) { string text = (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormD); StringBuilder stringBuilder = new StringBuilder(text.Length); bool flag = false; string text2 = text; foreach (char c in text2) { if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark) { continue; } if (char.IsWhiteSpace(c)) { flag = stringBuilder.Length > 0; continue; } if (flag) { stringBuilder.Append(' '); flag = false; } stringBuilder.Append(char.ToLowerInvariant(c)); } return stringBuilder.ToString().Normalize(NormalizationForm.FormC); } private static int Rank(string name, string query, bool fuzzy, out double similarity) { similarity = 1.0; if (query.Length == 0) { return 1; } if (name == query) { return 0; } if (name.StartsWith(query, StringComparison.Ordinal)) { return 1; } if (name.Split(new char[1] { ' ' }).Any((string word) => word.StartsWith(query, StringComparison.Ordinal))) { return 2; } if (name.Contains(query)) { return 3; } if (!fuzzy) { return -1; } similarity = Similarity(name, query); if (!(similarity >= 0.72)) { return -1; } return 4; } private static double Similarity(string left, string right) { if (left.Length == 0 || right.Length == 0) { return 0.0; } int[] array = new int[right.Length + 1]; int[] array2 = new int[right.Length + 1]; for (int i = 0; i <= right.Length; i++) { array[i] = i; } for (int j = 1; j <= left.Length; j++) { array2[0] = j; for (int k = 1; k <= right.Length; k++) { array2[k] = Math.Min(Math.Min(array2[k - 1] + 1, array[k] + 1), array[k - 1] + ((left[j - 1] != right[k - 1]) ? 1 : 0)); } int[] array3 = array; array = array2; array2 = array3; } return 1.0 - (double)array[right.Length] / (double)Math.Max(left.Length, right.Length); } } internal sealed class PlayerAutoCompleteSession { internal string Prefix { get; set; } = string.Empty; internal List<ConnectedPlayerInfo> Suggestions { get; set; } = new List<ConnectedPlayerInfo>(); internal int Index { get; set; } internal string CompletedText { get; set; } = string.Empty; internal string WorldId { get; set; } = string.Empty; internal string ConnectionSignature { get; set; } = string.Empty; } internal enum PlayerNameResolutionStatus { Found, NotFound, Ambiguous } internal sealed class PlayerNameResolution<T> { internal PlayerNameResolutionStatus Status { get; set; } internal T? Value { get; set; } internal IReadOnlyList<T> Candidates { get; set; } = Array.Empty<T>(); } internal static class PlayerNameResolver { internal static PlayerNameResolution<T> Resolve<T>(IEnumerable<T> values, Func<T, string> name, string query) { List<T> list = values.ToList(); List<T> list2 = list.Where((T x) => name(x) == query).ToList(); if (list2.Count == 1) { return Found(list2[0]); } List<T> list3 = list.Where((T x) => name(x).Equals(query, StringComparison.OrdinalIgnoreCase)).ToList(); if (list3.Count == 1) { return Found(list3[0]); } string normalized = NameMatcher.Normalize(query); List<T> list4 = list.Where((T x) => NameMatcher.Normalize(name(x)) == normalized).ToList(); if (list4.Count == 1) { return Found(list4[0]); } List<T> list5 = NameMatcher.Match(list, name, query, fuzzy: false, 0); if (list5.Count == 1) { return Found(list5[0]); } return new PlayerNameResolution<T> { Status = ((list5.Count <= 1) ? PlayerNameResolutionStatus.NotFound : PlayerNameResolutionStatus.Ambiguous), Candidates = list5 }; } private static PlayerNameResolution<T> Found<T>(T value) { PlayerNameResolution<T> playerNameResolution = new PlayerNameResolution<T>(); playerNameResolution.Status = PlayerNameResolutionStatus.Found; playerNameResolution.Value = value; playerNameResolution.Candidates = new T[1] { value }; return playerNameResolution; } } internal enum TeleportAutoCompleteMode { None, Teleport, Delete, Player } internal sealed class TeleportAutoCompleteContext { internal TeleportAutoCompleteMode Mode { get; set; } internal string Prefix { get; set; } = string.Empty; internal string Query { get; set; } = string.Empty; internal bool IsEnabled => Mode != TeleportAutoCompleteMode.None; } internal static class TeleportAutoCompleteContextParser { internal static TeleportAutoCompleteContext Parse(string? text) { if (string.IsNullOrEmpty(text) || text.Length < 3 || text[0] != '/' || !text.Substring(1, 2).Equals("tp", StringComparison.OrdinalIgnoreCase) || (text.Length > 3 && !char.IsWhiteSpace(text[3]))) { return None(); } int i; for (i = 3; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } if (i >= text.Length) { return Enabled(TeleportAutoCompleteMode.Teleport, EnsureTrailingSpace(text), string.Empty); } string text2 = text.Substring(i); if (StartsOperation(text2, "create", out var afterOperation)) { return None(); } if (StartsOperation(text2, "list", out afterOperation) || StartsOperation(text2, "help", out afterOperation)) { return None(); } if (StartsOperation(text2, "player", out var afterOperation2)) { int j; for (j = i + afterOperation2; j < text.Length && char.IsWhiteSpace(text[j]); j++) { } string prefix = ((j < text.Length) ? text.Substring(0, j) : EnsureTrailingSpace(text)); return Enabled(TeleportAutoCompleteMode.Player, prefix, (j < text.Length) ? text.Substring(j) : string.Empty); } if (StartsOperation(text2, "delete", out var afterOperation3)) { int k; for (k = i + afterOperation3; k < text.Length && char.IsWhiteSpace(text[k]); k++) { } string prefix2 = ((k < text.Length) ? text.Substring(0, k) : EnsureTrailingSpace(text)); return Enabled(TeleportAutoCompleteMode.Delete, prefix2, (k < text.Length) ? text.Substring(k) : string.Empty); } return Enabled(TeleportAutoCompleteMode.Teleport, text.Substring(0, i), text2); } private static bool StartsOperation(string text, string operation, out int afterOperation) { afterOperation = operation.Length; if (!text.Equals(operation, StringComparison.OrdinalIgnoreCase)) { if (text.Length > operation.Length && text.StartsWith(operation, StringComparison.OrdinalIgnoreCase)) { return char.IsWhiteSpace(text[operation.Length]); } return false; } return true; } private static string EnsureTrailingSpace(string text) { if (text.Length <= 0 || !char.IsWhiteSpace(text[text.Length - 1])) { return text + " "; } return text; } private static TeleportAutoCompleteContext None() { return new TeleportAutoCompleteContext(); } private static TeleportAutoCompleteContext Enabled(TeleportAutoCompleteMode mode, string prefix, string query) { return new TeleportAutoCompleteContext { Mode = mode, Prefix = prefix, Query = query }; } } internal sealed class TeleportAutoCompleteController { private readonly TeleportMarkerService _markers; private readonly ConnectedPlayerService _players; private TeleportAutoCompleteSession? _session; private PlayerAutoCompleteSession? _playerSession; internal TeleportAutoCompleteController(TeleportMarkerService markers, ConnectedPlayerService players) { _markers = markers; _players = players; } internal bool TryComplete(string text, int caretPosition, bool reverse, out string completedText) { completedText = text; if (caretPosition != text.Length || !PlayerIdentityService.TryGet(out string playerId) || !WorldIdentityService.TryGet(out string worldId)) { Cancel("cursor or game context changed"); return false; } TeleportAutoCompleteContext teleportAutoCompleteContext = TeleportAutoCompleteContextParser.Parse(text); if (teleportAutoCompleteContext.Mode == TeleportAutoCompleteMode.Player) { return TryCompletePlayer(text, worldId, teleportAutoCompleteContext, reverse, out completedText); } _playerSession = null; if (_session != null && _session.CompletedText == text && _session.PlayerId == playerId && _session.WorldId == worldId && _session.MarkerRevision == _markers.Revision) { _session.Index = Wrap(_session.Index + ((!reverse) ? 1 : (-1)), _session.Suggestions.Count); completedText = _session.Prefix + _session.Suggestions[_session.Index].Name; _session.CompletedText = completedText; DebugSelection(); return true; } Cancel("text, world, player, or marker list changed"); if (!teleportAutoCompleteContext.IsEnabled) { return false; } List<TeleportMarker> list = TeleportMarkerMatcher.Match(_markers.List(playerId, worldId), teleportAutoCompleteContext.Query, TeleportMarkerConfig.AutoCompleteFuzzySearch.Value, Math.Max(0, TeleportMarkerConfig.AutoCompleteMaxSuggestions.Value)); if (list.Count == 0) { return false; } int index = (reverse ? (list.Count - 1) : 0); completedText = teleportAutoCompleteContext.Prefix + list[index].Name; _session = new TeleportAutoCompleteSession { OriginalQuery = teleportAutoCompleteContext.Query, Prefix = teleportAutoCompleteContext.Prefix, Suggestions = list, Index = index, CompletedText = completedText, PlayerId = playerId, WorldId = worldId, MarkerRevision = _markers.Revision }; if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)string.Format("Autocomplete session started: query='{0}', suggestions={1} [{2}].", teleportAutoCompleteContext.Query, list.Count, string.Join(", ", list.Select((TeleportMarker x) => x.Name)))); } DebugSelection(); return true; } internal void Cancel(string reason) { if (_session != null && ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)("Autocomplete session ended: " + reason + ".")); } _session = null; _playerSession = null; } private void DebugSelection() { if (_session != null && ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Autocomplete selected {_session.Index + 1}/{_session.Suggestions.Count}: {_session.Suggestions[_session.Index].Name}."); } } private static int Wrap(int value, int count) { return (value % count + count) % count; } private bool TryCompletePlayer(string text, string worldId, TeleportAutoCompleteContext context, bool reverse, out string completed) { completed = text; List<ConnectedPlayerInfo> list = _players.ListOthers(); string text2 = string.Join("|", list.Select((ConnectedPlayerInfo x) => ((object)x.CharacterId/*cast due to .constrained prefix*/).ToString() + ":" + x.Name)); if (_playerSession != null && _playerSession.CompletedText == text && _playerSession.WorldId == worldId && _playerSession.ConnectionSignature == text2) { _playerSession.Index = Wrap(_playerSession.Index + ((!reverse) ? 1 : (-1)), _playerSession.Suggestions.Count); completed = _playerSession.Prefix + _playerSession.Suggestions[_playerSession.Index].Name; _playerSession.CompletedText = completed; return true; } _playerSession = null; List<ConnectedPlayerInfo> list2 = NameMatcher.Match(list, (ConnectedPlayerInfo x) => x.Name, context.Query, TeleportMarkerConfig.AutoCompleteFuzzySearch.Value, Math.Max(0, TeleportMarkerConfig.AutoCompleteMaxSuggestions.Value)); if (list2.Count == 0) { return false; } int index = (reverse ? (list2.Count - 1) : 0); completed = context.Prefix + list2[index].Name; _playerSession = new PlayerAutoCompleteSession { Prefix = context.Prefix, Suggestions = list2, Index = index, CompletedText = completed, WorldId = worldId, ConnectionSignature = text2 }; if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Player autocomplete query='{context.Query}', matches={list2.Count}, selected='{list2[index].Name}'."); } return true; } } internal sealed class TeleportAutoCompleteSession { internal string OriginalQuery { get; set; } = string.Empty; internal string Prefix { get; set; } = string.Empty; internal List<TeleportMarker> Suggestions { get; set; } = new List<TeleportMarker>(); internal int Index { get; set; } internal string CompletedText { get; set; } = string.Empty; internal string PlayerId { get; set; } = string.Empty; internal string WorldId { get; set; } = string.Empty; internal int MarkerRevision { get; set; } } internal sealed class TeleportCommandHandler { private readonly TeleportMarkerService _markers; private readonly TeleportService _teleport; private readonly ConnectedPlayerService _connectedPlayers; private readonly PlayerTeleportService _playerTeleport; internal TeleportCommandHandler(TeleportMarkerService markers, TeleportService teleport, ConnectedPlayerService connectedPlayers) { _markers = markers; _teleport = teleport; _connectedPlayers = connectedPlayers; _playerTeleport = new PlayerTeleportService(connectedPlayers, teleport); } internal void Handle(ConsoleEventArgs args) { try { if (!TeleportMarkerConfig.Enabled.Value) { Reply(args, TeleportMessages.Disabled); return; } if (!TeleportMarkerConfig.AllowInMultiplayer.Value && !ZNet.IsSinglePlayer && ZNet.GetWorldIfIsHost() == null) { Reply(args, TeleportMessages.MultiplayerDisabled); return; } TeleportCommandResult teleportCommandResult = TeleportCommandParser.Parse(args.FullLine); if (teleportCommandResult.Operation == TeleportCommandOperation.Invalid) { Reply(args, TeleportMessages.Invalid); return; } if (teleportCommandResult.Operation == TeleportCommandOperation.Help) { Reply(args, TeleportMessages.Help(TeleportMarkerConfig.AutoCompleteEnabled.Value)); return; } if (!TryContext(out Player player, out string playerId, out string worldId)) { Reply(args, TeleportMessages.PlayerUnavailable); return; } if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Teleport marker command identified: operation={teleportCommandResult.Operation}, world loaded, player available."); } switch (teleportCommandResult.Operation) { case TeleportCommandOperation.Create: Create(args, player, playerId, worldId, teleportCommandResult.MarkerName); break; case TeleportCommandOperation.Delete: Delete(args, playerId, worldId, teleportCommandResult.MarkerName); break; case TeleportCommandOperation.List: List(args, playerId, worldId); break; case TeleportCommandOperation.Teleport: Teleport(args, player, playerId, worldId, teleportCommandResult.MarkerName); break; case TeleportCommandOperation.Player: TeleportPlayer(args, player, playerId, teleportCommandResult.MarkerName); break; default: Reply(args, TeleportMessages.Invalid); break; } } catch (Exception arg) { Plugin.Log.LogError((object)$"Teleport marker command failed.\n{arg}"); Reply(args, TeleportMessages.InternalError); } } private void Create(ConsoleEventArgs args, Player player, string playerId, string worldId, string rawName) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) string name = TeleportMarkerService.NormalizeName(rawName); if (TeleportCommandParser.IsReservedName(name)) { Reply(args, TeleportMessages.ReservedName(name)); return; } Vector3 position = ((Component)player).transform.position; Quaternion rotation = ((Component)player).transform.rotation; TeleportMarker marker = new TeleportMarker { Name = name, PlayerId = playerId, WorldId = worldId, CreatedAt = DateTime.UtcNow, CreatedAtUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), PositionX = position.x, PositionY = position.y, PositionZ = position.z, RotationX = rotation.x, RotationY = rotation.y, RotationZ = rotation.z, RotationW = rotation.w }; int num = Math.Max(0, TeleportMarkerConfig.MaxMarkersPerPlayer.Value); MarkerOperationResult markerOperationResult = _markers.Create(marker, num); switch (markerOperationResult.Status) { case MarkerOperationStatus.Success: Reply(args, TeleportMessages.Created(marker)); break; case MarkerOperationStatus.Duplicate: Reply(args, TeleportMessages.Duplicate(name)); break; case MarkerOperationStatus.LimitReached: Reply(args, TeleportMessages.Limit(num)); break; default: LogSaveError(markerOperationResult.Error); Reply(args, TeleportMessages.SaveError); break; } } private void Delete(ConsoleEventArgs args, string playerId, string worldId, string name) { name = TeleportMarkerService.NormalizeName(name); MarkerOperationResult markerOperationResult = _markers.Delete(playerId, worldId, name); if (markerOperationResult.Status == MarkerOperationStatus.Success) { Reply(args, TeleportMessages.Removed(name)); return; } if (markerOperationResult.Status == MarkerOperationStatus.NotFound) { Reply(args, TeleportMessages.NotFound(name)); return; } LogSaveError(markerOperationResult.Error); Reply(args, TeleportMessages.SaveError); } private void List(ConsoleEventArgs args, string playerId, string worldId) { List<TeleportMarker> list = _markers.List(playerId, worldId); Reply(args, (list.Count == 0) ? TeleportMessages.Empty : TeleportMessages.List(list, _markers.CountOtherWorlds(playerId, worldId))); } private void Teleport(ConsoleEventArgs args, Player player, string playerId, string worldId, string name) { name = TeleportMarkerService.NormalizeName(name); TeleportMarker teleportMarker = _markers.Find(playerId, worldId, name); if (teleportMarker == null) { Reply(args, (_markers.FindInOtherWorld(playerId, worldId, name) == null) ? TeleportMessages.NotFound(name) : TeleportMessages.OtherWorld(name)); return; } TeleportAttemptResult teleportAttemptResult = _teleport.Teleport(player, playerId, teleportMarker, Math.Max(0, TeleportMarkerConfig.CooldownSeconds.Value)); switch (teleportAttemptResult.Status) { case TeleportAttemptStatus.Success: Reply(args, TeleportMessages.Teleported(teleportMarker.Name)); break; case TeleportAttemptStatus.Cooldown: Reply(args, TeleportMessages.Cooldown(teleportAttemptResult.RemainingSeconds)); break; case TeleportAttemptStatus.InProgress: Reply(args, TeleportMessages.TeleportInProgress); break; case TeleportAttemptStatus.PlayerUnavailable: Reply(args, TeleportMessages.PlayerUnavailable); break; default: Reply(args, TeleportMessages.InternalError); break; } } private static bool TryContext(out Player? player, out string playerId, out string worldId) { playerId = string.Empty; worldId = string.Empty; player = Player.m_localPlayer; if (player != null && PlayerIdentityService.TryGet(out playerId)) { return WorldIdentityService.TryGet(out worldId); } return false; } private void TeleportPlayer(ConsoleEventArgs args, Player local, string playerId, string query) { query = TeleportMarkerService.NormalizeName(query); if (!TeleportMarkerConfig.TeleportPlayersEnabled.Value) { Reply(args, TeleportMessages.Player("teleport.player.feature_disabled")); return; } if (query.Length == 0) { Reply(args, TeleportMessages.Player("teleport.player.name_required")); return; } if (!_connectedPlayers.IsAvailable) { Reply(args, TeleportMessages.Player("teleport.player.unavailable")); return; } if (NameMatcher.Normalize(local.GetPlayerName()) == NameMatcher.Normalize(query)) { Reply(args, TeleportMessages.Player("teleport.player.self")); return; } List<ConnectedPlayerInfo> list = _connectedPlayers.ListOthers(); if (list.Count == 0) { Reply(args, TeleportMessages.Player("teleport.player.none_connected")); return; } PlayerNameResolution<ConnectedPlayerInfo> playerNameResolution = PlayerNameResolver.Resolve(list, (ConnectedPlayerInfo x) => x.Name, query); if (ModConfig.DebugLogging.Value) { Plugin.Log.LogDebug((object)$"Player teleport query='{query}', matches={playerNameResolution.Candidates.Count}."); } if (playerNameResolution.Status == PlayerNameResolutionStatus.NotFound) { Reply(args, TeleportMessages.Player("teleport.player.not_found", query)); return; } if (playerNameResolution.Status == PlayerNameResolutionStatus.Ambiguous) { if (playerNameResolution.Candidates.Count > 1 && playerNameResolution.Candidates.All((ConnectedPlayerInfo x) => NameMatcher.Normalize(x.Name) == NameMatcher.Normalize(query))) { Reply(args, TeleportMessages.Player("teleport.player.duplicate_name", query)); return; } string text = string.Join("\n", playerNameResolution.Candidates.Select((ConnectedPlayerInfo x) => TeleportMessages.Player("teleport.player.multiple_item", x.Name))); Reply(args, TeleportMessages.Player("teleport.player.multiple_found", text)); return; } ConnectedPlayerInfo value = playerNameResolution.Value; PlayerTeleportResult playerTeleportResult = _playerTeleport.Teleport(local, playerId, value, Math.Max(0, TeleportMarkerConfig.CooldownSeconds.Value)); switch (playerTeleportResult.Status) { case PlayerTeleportStatus.Success: Reply(args, TeleportMessages.Player("teleport.player.success", value.Name)); break; case PlayerTeleportStatus.Disconnected: Reply(args, TeleportMessages.Player("teleport.player.disconnected", value.Name)); break; case PlayerTeleportStatus.PositionUnavailable: Reply(args, TeleportMessages.Player("teleport.player.position_unavailable", value.Name)); break; case PlayerTeleportStatus.PositionNotShared: Reply(args, TeleportMessages.Player("teleport.player.position_not_shared", value.Name)); break; case PlayerTeleportStatus.DestinationBlocked: Reply(args, TeleportMessages.Player("teleport.player.destination_blocked", value.Name)); break; case PlayerTeleportStatus.Cooldown: Reply(args, TeleportMessages.Cooldown(playerTeleportResult.RemainingSeconds)); break; case PlayerTeleportStatus.InProgress: Reply(args, TeleportMessages.TeleportInProgress); break; case PlayerTeleportStatus.PlayerUnavailable: Reply(args, TeleportMessages.PlayerUnavailable); break; default: Reply(args, TeleportMessages.InternalError); break; } } private static void Reply(ConsoleEventArgs args, string text) { Terminal context = args.Context; if (context != null) { context.AddString(text); } } private static void LogSaveError(string? error) { Plugin.Log.LogError((object)("Failed to save teleport markers: " + error)); } } internal static class TeleportCommandParser { internal static bool HasExactPrefix(string? input) { if (string.IsNullOrWhiteSpace(input)) { return false; } string text = input.TrimStart(Array.Empty<char>()); if (text.StartsWith("/", StringComparison.Ordinal)) { text = text.Substring(1); } if (!text.Equals("tp", StringComparison.OrdinalIgnoreCase)) { if (text.Length > 3 && text.StartsWith("tp", StringComparison.OrdinalIgnoreCase)) { return char.IsWhiteSpace(text[2]); } return false; } return true; } internal static TeleportCommandResult Parse(string? input) { if (!HasExactPrefix(input)) { return Invalid(); } string text = input.TrimStart(Array.Empty<char>()); if (text[0] == '/') { text = text.Substring(1); } string text2 = ((text.Length == 2) ? string.Empty : text.Substring(2).Trim()); if (text2.Length == 0) { return Invalid(); } if (TryOperation(text2, "create", out string name)) { if (name.Length != 0) { return Result(TeleportCommandOperation.Create, name); } return Invalid(); } if (TryOperation(text2, "delete", out name)) { if (name.Length != 0) { return Result(TeleportCommandOperation.Delete, name); } return Invalid(); } if (text2.Equals("list", StringComparison.OrdinalIgnoreCase)) { return Result(TeleportCommandOperation.List, string.Empty); } if (TryOperation(text2, "list", out string name2)) { return Invalid(); } if (text2.Equals("help", StringComparison.OrdinalIgnoreCase)) { return Result(TeleportCommandOperation.Help, string.Empty); } if (TryOperation(text2, "help", out name2)) { return Invalid(); } if (TryOperation(text2, "player", out name)) { return Result(TeleportCommandOperation.Player, name); } return Result(TeleportCommandOperation.Teleport, text2); } internal static bool IsReservedName(string name) { if (!name.Equals("create", StringComparison.OrdinalIgnoreCase) && !name.Equals("delete", StringComparison.OrdinalIgnoreCase) && !name.Equals("list", StringComparison.OrdinalIgnoreCase) && !name.Equals("help", StringComparison.OrdinalIgnoreCase)) { return name.Equals("player", StringComparison.OrdinalIgnoreCase); } return true; } private static bool TryOperation(string input, string operation, out string name) { if (input.Equals(operation, StringComparison.OrdinalIgnoreCase)) { name = string.Empty; return true; } if (input.Length > operation.Length && input.StartsWith(operation, StringComparison.OrdinalIgnoreCase) && char.IsWhiteSpace(input[operation.Length])) { name = input.Substring(operation.Length).Trim(); return true; } name = string.Empty; return false; } private static TeleportCommandResult Invalid() { return Result(TeleportCommandOperation.Invalid, string.Empty); } private static TeleportCommandResult Result(TeleportCommandOperation operation, string name) { return new TeleportCommandResult { Operation = operation, MarkerName = name }; } } internal enum TeleportCommandOperation { Invalid, Create, Delete, List, Help, Player, Teleport } internal sealed class TeleportCommandResult { internal TeleportCommandOperation Operation { get; set; } internal string MarkerName { get; set; } = string.Empty; } internal static class TeleportMarkerMatcher { internal static List<TeleportMarker> Match(IEnumerable<TeleportMarker> markers, string query, bool fuzzy, int maximum) { return NameMatcher.Match(markers, (TeleportMarker x) => x.Name, query, fuzzy, maximum); } internal static string Normalize(string value) { return NameMatcher.Normalize(value); } } internal static class TeleportMessages { internal static string Invalid => Get("command.invalid"); internal static string Empty => Get("marker.empty"); internal static string Disabled => Get("marker.disabled"); internal static string MultiplayerDisabled => Get("marker.multiplayer_disabled"); internal static string PlayerUnavailable => Get("marker.player_unavailable"); internal static string TeleportInProgress => Get("marker.in_progress"); internal static string SaveError => Get("marker.save_error"); internal static string InternalError => Get("command.internal_error"); internal static string Created(TeleportMarker marker) { return Get("marker.created", marker.Name, Coordinates(marker)); } internal static string Duplicate(string name) { return Get("marker.duplicate", name); } internal static string Removed(string name) { return Get("marker.removed", name); } internal static string NotFound(string name) { return Get("marker.not_found", name); } internal static string OtherWorld(string name) { return Get("marker.other_world", name); } internal static string Limit(int value) { return Get("marker.limit", value); } internal static string Cooldown(int seconds) { return Get("marker.cooldown", seconds); } internal static string Teleported(string name) { return Get("marker.teleported", name); } internal static string ReservedName(string name) { return Get("command.reserved_name", name); } internal static string Help(bool autocomplete) { string text = string.Join("\n\n", Get("command.help.title"), Get("command.help.create"), Get("command.help.teleport"), Get("command.help.player"), Get("command.help.delete"), Get("command.help.list"), Get("command.help.help")); if (!autocomplete) { return text; } return text + "\n\n" + Get("command.help.autocomplete_tip"); } internal static string List(IReadOnlyList<TeleportMarker> markers, int otherWorldCount) { StringBuilder stringBuilder = new StringBuilder(Get("marker.list.header", markers.Count)); for (int i = 0; i < markers.Count; i++) { stringBuilder.Append($"\n\n{i + 1}. {markers[i].Name} — {Coordinates(markers[i])}"); } if (otherWorldCount > 0) { stringBuilder.Append("\n\n" + Get("marker.other_world_count", otherWorldCount)); } return stringBuilder.ToString(); } private static string Coordinates(TeleportMarker marker) { return string.Format(CultureInfo.CurrentCulture, "X: {0:F2}, Y: {1:F2}, Z: {2:F2}", marker.PositionX, marker.PositionY, marker.PositionZ); } private static string Get(string key, params object[] values) { return LanguageManager.Get(key, values); } internal static string Player(string key, params object[] values) { return Get(key, values); } } }