using 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 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(); } ZDOID local = ZNet.instance.LocalPlayerCharacterID; List 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 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)ZNet.instance.GetPlayerList()).FirstOrDefault((Func)((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.GetAllPlayers()).FirstOrDefault((Func)((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 = "$item_noteleport"; 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((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$item_noteleport", string.Empty).Replace("$item_noteleport", 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(); 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 x) => x.Key != worldId).SelectMany((KeyValuePair x) => x.Value.Markers).FirstOrDefault((TeleportMarker x) => NamesEqual(x.Name, name))); } internal List List(string playerId, string worldId) { return _repository.Read((TeleportMarkerDatabase database) => (GetWorld(database, playerId, worldId, create: false)?.Markers ?? new List()).OrderBy((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 x) => x.Key != worldId).Sum((KeyValuePair 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 _lastTeleports = new Dictionary(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(Func operation) { lock (_sync) { return operation(_database); } } internal bool MutateAndSave(Func 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 dictionary = (database.Players = new Dictionary()); } foreach (PlayerTeleportMarkers value in database.Players.Values) { PlayerTeleportMarkers current; PlayerTeleportMarkers playerTeleportMarkers = (current = value); if (current.Worlds == null) { Dictionary dictionary3 = (current.Worlds = new Dictionary()); } foreach (WorldTeleportMarkers value2 in playerTeleportMarkers.Worlds.Values) { if (value2.Markers == null) { List list = (value2.Markers = new List()); } } } } 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 Worlds { get; set; } = new Dictionary(); } [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 Players { get; set; } = new Dictionary(); } [DataContract] internal sealed class WorldTeleportMarkers { [DataMember(Order = 1)] public List Markers { get; set; } = new List(); } } namespace AndersonDavid.UnrestrictedMineralPortals.Localization { internal static class EnglishMessages { internal static readonly IReadOnlyDictionary Values = new Dictionary { ["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(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 Values = new Dictionary { ["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 Enabled { get; private set; } internal static ConfigEntry DebugLogging { get; private set; } internal static ConfigEntry Language { get; private set; } internal static void Bind(ConfigFile config) { Enabled = config.Bind("General", "Enabled", true, "Allow restricted items through portals."); DebugLogging = config.Bind("General", "DebugLogging", false, "Enable diagnostic logging."); Language = config.Bind("General", "Language", "auto", "Language used by mod messages. Supported values: auto, pt-BR, en."); } } internal static class TeleportMarkerConfig { internal static ConfigEntry Enabled { get; private set; } internal static ConfigEntry MaxMarkersPerPlayer { get; private set; } internal static ConfigEntry CooldownSeconds { get; private set; } internal static ConfigEntry AllowInMultiplayer { get; private set; } internal static ConfigEntry AutoCompleteEnabled { get; private set; } internal static ConfigEntry AutoCompleteMaxSuggestions { get; private set; } internal static ConfigEntry AutoCompleteFuzzySearch { get; private set; } internal static ConfigEntry TeleportPlayersEnabled { get; private set; } internal static void Bind(ConfigFile config) { Enabled = config.Bind("TeleportMarkers", "Enabled", true, "Enable personal /tp marker commands."); MaxMarkersPerPlayer = config.Bind("TeleportMarkers", "MaxMarkersPerPlayer", 50, "Maximum markers per player per world. Zero means unlimited."); CooldownSeconds = config.Bind("TeleportMarkers", "CooldownSeconds", 3, "Seconds between marker teleports. Zero disables cooldown."); AllowInMultiplayer = config.Bind("TeleportMarkers", "AllowInMultiplayer", true, "Allow marker commands on remote multiplayer servers."); AutoCompleteEnabled = config.Bind("TeleportMarkers", "AutoCompleteEnabled", true, "Complete marker names with Tab in the normal chat."); AutoCompleteMaxSuggestions = config.Bind("TeleportMarkers", "AutoCompleteMaxSuggestions", 20, "Maximum autocomplete results. Zero means unlimited."); AutoCompleteFuzzySearch = config.Bind("TeleportMarkers", "AutoCompleteFuzzySearch", true, "Suggest marker names for small typing mistakes."); TeleportPlayersEnabled = config.Bind("TeleportPlayers", "Enabled", true, "Enable teleporting to other connected players with /tp player playerName."); } } } namespace AndersonDavid.UnrestrictedMineralPortals.Commands { internal static class NameMatcher { internal static List Match(IEnumerable values, Func name, string query, bool fuzzy, int maximum) { string normalizedQuery = Normalize(query); double similarity; IEnumerable 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 Suggestions { get; set; } = new List(); 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 { internal PlayerNameResolutionStatus Status { get; set; } internal T? Value { get; set; } internal IReadOnlyList Candidates { get; set; } = Array.Empty(); } internal static class PlayerNameResolver { internal static PlayerNameResolution Resolve(IEnumerable values, Func name, string query) { List list = values.ToList(); List list2 = list.Where((T x) => name(x) == query).ToList(); if (list2.Count == 1) { return Found(list2[0]); } List 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 list4 = list.Where((T x) => NameMatcher.Normalize(name(x)) == normalized).ToList(); if (list4.Count == 1) { return Found(list4[0]); } List list5 = NameMatcher.Match(list, name, query, fuzzy: false, 0); if (list5.Count == 1) { return Found(list5[0]); } return new PlayerNameResolution { Status = ((list5.Count <= 1) ? PlayerNameResolutionStatus.NotFound : PlayerNameResolutionStatus.Ambiguous), Candidates = list5 }; } private static PlayerNameResolution Found(T value) { PlayerNameResolution playerNameResolution = new PlayerNameResolution(); 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 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 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 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 Suggestions { get; set; } = new List(); 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 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 list = _connectedPlayers.ListOthers(); if (list.Count == 0) { Reply(args, TeleportMessages.Player("teleport.player.none_connected")); return; } PlayerNameResolution 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()); 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()); 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 Match(IEnumerable 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 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); } } }