using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using UnityEngine; using ValheimRelay.Core.Identity; using ValheimRelay.Core.Protocol; using ValheimRelay.Core.Qr; using ValheimRelay.Core.Session; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("ValheimRelay")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+72cd49b324620f005e0fd406d82cf7ff2dce1dd1")] [assembly: AssemblyProduct("ValheimRelay")] [assembly: AssemblyTitle("ValheimRelay")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimRelay.Plugin { public enum PingStyle { Auto, Map, Pin } public sealed class PluginConfig { public const string DefaultRelayUrl = "wss://valheimrelay.bobmitch.com/ws"; public const string DefaultMapUrl = "https://bobmitch.com/valheim"; public ConfigEntry Enabled { get; } public ConfigEntry RelayUrl { get; } public ConfigEntry MapUrl { get; } public ConfigEntry AnnounceInChat { get; } public ConfigEntry ShareMyPosition { get; } public ConfigEntry ShareHealth { get; } public ConfigEntry AcceptMapMarkers { get; } public ConfigEntry ShareMyPings { get; } public ConfigEntry PositionInterval { get; } public ConfigEntry ToggleKey { get; } public ConfigEntry ToggleRequiresShift { get; } public ConfigEntry PingStyle { get; } public bool HasMapLink => MapLink.Normalise(MapUrl.Value).Length > 0; public PluginConfig(ConfigFile file) { //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Expected O, but got Unknown if (file == null) { throw new ArgumentNullException("file"); } Enabled = file.Bind("General", "Enabled", true, "Master switch. Turn this off and the mod does nothing at all."); RelayUrl = file.Bind("General", "RelayUrl", "wss://valheimrelay.bobmitch.com/ws", "Relay WebSocket URL. Leave this alone unless you run your own relay."); MapUrl = file.Bind("General", "MapUrl", "https://bobmitch.com/valheim", "Web map base URL, used to build the copyable link. The code is appended as a fragment."); AnnounceInChat = file.Bind("General", "AnnounceInChat", true, "Print the session code in chat when the session starts. Local only — other players do not see it."); ShareMyPosition = file.Bind("Privacy", "ShareMyPosition", true, "Broadcast your position. Turning this off keeps you in the session and still shows you everyone else."); ShareHealth = file.Bind("Privacy", "ShareHealth", true, "Include health in position updates."); AcceptMapMarkers = file.Bind("Privacy", "AcceptMapMarkers", true, "Let the web map place pins on your in-game minimap."); ShareMyPings = file.Bind("Privacy", "ShareMyPings", true, "Send the pings you make in game to the web map. Separate from ShareMyPosition: a ping is something you chose to do, so turning off the position stream does not turn this off. Pings from other players are never forwarded by you, only your own."); PositionInterval = file.Bind("Performance", "PositionInterval", 1f, new ConfigDescription("Seconds between position updates. Clamped to at least 0.5.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 10f), Array.Empty())); ToggleKey = file.Bind("UI", "ToggleKey", (KeyCode)289, "Shows and hides the relay panel. Held together with Shift unless ToggleRequiresShift is off."); ToggleRequiresShift = file.Bind("UI", "ToggleRequiresShift", true, "Require Shift to be held with ToggleKey. Turn this off for a bare keypress."); PingStyle = file.Bind("UI", "PingStyle", ValheimRelay.Plugin.PingStyle.Auto, "How a ping from the web map is shown. Auto hands it to the game's own ping code, so it looks, sounds and reads exactly like a player's ping. Map draws the minimap marker only. Pin drops a short-lived pin and writes a chat line. Drop down a level if a game update breaks the one above it."); } public SessionOptions ToSessionOptions() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_001b: 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_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown SessionOptions val = new SessionOptions { RelayUrl = NormaliseRelayUrl(RelayUrl.Value), PositionInterval = TimeSpan.FromSeconds(PositionInterval.Value), SharePosition = ShareMyPosition.Value, SharePings = ShareMyPings.Value }; val.Normalise(); return val; } public static string NormaliseRelayUrl(string raw) { return RelayUrl.Normalise(raw, "wss://valheimrelay.bobmitch.com/ws"); } public string BuildShareText(string code, string? seed = null) { return MapLink.Build(MapUrl.Value, code, seed); } } public sealed class GameBridge : IPeerView { private readonly struct PingPin { public object Pin { get; } public float Expiry { get; } public PingPin(object pin, float expiry) { Pin = pin; Expiry = expiry; } } private readonly ILog _log; private readonly Func? _pingStyle; private const float PingLifetimeSeconds = 6f; private const BindingFlags AnyInstanceMethod = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private readonly List _pingPins = new List(); private readonly PingEcho _pingEcho = new PingEcho((TimeSpan?)null, 2.0); private MethodInfo? _chatPing; private bool _chatPingResolved; private MethodInfo? _groundHeight; private MethodInfo? _groundHeightDirect; private MethodInfo? _generatedHeight; private bool _heightResolved; private MethodInfo? _addPing; private bool _addPingResolved; private bool _pingApiDescribed; private string? _pingPathLogged; private static readonly PinType PingPinType = ResolvePingPinType(); public static bool HasLocalPlayer => (Object)(object)Player.m_localPlayer != (Object)null; public static bool IsWorldLoaded => (Object)(object)ZNet.instance != (Object)null; public string PlayerName { get { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return "Viking"; } string playerName = localPlayer.GetPlayerName(); if (!string.IsNullOrEmpty(playerName)) { return playerName; } return "Viking"; } } public string ProfileId { get { try { Game instance = Game.instance; PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null); if (val == null) { return "unknown-profile"; } return val.GetPlayerID().ToString(CultureInfo.InvariantCulture); } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the player profile id: " + ex.Message); return "unknown-profile"; } } } public bool IsHost { get { try { return (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); } catch (Exception) { return false; } } } public long SelfPeerId { get { try { return ((Object)(object)ZNet.instance == (Object)null) ? 0 : ZNet.GetUID(); } catch (Exception) { return 0L; } } } public IReadOnlyList PeerIds { get { List list = new List(); try { ZNet instance = ZNet.instance; List list2 = ((instance != null) ? instance.GetPeers() : null); if (list2 == null) { return list; } foreach (ZNetPeer item in list2) { if (item != null) { list.Add(item.m_uid); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the peer list: " + ex.Message); } return list; } } public static bool IsRenderingPing { get; private set; } private static TimeSpan Now => TimeSpan.FromSeconds(Time.realtimeSinceStartup); public GameBridge(ILog log, Func? pingStyle = null) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown _log = log ?? throw new ArgumentNullException("log"); _pingStyle = pingStyle; } public WorldInfo ReadWorld() { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: 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_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return default(WorldInfo); } string worldName = instance.GetWorldName(); string text = instance.GetWorldUID().ToString(CultureInfo.InvariantCulture); string text2 = null; long num = 0L; WorldGenerator instance2 = WorldGenerator.instance; if (instance2?.m_world != null) { text2 = instance2.m_world.m_seedName; num = instance2.m_world.m_seed; } return new WorldInfo(worldName, text2, num, text); } catch (Exception ex) { LogExtensions.Warn(_log, "could not read world information: " + ex.Message); return default(WorldInfo); } } public bool TryReadPosition(bool includeHealth, long timestampMs, out PositionSample sample) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: 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_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0076: 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_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) sample = default(PositionSample); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return false; } try { Transform transform = ((Component)localPlayer).transform; Vector3 position = transform.position; Quaternion rotation = transform.rotation; float y = ((Quaternion)(ref rotation)).eulerAngles.y; int num = Mathf.RoundToInt(((Character)localPlayer).GetHealth()); int num2 = Mathf.RoundToInt(((Character)localPlayer).GetMaxHealth()); bool flag = ((Character)localPlayer).IsDead() || num <= 0; sample = new PositionSample((double)position.x, (double)position.z, (double)position.y, NormaliseDegrees(y), ReadBiome(position.x, position.z), num, num2, includeHealth, flag, timestampMs); return true; } catch (Exception ex) { LogExtensions.Warn(_log, "could not read the local player: " + ex.Message); return false; } } private string? ReadBiome(float x, float z) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) try { WorldGenerator instance = WorldGenerator.instance; if (instance == null) { return null; } return ((object)instance.GetBiome(x, z, 0.02f, false)/*cast due to .constrained prefix*/).ToString(); } catch (Exception) { return null; } } private static double NormaliseDegrees(double degrees) { double num = degrees % 360.0; if (!(num < 0.0)) { return num; } return num + 360.0; } public static PinType ToPinType(string? icon) { return (PinType)(MarkerIcons.Normalise(icon) switch { "ore" => 3, "boss" => 9, "home" => 1, "death" => 4, "danger" => 2, _ => 0, }); } public object? AddPin(double x, double z, string? label, string? icon) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) return AddPinAt(new Vector3((float)x, 0f, (float)z), ToPinType(icon), label ?? string.Empty); } private object? AddPinAt(Vector3 position, PinType type, string label) { //IL_0014: 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_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { Minimap instance = Minimap.instance; if ((Object)(object)instance == (Object)null) { return null; } return instance.AddPin(position, type, label, false, false, 0L, default(PlatformUserID)); } catch (Exception ex) { LogExtensions.Warn(_log, "could not add a map pin: " + ex.Message); return null; } } public void RemovePin(object? pin) { PinData val = (PinData)((pin is PinData) ? pin : null); if (val == null) { return; } try { Minimap instance = Minimap.instance; if (instance != null) { instance.RemovePin(val); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not remove a map pin: " + ex.Message); } } public void ShowPing(double x, double z, string? who) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)Minimap.instance == (Object)null) && !_pingEcho.ShouldSuppress(x, z, Now)) { DescribePingApi(); Vector3 position = default(Vector3); ((Vector3)(ref position))..ctor((float)x, GroundHeight((float)x, (float)z), (float)z); string name = (string.IsNullOrEmpty(who) ? "Ping" : who); PingStyle pingStyle = ReadPingStyle(); if ((pingStyle != PingStyle.Auto || !TryVanillaPing(position, name)) && (pingStyle == PingStyle.Pin || !TryMinimapPing(position, name))) { ShowFallbackPing(position, name); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not show a ping: " + ex.Message); } } public void NoteGamePing(double x, double z) { _pingEcho.Observe(x, z, Now); } public void ExpirePings() { if (_pingPins.Count == 0) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; for (int num = _pingPins.Count - 1; num >= 0; num--) { if (!(_pingPins[num].Expiry > realtimeSinceStartup)) { RemovePin(_pingPins[num].Pin); _pingPins.RemoveAt(num); } } } public void ClearPings() { foreach (PingPin pingPin in _pingPins) { RemovePin(pingPin.Pin); } _pingPins.Clear(); _pingEcho.Clear(); } private bool TryVanillaPing(Vector3 position, string name) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) Chat instance = Chat.instance; if ((Object)(object)instance == (Object)null) { return false; } MethodInfo methodInfo = ResolveChatPing(); if (methodInfo == null) { return false; } object[] array = BuildChatPingArgs(methodInfo, position, name); if (array == null) { return false; } IsRenderingPing = true; try { methodInfo.Invoke(instance, array); LogPingPath("the game's own chat path — " + Describe(methodInfo)); return true; } catch (Exception ex) { _chatPing = null; LogExtensions.Warn(_log, "the game's ping path failed, falling back to the minimap: " + Unwrap(ex).Message); return false; } finally { IsRenderingPing = false; } } private bool TryMinimapPing(Vector3 position, string name) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) MethodInfo methodInfo = ResolveAddPing(); if (methodInfo == null) { return false; } try { methodInfo.Invoke(Minimap.instance, BuildPingArgs(methodInfo, position, name)); LogPingPath("Minimap.AddPing — the marker only, with no sound or world text"); return true; } catch (Exception ex) { _addPing = null; LogExtensions.Warn(_log, "Minimap.AddPing failed, so pings will show as pins: " + Unwrap(ex).Message); return false; } } private PingStyle ReadPingStyle() { try { return _pingStyle?.Invoke() ?? PingStyle.Auto; } catch (Exception) { return PingStyle.Auto; } } private void LogPingPath(string path) { if (!string.Equals(_pingPathLogged, path, StringComparison.Ordinal)) { _pingPathLogged = path; LogExtensions.Info(_log, "pings are being shown through " + path); } } private void ShowFallbackPing(Vector3 position, string name) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_004d: 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) object obj = AddPinAt(position, PingPinType, name); if (obj != null) { _pingPins.Add(new PingPin(obj, Time.realtimeSinceStartup + 6f)); } LogPingPath("a short-lived pin and a chat line"); LocalMessage(name + " pinged " + Mathf.RoundToInt(position.x).ToString(CultureInfo.InvariantCulture) + ", " + Mathf.RoundToInt(position.z).ToString(CultureInfo.InvariantCulture)); } private float GroundHeight(float x, float z) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) ResolveHeight(); try { Vector3 val = default(Vector3); ((Vector3)(ref val))..ctor(x, 5000f, z); ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null && _groundHeight != null) { object[] array = Fill(_groundHeight, val); object obj = _groundHeight.Invoke(instance, array); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0 && array[1] is float result) { return result; } } if ((Object)(object)instance != (Object)null && _groundHeightDirect != null && _groundHeightDirect.Invoke(instance, Fill(_groundHeightDirect, val)) is float num2 && !Mathf.Approximately(num2, val.y) && Mathf.Abs(num2) > 0.001f) { return num2; } WorldGenerator instance2 = WorldGenerator.instance; if (instance2 != null && _generatedHeight != null && _generatedHeight.Invoke(instance2, Fill(_generatedHeight, x, z)) is float num3 && !float.IsNaN(num3)) { return num3; } } catch (Exception ex) { LogExtensions.Debug(_log, "could not read the ground height: " + ex.Message); } return 0f; } private void ResolveHeight() { if (_heightResolved) { return; } _heightResolved = true; try { string[] array = new string[2] { "GetGroundHeight", "GetSolidHeight" }; MethodInfo[] methods; foreach (string b in array) { methods = typeof(ZoneSystem).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, b, StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length < 1 || parameters[0].ParameterType != typeof(Vector3)) { continue; } if (methodInfo.ReturnType == typeof(bool) && parameters.Length == 2 && parameters[1].IsOut && parameters[1].ParameterType.GetElementType() == typeof(float)) { if (_groundHeight == null) { _groundHeight = methodInfo; } } else if (methodInfo.ReturnType == typeof(float) && parameters.Length == 1 && _groundHeightDirect == null) { _groundHeightDirect = methodInfo; } } if (_groundHeight != null || _groundHeightDirect != null) { break; } } methods = typeof(WorldGenerator).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo2 in methods) { if (!string.Equals(methodInfo2.Name, "GetHeight", StringComparison.Ordinal) || methodInfo2.ReturnType != typeof(float)) { continue; } ParameterInfo[] parameters2 = methodInfo2.GetParameters(); if (parameters2.Length < 2 || parameters2[0].ParameterType != typeof(float) || parameters2[1].ParameterType != typeof(float)) { continue; } bool flag = true; for (int k = 2; k < parameters2.Length; k++) { if (!parameters2[k].IsOut && !parameters2[k].IsOptional) { flag = false; break; } } if (flag) { _generatedHeight = methodInfo2; break; } } if (_groundHeight == null && _groundHeightDirect == null && _generatedHeight == null) { LogExtensions.Info(_log, "no ground-height lookup was found on this build, so pings will sit at sea level."); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up the ground-height API: " + ex.Message); } } private static object[] Fill(MethodInfo method, params object[] leading) { ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { if (i < leading.Length) { array[i] = leading[i]; continue; } Type type = parameters[i].ParameterType; if (type.IsByRef) { type = type.GetElementType(); } array[i] = (type.IsValueType ? Activator.CreateInstance(type) : null); } return array; } private MethodInfo? ResolveChatPing() { if (_chatPingResolved) { return _chatPing; } _chatPingResolved = true; try { _chatPing = FindChatPingMethod(); if (_chatPing != null) { return _chatPing; } LogExtensions.Info(_log, "Chat.OnNewChatMessage is not usable for pings on this build, so pings will be drawn on the map only."); } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up the chat ping path: " + ex.Message); } return _chatPing; } public static MethodInfo? FindChatPingMethod() { MethodInfo[] methods = typeof(Chat).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "OnNewChatMessage", StringComparison.Ordinal)) { continue; } bool flag = false; bool flag2 = false; ParameterInfo[] parameters = methodInfo.GetParameters(); foreach (ParameterInfo parameterInfo in parameters) { if (parameterInfo.ParameterType == typeof(Vector3)) { flag = true; } else if (parameterInfo.ParameterType.IsEnum && HasEnumName(parameterInfo.ParameterType, "Ping")) { flag2 = true; } } if (flag && flag2) { return methodInfo; } } return null; } public static bool TryReadPingArgs(object[]? args, out Vector3 position, out long senderId) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) position = default(Vector3); senderId = 0L; if (args == null) { return false; } bool flag = false; bool flag2 = false; bool flag3 = false; foreach (object obj in args) { if (!(obj is Vector3 val)) { if (!(obj is long num)) { if (obj is Enum obj2 && !flag2) { flag2 = string.Equals(obj2.ToString(), "Ping", StringComparison.Ordinal); } } else if (!flag3) { senderId = num; flag3 = true; } } else if (!flag) { position = val; flag = true; } } return flag && flag2; } private object[]? BuildChatPingArgs(MethodInfo method, Vector3 position, string name) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; bool flag = false; ParameterInfo[] array2 = parameters; for (int i = 0; i < array2.Length; i++) { if (IsUserInfoLike(array2[i].ParameterType)) { flag = true; break; } } int num = ((!flag) ? 1 : 0); int num2 = (flag ? 1 : 2); int num3 = 0; for (int j = 0; j < parameters.Length; j++) { Type parameterType = parameters[j].ParameterType; if (parameterType == typeof(Vector3)) { array[j] = position; } else if (parameterType == typeof(long)) { array[j] = PingSenderId(name); } else if (parameterType.IsEnum) { if (!TryParseEnum(parameterType, "Ping", out object value)) { return null; } array[j] = value; } else if (parameterType == typeof(string)) { num3++; array[j] = ((num3 == num) ? name : ((num3 == num2) ? "Ping" : string.Empty)); } else if (IsUserInfoLike(parameterType)) { object obj = BuildUser(parameterType, name); if (obj == null) { return null; } array[j] = obj; } else { array[j] = (parameterType.IsValueType ? Activator.CreateInstance(parameterType) : null); } } return array; } private static object? BuildUser(Type type, string name) { object obj = null; try { MethodInfo method = type.GetMethod("GetLocalUser", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); if (method != null && type.IsAssignableFrom(method.ReturnType)) { obj = method.Invoke(null, null); } } catch (Exception) { obj = null; } if (obj == null) { try { obj = Activator.CreateInstance(type); } catch (Exception) { return null; } } if (obj == null) { return null; } SetStringMember(obj, "Name", name); SetStringMember(obj, "m_name", name); SetStringMember(obj, "Gamertag", name); SetStringMember(obj, "m_gamertag", name); return obj; } private static void SetStringMember(object target, string member, string value) { try { Type type = target.GetType(); FieldInfo field = type.GetField(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.FieldType == typeof(string) && !field.IsInitOnly) { field.SetValue(target, value); return; } PropertyInfo property = type.GetProperty(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.PropertyType == typeof(string) && property.CanWrite) { property.SetValue(target, value, null); } } catch (Exception) { } } private static long PingSenderId(string name) { return name.GetHashCode(); } private static bool IsUserInfoLike(Type type) { if (!type.IsPrimitive && !type.IsEnum && type != typeof(string) && type != typeof(Vector3)) { return !typeof(Object).IsAssignableFrom(type); } return false; } private static bool HasEnumName(Type type, string name) { string[] names = Enum.GetNames(type); for (int i = 0; i < names.Length; i++) { if (string.Equals(names[i], name, StringComparison.Ordinal)) { return true; } } return false; } private static bool TryParseEnum(Type type, string name, out object? value) { value = null; if (!HasEnumName(type, name)) { return false; } value = Enum.Parse(type, name); return true; } private void DescribePingApi() { if (_pingApiDescribed) { return; } _pingApiDescribed = true; try { StringBuilder stringBuilder = new StringBuilder("ping API on this game build:"); AppendMethods(stringBuilder, typeof(Chat), "OnNewChatMessage", "AddInworldText", "SendPing", "RPC_ChatMessage"); AppendMethods(stringBuilder, typeof(Minimap), "AddPing", "ShowPointOnMap"); AppendMethods(stringBuilder, typeof(ZoneSystem), "GetGroundHeight", "GetSolidHeight"); AppendMethods(stringBuilder, typeof(WorldGenerator), "GetHeight"); AppendEffectFields(stringBuilder, typeof(Chat)); AppendEffectFields(stringBuilder, typeof(Minimap)); LogExtensions.Info(_log, stringBuilder.ToString()); } catch (Exception ex) { LogExtensions.Debug(_log, "could not describe the ping API: " + ex.Message); } } private static void AppendMethods(StringBuilder report, Type type, params string[] names) { MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { foreach (string b in names) { if (string.Equals(methodInfo.Name, b, StringComparison.Ordinal)) { report.Append("\n ").Append(type.Name).Append('.') .Append(Describe(methodInfo)); break; } } } } private static void AppendEffectFields(StringBuilder report, Type type) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType.Name.IndexOf("Effect", StringComparison.OrdinalIgnoreCase) >= 0) { report.Append("\n ").Append(type.Name).Append('.') .Append(fieldInfo.Name) .Append(" : ") .Append(fieldInfo.FieldType.Name); } } } private static string Describe(MethodInfo method) { StringBuilder stringBuilder = new StringBuilder(method.Name).Append('('); ParameterInfo[] parameters = method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { if (i > 0) { stringBuilder.Append(", "); } stringBuilder.Append(parameters[i].ParameterType.Name); if (parameters[i].IsOptional) { stringBuilder.Append('?'); } } return stringBuilder.Append(')').ToString(); } private MethodInfo? ResolveAddPing() { if (_addPingResolved) { return _addPing; } _addPingResolved = true; try { MethodInfo[] methods = typeof(Minimap).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (!string.Equals(methodInfo.Name, "AddPing", StringComparison.Ordinal)) { continue; } ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length < 2 || parameters[0].ParameterType != typeof(Vector3) || parameters[1].ParameterType != typeof(string)) { continue; } bool flag = true; for (int j = 2; j < parameters.Length; j++) { if (!parameters[j].IsOptional) { flag = false; break; } } if (flag) { _addPing = methodInfo; return _addPing; } } LogExtensions.Warn(_log, "Minimap.AddPing was not found, so inbound pings will show as short-lived pins. If the game has updated, this is the signature to check."); } catch (Exception ex) { LogExtensions.Warn(_log, "could not look up Minimap.AddPing: " + ex.Message); } return _addPing; } private static object[] BuildPingArgs(MethodInfo method, Vector3 position, string name) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) ParameterInfo[] parameters = method.GetParameters(); object[] array = new object[parameters.Length]; array[0] = position; array[1] = name; for (int i = 2; i < parameters.Length; i++) { ParameterInfo parameterInfo = parameters[i]; object obj = parameterInfo.DefaultValue; if (obj == null || obj == DBNull.Value || obj is Missing) { obj = (parameterInfo.ParameterType.IsValueType ? Activator.CreateInstance(parameterInfo.ParameterType) : null); } array[i] = obj; } return array; } private static PinType ResolvePingPinType() { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) string[] array = new string[2] { "Ping", "Shout" }; for (int i = 0; i < array.Length; i++) { if (Enum.TryParse(array[i], out PinType result) && Enum.IsDefined(typeof(PinType), result)) { return result; } } return (PinType)0; } private static Exception Unwrap(Exception ex) { return (ex as TargetInvocationException)?.InnerException ?? ex; } public void LocalMessage(string message) { try { Chat instance = Chat.instance; if (!((Object)(object)instance == (Object)null)) { ((Terminal)instance).AddString("ValheimRelay: " + message); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not write to chat: " + ex.Message); } } } public sealed class GameCodeChannel : IGameChannel { private const string RpcAnnounce = "ValheimRelay_Code"; private const string RpcRequest = "ValheimRelay_CodeRequest"; private const string ChatPrefix = "[vrelay]"; private readonly ILog _log; private readonly Func _chatFallbackEnabled; private bool _registered; private bool _rpcAcknowledged; private bool _useChatFallback; public bool IsReady { get { if (_registered) { return ZRoutedRpc.instance != null; } return false; } } public bool RpcWorks => _rpcAcknowledged; public event Action? CodeAnnounced; public event Action? CodeRequested; public GameCodeChannel(ILog log, Func? chatFallbackEnabled = null) { _log = log ?? throw new ArgumentNullException("log"); _chatFallbackEnabled = chatFallbackEnabled ?? ((Func)(() => true)); } public void Register() { if (_registered) { return; } try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { LogExtensions.Warn(_log, "no ZRoutedRpc yet; the code channel will register later"); return; } instance.Register("ValheimRelay_Code", (Action)OnRpcAnnounce); instance.Register("ValheimRelay_CodeRequest", (Action)OnRpcRequest); _registered = true; LogExtensions.Info(_log, "code channel registered"); } catch (Exception ex) { LogExtensions.Warn(_log, "could not register the code RPC; falling back to chat: " + ex.Message); _useChatFallback = true; } } public void Reset() { _rpcAcknowledged = false; _useChatFallback = false; } public void EnableChatFallback() { if (!_useChatFallback && !_rpcAcknowledged) { _useChatFallback = true; LogExtensions.Info(_log, "no peer answered over RPC; using the chat channel for the session code"); } } public void RequestCode() { try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ValheimRelay_CodeRequest", Array.Empty()); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not ask peers for the code: " + ex.Message); } if (_useChatFallback && _chatFallbackEnabled()) { SendChat("[vrelay] ?"); } } public void AnnounceCode(string code, long epoch) { if (string.IsNullOrEmpty(code)) { return; } try { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "ValheimRelay_Code", new object[2] { code, epoch }); } } catch (Exception ex) { LogExtensions.Warn(_log, "could not announce the code over RPC: " + ex.Message); } if (_useChatFallback && _chatFallbackEnabled()) { SendChat("[vrelay] " + code + " " + epoch.ToString(CultureInfo.InvariantCulture)); } } private void OnRpcAnnounce(long sender, string code, long epoch) { _rpcAcknowledged = true; Raise(code, epoch, sender); } private void OnRpcRequest(long sender) { _rpcAcknowledged = true; this.CodeRequested?.Invoke(); } public bool TryConsumeChat(long sender, string? text) { if (string.IsNullOrEmpty(text)) { return false; } string text2 = text.Trim(); if (!text2.StartsWith("[vrelay]", StringComparison.Ordinal)) { return false; } string text3 = text2.Substring("[vrelay]".Length).Trim(); if (text3 == "?") { this.CodeRequested?.Invoke(); return true; } string[] array = text3.Split(new char[1] { ' ' }); if (array.Length >= 1 && array[0].Length > 0) { long result = 1L; if (array.Length >= 2) { long.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result); } Raise(array[0], result, sender); } return true; } private void SendChat(string message) { try { if (!((Object)(object)Player.m_localPlayer == (Object)null)) { Chat instance = Chat.instance; if (instance != null) { instance.SendText((Type)1, message); } } } catch (Exception ex) { LogExtensions.Warn(_log, "could not send on the chat channel: " + ex.Message); } } private void Raise(string code, long epoch, long sender) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(code)) { this.CodeAnnounced?.Invoke(new CodeAnnouncement(code, epoch, sender)); } } } [BepInPlugin("com.valheimrelay.mod", "ValheimRelay", "0.1.0")] [BepInProcess("valheim.exe")] [BepInProcess("valheim_server.exe")] public sealed class ValheimRelayPlugin : BaseUnityPlugin { public const string PluginId = "com.valheimrelay.mod"; public const string PluginName = "ValheimRelay"; public const string PluginVersion = "0.1.0"; private Harmony? _harmony; public static ValheimRelayPlugin? Instance { get; private set; } public PluginConfig Settings { get; private set; } public BepInExLog Log { get; private set; } public RelayBehaviour? Behaviour { get; private set; } public string SessionStorePath => Path.Combine(Paths.ConfigPath, "ValheimRelay.session.json"); private void Awake() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown Instance = this; Log = new BepInExLog(((BaseUnityPlugin)this).Logger); Settings = new PluginConfig(((BaseUnityPlugin)this).Config); if (!Settings.Enabled.Value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"ValheimRelay is disabled in config; not patching."); return; } try { _harmony = new Harmony("com.valheimrelay.mod"); _harmony.PatchAll(typeof(ValheimRelayPlugin).Assembly); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("ValheimRelay could not apply its patches and will stay dormant. This usually means the game updated. Details: " + ex)); return; } GameObject val = new GameObject("ValheimRelay"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; Behaviour = val.AddComponent(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"ValheimRelay 0.1.0 loaded."); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } Instance = null; } } public sealed class BepInExLog : ILog { private readonly ManualLogSource _source; public BepInExLog(ManualLogSource source) { _source = source ?? throw new ArgumentNullException("source"); } public void Log(LogLevel level, string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected I4, but got Unknown switch ((int)level) { case 0: _source.LogDebug((object)message); break; case 2: _source.LogWarning((object)message); break; case 3: _source.LogError((object)message); break; default: _source.LogInfo((object)message); break; } } } public sealed class FileReclaimStorage : IReclaimStorage { private readonly string _path; private readonly ILog _log; public FileReclaimStorage(string path, ILog log) { _path = path ?? throw new ArgumentNullException("path"); _log = log ?? throw new ArgumentNullException("log"); } public string? Read() { if (!File.Exists(_path)) { return null; } return File.ReadAllText(_path); } public void Write(string contents) { string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } string text = _path + ".tmp"; File.WriteAllText(text, contents); if (File.Exists(_path)) { File.Delete(_path); } File.Move(text, _path); } } public sealed class RelayBehaviour : MonoBehaviour { private readonly Dictionary _pins = new Dictionary(StringComparer.Ordinal); private ValheimRelayPlugin _plugin; private GameBridge _bridge; private GameCodeChannel _channel; private ClientWebSocketTransport _transport; private ReclaimStore _reclaim; private RelaySession? _session; private RelayPanel _panel; private bool _sessionRunning; private float _discoveryDeadline; private bool _fallbackConsidered; private bool _pingSenderWarned; private bool _pingSilenceWarned; private int _pingsForwarded; private int _pingsRejected; private const int PingRejectionsBeforeWarning = 3; public RelaySession? Session => _session; public string? Code { get { RelaySession? session = _session; if (session == null) { return null; } return session.Code; } } public SessionState State { get { //IL_000c: Unknown result type (might be due to invalid IL or missing references) RelaySession? session = _session; if (session == null) { return (SessionState)0; } return session.State; } } public string? ShareText { get { string code = Code; if (code != null) { return _plugin.Settings.BuildShareText(code, WorldSeed); } return null; } } private string? WorldSeed { get { //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) WorldInfo val = _bridge.ReadWorld(); return ((WorldInfo)(ref val)).Seed; } } private void Awake() { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown _plugin = ValheimRelayPlugin.Instance ?? throw new InvalidOperationException("RelayBehaviour created without a plugin"); _bridge = new GameBridge((ILog)(object)_plugin.Log, () => _plugin.Settings.PingStyle.Value); _channel = new GameCodeChannel((ILog)(object)_plugin.Log, () => _plugin.Settings.AnnounceInChat.Value); _transport = new ClientWebSocketTransport((ILog)(object)_plugin.Log, 256); _reclaim = new ReclaimStore((IReclaimStorage)(object)new FileReclaimStorage(_plugin.SessionStorePath, (ILog)(object)_plugin.Log), (ILog)(object)_plugin.Log); _panel = new RelayPanel(this, _plugin.Settings); } public void OnGameStart() { _channel.Register(); } public bool TryConsumeChat(long sender, string? text) { return _channel.TryConsumeChat(sender, text); } public void OnGamePing(Vector3 position, long senderId) { //IL_0006: 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_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) _bridge.NoteGamePing(position.x, position.z); if (!_plugin.Settings.ShareMyPings.Value || !_sessionRunning) { return; } if (!IsLocalPing(senderId)) { WarnIfNothingIsEverForwarded(senderId); return; } _pingsForwarded++; RelaySession? session = _session; if (session != null) { session.SendPing((double)position.x, (double)position.z); } } private void WarnIfNothingIsEverForwarded(long senderId) { if (!_pingSilenceWarned && _pingsForwarded <= 0 && ++_pingsRejected >= 3) { _pingSilenceWarned = true; LogExtensions.Warn((ILog)(object)_plugin.Log, "seen " + _pingsRejected + " pings in game and forwarded none of them: every one read as another player's (self=" + _bridge.SelfPeerId + ", last sender=" + senderId + "). If you are alone in this world that is a bug — pings made here are not reaching the web map, while pings FROM the map still work."); } } private bool IsLocalPing(long senderId) { long selfPeerId = _bridge.SelfPeerId; if (selfPeerId != 0L && senderId != 0L) { return senderId == selfPeerId; } if (!_pingSenderWarned) { _pingSenderWarned = true; LogExtensions.Warn((ILog)(object)_plugin.Log, "could not tell whose ping this is (self=" + selfPeerId + ", sender=" + senderId + "), so pings made in game are being forwarded without that check. If several players here run the mod, the web map may draw one ring per player for a single ping."); } return true; } public void StartSession() { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Expected O, but got Unknown if (!_sessionRunning && _plugin.Settings.Enabled.Value && GameBridge.IsWorldLoaded && GameBridge.HasLocalPlayer) { _channel.Register(); _channel.Reset(); SessionOptions val = _plugin.Settings.ToSessionOptions(); RelaySession? session = _session; if (session != null) { session.Dispose(); } _session = new RelaySession(val, (IRelayTransport)(object)_transport, (IGameChannel)(object)_channel, (IPeerView)(object)_bridge, (IClock)(object)new UnityClock(), (ILog)(object)_plugin.Log, _reclaim, (Func)null); _session.Notice += OnNotice; _session.PingReceived += OnPingReceived; _session.MarkerReceived += OnMarkerReceived; byte[] array = default(byte[]); if (!StableUid.TryDecodeSalt(_reclaim.Salt, ref array)) { LogExtensions.Error((ILog)(object)_plugin.Log, "could not establish an identity salt; not starting a session"); _sessionRunning = false; return; } string text = StableUid.Derive(_bridge.ProfileId, array); _session.Start(new SessionIdentity(_bridge.PlayerName, text, "0.1.0", _bridge.ReadWorld())); _sessionRunning = true; _fallbackConsidered = false; _discoveryDeadline = Time.realtimeSinceStartup + (float)val.DiscoveryWindow.TotalSeconds; } } public void StopSession(string reason) { if (_sessionRunning) { _sessionRunning = false; ClearPins(); RelaySession? session = _session; if (session != null) { session.Stop(reason); } } } private void Update() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) _bridge.ExpirePings(); if (_session == null) { return; } if (Input.GetKeyDown(_plugin.Settings.ToggleKey.Value) && ToggleModifierHeld()) { _panel.Toggle(); } if (!_sessionRunning) { _session.Tick(); return; } if (!_fallbackConsidered && Time.realtimeSinceStartup >= _discoveryDeadline) { _fallbackConsidered = true; if (!_channel.RpcWorks) { _channel.EnableChatFallback(); } } SubmitPosition(); _session.Tick(); } private bool ToggleModifierHeld() { if (!_plugin.Settings.ToggleRequiresShift.Value) { return true; } if (!Input.GetKey((KeyCode)304)) { return Input.GetKey((KeyCode)303); } return true; } private void SubmitPosition() { if (_plugin.Settings.ShareMyPosition.Value && _bridge.TryReadPosition(_plugin.Settings.ShareHealth.Value, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), out var sample)) { _session.SubmitPosition(ref sample); } } private void OnGUI() { _panel.Draw(); } private void OnNotice(SessionNotice notice) { //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_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected I4, but got Unknown NoticeKind kind = notice.Kind; switch ((int)kind) { case 0: if (_plugin.Settings.AnnounceInChat.Value && notice.Code != null) { _bridge.LocalMessage(_plugin.Settings.HasMapLink ? (notice.Code + " · " + _plugin.Settings.BuildShareText(notice.Code, WorldSeed) + " (Shift+F8 copies it and shows a QR)") : ("map code " + notice.Code + " (Shift+F8 for the panel)")); } break; case 1: _bridge.LocalMessage(notice.Message); break; case 4: _bridge.LocalMessage(notice.Message); break; } LogExtensions.Info((ILog)(object)_plugin.Log, notice.Message); } private void OnPingReceived(PingFrame ping) { _bridge.ShowPing(((PingFrame)(ref ping)).X, ((PingFrame)(ref ping)).Z, ((PingFrame)(ref ping)).Name); } private void OnMarkerReceived(MarkerFrame marker) { if (!_plugin.Settings.AcceptMapMarkers.Value) { return; } if (marker.IsRemove) { if (_pins.TryGetValue(marker.Id, out object value)) { _bridge.RemovePin(value); _pins.Remove(marker.Id); } return; } if (_pins.TryGetValue(marker.Id, out object value2)) { _bridge.RemovePin(value2); _pins.Remove(marker.Id); } object obj = _bridge.AddPin(marker.X, marker.Z, marker.Label, marker.Icon); if (obj != null) { _pins[marker.Id] = obj; } } private void ClearPins() { foreach (object value in _pins.Values) { _bridge.RemovePin(value); } _pins.Clear(); _bridge.ClearPings(); } public void RetryAfterRoomFull() { RelaySession? session = _session; if (session != null) { session.Retry(); } } private void OnDestroy() { RelaySession? session = _session; if (session != null) { session.Dispose(); } _transport.Dispose(); _panel.Release(); } } public sealed class UnityClock : IClock { public TimeSpan Elapsed => TimeSpan.FromSeconds(Time.realtimeSinceStartup); public long UnixTimeMilliseconds => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } internal static class QrTexture { private const int QuietZone = 4; internal static Texture2D Create(QrCode qr, int targetPixels) { //IL_0022: 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) //IL_002e: 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_003e: Expected O, but got Unknown //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: 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) int num = qr.Size + 8; int num2 = Mathf.Max(2, Mathf.RoundToInt((float)targetPixels / (float)num)); int num3 = num * num2; Texture2D val = new Texture2D(num3, num3, (TextureFormat)3, false) { filterMode = (FilterMode)0, wrapMode = (TextureWrapMode)1, hideFlags = (HideFlags)61 }; Color32 val2 = default(Color32); ((Color32)(ref val2))..ctor(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); Color32 val3 = default(Color32); ((Color32)(ref val3))..ctor((byte)0, (byte)0, (byte)0, byte.MaxValue); Color32[] array = (Color32[])(object)new Color32[num3 * num3]; for (int i = 0; i < array.Length; i++) { array[i] = val2; } for (int j = 0; j < qr.Size; j++) { for (int k = 0; k < qr.Size; k++) { if (!qr[k, j]) { continue; } int num4 = (k + 4) * num2; int num5 = (num - 1 - (j + 4)) * num2; for (int l = 0; l < num2; l++) { int num6 = (num5 + l) * num3; for (int m = 0; m < num2; m++) { array[num6 + num4 + m] = val3; } } } } val.SetPixels32(array); val.Apply(false, false); return val; } } public sealed class RelayPanel { private const int Width = 330; private const int BaseHeight = 190; private const int QrTargetPixels = 160; private const int QrGap = 10; private readonly RelayBehaviour _behaviour; private readonly PluginConfig _config; private bool _visible; private float _copiedAt = float.NegativeInfinity; private GUIStyle? _codeStyle; private GUIStyle? _noteStyle; private string? _code; private SessionState _state; private string? _shareText; private string? _copiedText; private bool _wasVisible; private Texture2D? _qr; private string? _qrFor; private int _qrPixels; private int Height => 190 + ((!((Object)(object)_qr == (Object)null)) ? (10 + _qrPixels) : 0); public RelayPanel(RelayBehaviour behaviour, PluginConfig config) { _behaviour = behaviour ?? throw new ArgumentNullException("behaviour"); _config = config ?? throw new ArgumentNullException("config"); } public void Toggle() { _visible = !_visible; } public void Release() { ReleaseQr(); } public void Draw() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Invalid comparison between Unknown and I4 //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: 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_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Invalid comparison between Unknown and I4 if (!_config.Enabled.Value) { return; } EnsureStyles(); if ((int)Event.current.type == 8) { Refresh(); } DrawIndicator(); if (!_visible) { return; } int height = Height; float num = Mathf.Max(10f, Mathf.Min(20f, (float)(Screen.height - height) - 10f)); Rect val = default(Rect); ((Rect)(ref val))..ctor((float)(Screen.width - 330 - 20), num, 330f, (float)height); GUI.Box(val, "ValheimRelay"); GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 12f, ((Rect)(ref val)).y + 26f, ((Rect)(ref val)).width - 24f, ((Rect)(ref val)).height - 38f)); if (_code == null) { GUILayout.Label(DescribeState(_state), Array.Empty()); } else { GUILayout.Label(_code, _codeStyle, Array.Empty()); GUILayout.Label(DescribeState(_state), Array.Empty()); GUILayout.BeginHorizontal(Array.Empty()); if (GUILayout.Button((Time.realtimeSinceStartup - _copiedAt < 2f) ? "Copied to clipboard" : CopyButtonLabel(), Array.Empty()) && _shareText != null) { CopyToClipboard(_shareText); } GUILayout.EndHorizontal(); GUILayout.Label("Anyone with this code can watch everyone in this session move, for as long as it lasts. Share it like a link, not a name" + (((Object)(object)_qr == (Object)null) ? "." : " — and don't leave this panel up on stream."), _noteStyle, Array.Empty()); DrawQr(); } if ((int)_state == 6 && GUILayout.Button("Retry", Array.Empty())) { _behaviour.RetryAfterRoomFull(); } GUILayout.EndArea(); } private string CopyButtonLabel() { if (!_config.HasMapLink) { return "Copy code"; } return "Copy map link"; } private void Refresh() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) _code = _behaviour.Code; _state = _behaviour.State; bool flag = _visible && !_wasVisible; _wasVisible = _visible; if (!_visible) { _shareText = null; return; } _shareText = ((_code == null) ? null : _behaviour.ShareText); if (_shareText != null && (flag || _shareText != _copiedText)) { CopyToClipboard(_shareText); } RefreshQr(); } private void CopyToClipboard(string shareText) { _copiedText = shareText; _copiedAt = Time.realtimeSinceStartup; if (GUIUtility.systemCopyBuffer != shareText) { GUIUtility.systemCopyBuffer = shareText; } } private void RefreshQr() { if (_shareText == null || !_config.HasMapLink) { ReleaseQr(); } else if (!(_qrFor == _shareText)) { ReleaseQr(); _qrFor = _shareText; QrCode val = QrCode.Encode(_shareText); if (val != null) { _qr = QrTexture.Create(val, 160); _qrPixels = ((Texture)_qr).width; } } } private void ReleaseQr() { if ((Object)(object)_qr != (Object)null) { Object.Destroy((Object)(object)_qr); } _qr = null; _qrFor = null; _qrPixels = 0; } private void DrawQr() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Invalid comparison between Unknown and I4 //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_qr == (Object)null)) { GUILayout.Space(10f); GUILayout.BeginHorizontal(Array.Empty()); GUILayout.FlexibleSpace(); Rect rect = GUILayoutUtility.GetRect((float)_qrPixels, (float)_qrPixels, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(false) }); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); if ((int)Event.current.type == 7) { ((Rect)(ref rect)).x = Mathf.Round(((Rect)(ref rect)).x); ((Rect)(ref rect)).y = Mathf.Round(((Rect)(ref rect)).y); Color color = GUI.color; GUI.color = Color.white; GUI.DrawTexture(rect, (Texture)(object)_qr); GUI.color = color; } } } private void DrawIndicator() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_005f: 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_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0093: 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_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Invalid comparison between Unknown and I4 //IL_008d: 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_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Invalid comparison between Unknown and I4 //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) if ((int)_state != 0 && (int)_state != 7) { bool value = _config.ShareMyPosition.Value; SessionState state = _state; Color val = (((int)state == 4) ? (value ? new Color(0.49f, 0.78f, 0.89f) : new Color(0.7f, 0.7f, 0.7f)) : (((int)state != 6) ? new Color(0.9f, 0.8f, 0.4f) : new Color(0.9f, 0.45f, 0.45f))); Color color = val; state = _state; string text = (((int)state == 4) ? (value ? "relay ●" : "relay ○ (hidden)") : (((int)state != 6) ? "relay …" : "relay ✕ full")); string text2 = text; Color color2 = GUI.color; GUI.color = color; GUI.Label(new Rect((float)(Screen.width - 130), (float)(Screen.height - 28), 120f, 20f), text2); GUI.color = color2; } } private static string DescribeState(SessionState state) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected I4, but got Unknown return (state - 1) switch { 0 => "looking for a session…", 1 => "creating a session…", 2 => "joining…", 3 => "connected", 4 => "reconnecting…", 5 => "this session is full (16 players)", 6 => "not connected", _ => "idle", }; } private void EnsureStyles() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown if (_codeStyle == null) { _codeStyle = new GUIStyle(GUI.skin.label) { fontSize = 26, fontStyle = (FontStyle)1 }; } if (_noteStyle == null) { _noteStyle = new GUIStyle(GUI.skin.label) { fontSize = 10, wordWrap = true }; } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "ValheimRelay"; public const string PLUGIN_NAME = "ValheimRelay"; public const string PLUGIN_VERSION = "0.1.0"; } } namespace ValheimRelay.Plugin.Patches { [HarmonyPatch] internal static class ChatMessagePatch { [HarmonyPrepare] private static bool Prepare(MethodBase? original) { if (original != null) { return true; } bool num = AccessTools.Method(typeof(Chat), "OnNewChatMessage", (Type[])null, (Type[])null) != null; if (!num) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance == null) { return num; } LogExtensions.Warn((ILog)(object)instance.Log, "Chat.OnNewChatMessage was not found, so the chat fallback for the session code is unavailable. The routed RPC channel still works; if the game has updated, this patch needs its signature checked."); } return num; } [HarmonyTargetMethod] private static MethodBase? TargetMethod() { return AccessTools.Method(typeof(Chat), "OnNewChatMessage", (Type[])null, (Type[])null); } private static bool Prefix(object[] __args) { RelayBehaviour behaviour = PatchHelpers.Behaviour; if ((Object)(object)behaviour == (Object)null) { return true; } try { long num = 0L; string text = null; foreach (object obj in __args) { if (!(obj is long num2)) { if (obj is string text2 && text == null && text2.Length > 0) { text = text2; } } else if (num == 0L) { num = num2; } } if (text == null) { return true; } return !behaviour.TryConsumeChat(num, text); } catch (Exception ex) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "chat patch error: " + ex.Message); } return true; } } } [HarmonyPatch] [HarmonyPriority(800)] internal static class ChatPingPatch { [HarmonyPrepare] private static bool Prepare(MethodBase? original) { if (original != null) { return true; } if (GameBridge.FindChatPingMethod() != null) { return true; } ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "no Chat.OnNewChatMessage overload on this build carries a position and a ping talker type, so pings made in game will not reach the web map. Everything else, including pings FROM the map, is unaffected."); } return false; } [HarmonyTargetMethod] private static MethodBase? TargetMethod() { return GameBridge.FindChatPingMethod(); } private static void Prefix(object[] __args) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) if (GameBridge.IsRenderingPing) { return; } RelayBehaviour behaviour = PatchHelpers.Behaviour; if ((Object)(object)behaviour == (Object)null) { return; } try { if (GameBridge.TryReadPingArgs(__args, out var position, out var senderId)) { behaviour.OnGamePing(position, senderId); } } catch (Exception ex) { ValheimRelayPlugin? instance = ValheimRelayPlugin.Instance; if (instance != null) { LogExtensions.Warn((ILog)(object)instance.Log, "ping capture error: " + ex.Message); } } } } internal static class PatchHelpers { internal static RelayBehaviour? Behaviour => ValheimRelayPlugin.Instance?.Behaviour; } [HarmonyPatch(typeof(Game), "Start")] internal static class GameStartPatch { private static void Postfix() { PatchHelpers.Behaviour?.OnGameStart(); } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class PlayerOnSpawnedPatch { private static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { PatchHelpers.Behaviour?.StartSession(); } } } [HarmonyPatch(typeof(ZNet), "Shutdown")] internal static class ZNetShutdownPatch { private static void Prefix() { PatchHelpers.Behaviour?.StopSession("left the world"); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class GameLogoutPatch { private static void Prefix() { PatchHelpers.Behaviour?.StopSession("logged out"); } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }